From 06a4d15fbf1575650054838bbdf46181452fa093 Mon Sep 17 00:00:00 2001 From: Alisson Pereira Date: Fri, 22 Aug 2025 21:55:36 -0300 Subject: [PATCH 1/2] build: install next-connect --- package-lock.json | 26 ++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 27 insertions(+) diff --git a/package-lock.json b/package-lock.json index 07834e6..f475c59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "dotenv": "16.4.4", "dotenv-expand": "11.0.6", "next": "13.1.6", + "next-connect": "1.0.0", "node-pg-migrate": "6.2.2", "pg": "8.11.3", "react": "18.2.0", @@ -1856,6 +1857,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", @@ -7484,6 +7490,18 @@ } } }, + "node_modules/next-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-connect/-/next-connect-1.0.0.tgz", + "integrity": "sha512-FeLURm9MdvzY1SDUGE74tk66mukSqL6MAzxajW7Gqh6DZKBZLrXmXnGWtHJZXkfvoi+V/DUe9Hhtfkl4+nTlYA==", + "dependencies": { + "@tsconfig/node16": "^1.0.3", + "regexparam": "^2.0.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8342,6 +8360,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexparam": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz", + "integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==", + "engines": { + "node": ">=8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index fc3cb17..c09d2ff 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "dotenv": "16.4.4", "dotenv-expand": "11.0.6", "next": "13.1.6", + "next-connect": "1.0.0", "node-pg-migrate": "6.2.2", "pg": "8.11.3", "react": "18.2.0", From 753cf01453ed8ed6133c910fc5d6076b1d2a2c18 Mon Sep 17 00:00:00 2001 From: Alisson Pereira Date: Fri, 22 Aug 2025 22:29:22 -0300 Subject: [PATCH 2/2] refactor: use next-connect in migrations --- infra/controller.js | 26 +++++++ infra/database.js | 9 ++- infra/errors.js | 43 ++++++++++- pages/api/v1/migrations/index.js | 72 ++++++++++-------- pages/api/v1/status/index.js | 80 +++++++++----------- tests/integration/api/v1/status/post.test.js | 26 +++++++ 6 files changed, 176 insertions(+), 80 deletions(-) create mode 100644 infra/controller.js create mode 100644 tests/integration/api/v1/status/post.test.js diff --git a/infra/controller.js b/infra/controller.js new file mode 100644 index 0000000..ade6e18 --- /dev/null +++ b/infra/controller.js @@ -0,0 +1,26 @@ +import { InternalServerError, MethodNotAllowedError } from "infra/errors"; + +function onNoMatchHandler(request, response) { + const publicErrorObject = new MethodNotAllowedError(); + response.status(publicErrorObject.statusCode).json(publicErrorObject); +} + +function onErrorHandler(error, request, response) { + const publicErrorObject = new InternalServerError({ + statusCode: error.statusCode, + cause: error, + }); + + console.error(publicErrorObject); + + response.status(publicErrorObject.statusCode).json(publicErrorObject); +} + +const controller = { + errorHandlers: { + onNoMatch: onNoMatchHandler, + onError: onErrorHandler, + }, +}; + +export default controller; diff --git a/infra/database.js b/infra/database.js index 1bdcee8..58961c9 100644 --- a/infra/database.js +++ b/infra/database.js @@ -1,4 +1,5 @@ import { Client } from "pg"; +import { ServiceError } from "./errors.js"; async function query(queryObject) { let client; @@ -7,9 +8,11 @@ async function query(queryObject) { const result = await client.query(queryObject); return result; } catch (error) { - console.log("\n Erro dentro do catch do database.js:"); - console.error(error); - throw error; + const serviceErrorObject = new ServiceError({ + message: "Erro na conexão com Banco ou na Query.", + cause: error, + }); + throw serviceErrorObject; } finally { await client?.end(); } diff --git a/infra/errors.js b/infra/errors.js index 28d496b..c76a42e 100644 --- a/infra/errors.js +++ b/infra/errors.js @@ -1,11 +1,50 @@ export class InternalServerError extends Error { - constructor({ cause }) { + constructor({ cause, statusCode }) { super("Um erro interno não esperado aconteceu.", { cause, }); this.name = "InternalServerError"; this.action = "Entre em contato com o suporte."; - this.statusCode = 500; + this.statusCode = statusCode || 500; + } + + toJSON() { + return { + name: this.name, + message: this.message, + action: this.action, + status_code: this.statusCode, + }; + } +} + +export class ServiceError extends Error { + constructor({ cause, message }) { + super(message || "Serviço indisponível no momento.", { + cause, + }); + this.name = "ServiceError"; + this.action = "Verifique se o serviço está disponível."; + this.statusCode = 503; + } + + toJSON() { + return { + name: this.name, + message: this.message, + action: this.action, + status_code: this.statusCode, + }; + } +} + +export class MethodNotAllowedError extends Error { + constructor() { + super("Método não permitido para este endpoint."); + this.name = "MethodNotAllowedError"; + this.action = + "Verifique se o método HTTP enviado é válido para este endpoint."; + this.statusCode = 405; } toJSON() { diff --git a/pages/api/v1/migrations/index.js b/pages/api/v1/migrations/index.js index 2b982ba..cb7312c 100644 --- a/pages/api/v1/migrations/index.js +++ b/pages/api/v1/migrations/index.js @@ -1,49 +1,57 @@ +import { createRouter } from "next-connect"; import migrationRunner from "node-pg-migrate"; import { resolve } from "node:path"; import database from "infra/database.js"; +import controller from "infra/controller.js"; -export default async function migrations(request, response) { - const allowedMethods = ["GET", "POST"]; - if (!allowedMethods.includes(request.method)) { - return response.status(405).json({ - error: `Method "${request.method}" not allowed`, - }); - } +const router = createRouter(); + +router.get(getHandler); +router.post(postHandler); + +export default router.handler(controller.errorHandlers); + +const defaultMigrationOptions = { + dryRun: true, + dir: resolve("infra", "migrations"), + direction: "up", + verbose: true, + migrationsTable: "pgmigrations", +}; +async function getHandler(request, response) { let dbClient; try { dbClient = await database.getNewClient(); - const defaultMigrationOptions = { - dbClient: dbClient, - dryRun: true, - dir: resolve("infra", "migrations"), - direction: "up", - verbose: true, - migrationsTable: "pgmigrations", - }; - - if (request.method === "GET") { - const pendingMigrations = await migrationRunner(defaultMigrationOptions); - return response.status(200).json(pendingMigrations); - } + const pendingMigrations = await migrationRunner({ + ...defaultMigrationOptions, + dbClient, + }); + return response.status(200).json(pendingMigrations); + } finally { + await dbClient.end(); + } +} + +async function postHandler(request, response) { + let dbClient; - if (request.method === "POST") { - const migratedMigrations = await migrationRunner({ - ...defaultMigrationOptions, - dryRun: false, - }); + try { + dbClient = await database.getNewClient(); - if (migratedMigrations.length > 0) { - return response.status(201).json(migratedMigrations); - } + const migratedMigrations = await migrationRunner({ + ...defaultMigrationOptions, + dbClient, + dryRun: false, + }); - return response.status(200).json(migratedMigrations); + if (migratedMigrations.length > 0) { + return response.status(201).json(migratedMigrations); } - } catch (error) { - console.error(error); - throw error; + + return response.status(200).json(migratedMigrations); } finally { await dbClient.end(); } diff --git a/pages/api/v1/status/index.js b/pages/api/v1/status/index.js index dcfad1c..1e435b9 100644 --- a/pages/api/v1/status/index.js +++ b/pages/api/v1/status/index.js @@ -1,47 +1,41 @@ +import { createRouter } from "next-connect"; import database from "infra/database"; -import { InternalServerError } from "infra/errors"; - -async function status(request, response) { - try { - const updatedAt = new Date().toISOString(); - - const databaseVersionResult = await database.query("SHOW server_version;"); - const databaseVersionValue = databaseVersionResult.rows[0].server_version; - - const databaseMaxConnectionsResult = await database.query( - "SHOW max_connections;" - ); - const databaseMaxConnectionsValue = - databaseMaxConnectionsResult.rows[0].max_connections; - - const databaseName = process.env.POSTGRES_DB; - const databaseOpenedConnectionsResult = await database.query({ - text: "SELECT count(*)::int FROM pg_stat_activity WHERE datname = $1;", - values: [databaseName], - }); - const databaseOpenedConnectionsValue = - databaseOpenedConnectionsResult.rows[0].count; - - response.status(200).json({ - updated_at: updatedAt, - dependencies: { - database: { - version: databaseVersionValue, - max_connections: parseInt(databaseMaxConnectionsValue), - opened_connections: databaseOpenedConnectionsValue, - }, - }, - }); - } catch (error) { - const publicErrorObject = new InternalServerError({ - cause: error, - }); +import controller from "infra/controller.js"; - console.log("\n Erro dentro do catch do controller:"); - console.error(publicErrorObject); +const router = createRouter(); - response.status(500).json(publicErrorObject); - } -} +router.get(getHandler); + +export default router.handler(controller.errorHandlers); + +async function getHandler(request, response) { + const updatedAt = new Date().toISOString(); + + const databaseVersionResult = await database.query("SHOW server_version;"); + const databaseVersionValue = databaseVersionResult.rows[0].server_version; -export default status; + const databaseMaxConnectionsResult = await database.query( + "SHOW max_connections;" + ); + const databaseMaxConnectionsValue = + databaseMaxConnectionsResult.rows[0].max_connections; + + const databaseName = process.env.POSTGRES_DB; + const databaseOpenedConnectionsResult = await database.query({ + text: "SELECT count(*)::int FROM pg_stat_activity WHERE datname = $1;", + values: [databaseName], + }); + const databaseOpenedConnectionsValue = + databaseOpenedConnectionsResult.rows[0].count; + + response.status(200).json({ + updated_at: updatedAt, + dependencies: { + database: { + version: databaseVersionValue, + max_connections: parseInt(databaseMaxConnectionsValue), + opened_connections: databaseOpenedConnectionsValue, + }, + }, + }); +} diff --git a/tests/integration/api/v1/status/post.test.js b/tests/integration/api/v1/status/post.test.js new file mode 100644 index 0000000..348beac --- /dev/null +++ b/tests/integration/api/v1/status/post.test.js @@ -0,0 +1,26 @@ +import orchestrator from "tests/orchestrator.js"; + +beforeAll(async () => { + await orchestrator.waitForAllServices(); +}); + +describe("POST /api/v1/status", () => { + describe("Anonymous user", () => { + test("Retrieving current system status", async () => { + const response = await fetch("http://localhost:3000/api/v1/status", { + method: "POST", + }); + expect(response.status).toBe(405); + + const responseBody = await response.json(); + + expect(responseBody).toEqual({ + name: "MethodNotAllowedError", + message: "Método não permitido para este endpoint.", + action: + "Verifique se o método HTTP enviado é válido para este endpoint.", + status_code: 405, + }); + }); + }); +});