From 53aa4f2727d6f4ed82a647693e7b0b2b04983840 Mon Sep 17 00:00:00 2001 From: Anthony Cruz Date: Fri, 21 Feb 2025 16:48:30 -0300 Subject: [PATCH 1/6] build: install `next-connect` --- package-lock.json | 29 +++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 30 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7dac987..f27b2eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "dotenv": "16.4.5", "dotenv-expand": "11.0.6", "next": "14.2.5", + "next-connect": "1.0.0", "node-pg-migrate": "7.6.1", "pg": "8.12.0", "react": "18.3.1", @@ -2089,6 +2090,12 @@ "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==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -8550,6 +8557,19 @@ } } }, + "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==", + "license": "MIT", + "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", @@ -9591,6 +9611,15 @@ "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==", + "license": "MIT", + "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 3d99858..12b6798 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "dotenv": "16.4.5", "dotenv-expand": "11.0.6", "next": "14.2.5", + "next-connect": "1.0.0", "node-pg-migrate": "7.6.1", "pg": "8.12.0", "react": "18.3.1", From 03a98f23edccbe80e06b8f72d3476b2bdf6f4b20 Mon Sep 17 00:00:00 2001 From: Anthony Cruz Date: Fri, 21 Feb 2025 17:15:59 -0300 Subject: [PATCH 2/6] feat: refatora o controller de status com `next-connect` --- infra/errors.js | 19 +++++ pages/api/v1/status/index.js | 85 +++++++++++--------- tests/integration/api/v1/status/post.test.js | 25 ++++++ 3 files changed, 91 insertions(+), 38 deletions(-) create mode 100644 tests/integration/api/v1/status/post.test.js diff --git a/infra/errors.js b/infra/errors.js index cdfe39e..9ec53e0 100644 --- a/infra/errors.js +++ b/infra/errors.js @@ -15,3 +15,22 @@ export class InternalServerError extends Error { }; } } + +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() { + return { + name: this.name, + message: this.message, + action: this.action, + status_code: this.statusCode, + }; + } +} diff --git a/pages/api/v1/status/index.js b/pages/api/v1/status/index.js index 597c6ae..d238f47 100644 --- a/pages/api/v1/status/index.js +++ b/pages/api/v1/status/index.js @@ -1,43 +1,52 @@ +import { createRouter } from "next-connect"; import database from "infra/database.js"; -import { InternalServerError } from "infra/errors"; -async function status(request, response) { - try { - const updatedAt = new Date().toISOString(); - - const postgresVersionResult = await database.query("SHOW server_version;"); - const postgresVersion = postgresVersionResult.rows[0].server_version; - - const maxConnectionsResult = await database.query("SHOW max_connections;"); - const maxConnections = parseInt( - maxConnectionsResult.rows[0].max_connections, - ); - - const databaseName = process.env.POSTGRES_DB; - const openedConnectionsResult = await database.query({ - text: `SELECT COUNT(*)::INTEGER AS opened_connections FROM pg_stat_activity WHERE datname = $1;`, - values: [databaseName], - }); - const openedConnections = - openedConnectionsResult.rows[0].opened_connections; - - response.status(200).json({ - updated_at: updatedAt, - dependencies: { - database: { - version: postgresVersion, - max_connections: maxConnections, - opened_connections: openedConnections, - }, - }, - }); - } catch (error) { - const publicErrorObject = new InternalServerError({ cause: error }); +import { InternalServerError, MethodNotAllowedError } from "infra/errors"; + +const router = createRouter(); +router.get(getHandler); +export default router.handler({ + onNoMatch: onNoMatchHandler, + onError: onErrorHandler, +}); + +function onNoMatchHandler(request, response) { + const publicErrorObject = new MethodNotAllowedError(); + response.status(publicErrorObject.statusCode).json(publicErrorObject); +} + +function onErrorHandler(error, request, response) { + const publicErrorObject = new InternalServerError({ cause: error }); - console.log("\n Erro dentro do catch do controller:"); - console.error(publicErrorObject); + console.log("\n Erro dentro do catch do next-connect:"); + console.error(publicErrorObject); - response.status(500).json(publicErrorObject); - } + response.status(500).json(publicErrorObject); } -export default status; +async function getHandler(request, response) { + const updatedAt = new Date().toISOString(); + + const postgresVersionResult = await database.query("SHOW server_version;"); + const postgresVersion = postgresVersionResult.rows[0].server_version; + + const maxConnectionsResult = await database.query("SHOW max_connections;"); + const maxConnections = parseInt(maxConnectionsResult.rows[0].max_connections); + + const databaseName = process.env.POSTGRES_DB; + const openedConnectionsResult = await database.query({ + text: `SELECT COUNT(*)::INTEGER AS opened_connections FROM pg_stat_activity WHERE datname = $1;`, + values: [databaseName], + }); + const openedConnections = openedConnectionsResult.rows[0].opened_connections; + + response.status(200).json({ + updated_at: updatedAt, + dependencies: { + database: { + version: postgresVersion, + max_connections: maxConnections, + opened_connections: openedConnections, + }, + }, + }); +} 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..03a9298 --- /dev/null +++ b/tests/integration/api/v1/status/post.test.js @@ -0,0 +1,25 @@ +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, + }); + }); + }); +}); From c53060762e77b121b58bdb44827736a70a048d50 Mon Sep 17 00:00:00 2001 From: Anthony Cruz Date: Fri, 21 Feb 2025 17:21:36 -0300 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20refatora=C3=A7=C3=A3o=20com=20`?= =?UTF-8?q?next-connect`=20para=20endpoint=20de=20migrations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/api/v1/migrations/index.js | 91 +++++++++++++++++--------------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/pages/api/v1/migrations/index.js b/pages/api/v1/migrations/index.js index f53b6dd..3646753 100644 --- a/pages/api/v1/migrations/index.js +++ b/pages/api/v1/migrations/index.js @@ -1,52 +1,55 @@ import { resolve } from "node:path"; import migrationRunner from "node-pg-migrate"; import database from "infra/database.js"; +import { createRouter } from "next-connect"; +import { InternalServerError, MethodNotAllowedError } from "infra/errors"; + +const router = createRouter(); +router.get(getHandler); +router.post(postHandler); +export default router.handler({ + onNoMatch: onNoMatchHandler, + onError: onErrorHandler, +}); + +const dbClient = await database.getNewClient(); +const defaultMigrationsOptions = { + dbClient: dbClient, + dryRun: true, + dir: resolve("infra", "migrations"), + direction: "up", + verbose: true, + migrationsTable: "pgmigrations", +}; + +function onNoMatchHandler(request, response) { + const publicErrorObject = new MethodNotAllowedError(); + response.status(publicErrorObject.statusCode).json(publicErrorObject); +} -export default async function migrations(request, response) { - let dbClient; - const allowedMethods = ["GET", "POST"]; +function onErrorHandler(error, request, response) { + const publicErrorObject = new InternalServerError({ cause: error }); - if (!allowedMethods.includes(request.method)) { - return response.status(405).json({ - error: `Method "${request.method}" not allowed`, - }); - } + console.log("\n Erro dentro do catch do next-connect:"); + console.error(publicErrorObject); + + response.status(500).json(publicErrorObject); +} - try { - dbClient = await database.getNewClient(); - - const defaultMigrationsOptions = { - dbClient: dbClient, - dryRun: true, - dir: resolve("infra", "migrations"), - direction: "up", - verbose: true, - migrationsTable: "pgmigrations", - }; - - if (request.method === "GET") { - const pendingMigrations = await migrationRunner(defaultMigrationsOptions); - return response.status(200).json(pendingMigrations); - } - - if (request.method === "POST") { - const migratedMigrations = await migrationRunner({ - ...defaultMigrationsOptions, - dryRun: false, - }); - - if (migratedMigrations.length > 0) { - return response.status(201).json(migratedMigrations); - } - - return response.status(200).json(migratedMigrations); - } - - return response.status(405).end(); - } catch (error) { - console.error(error); - throw error; - } finally { - await dbClient.end(); +async function getHandler(request, response) { + const pendingMigrations = await migrationRunner(defaultMigrationsOptions); + return response.status(200).json(pendingMigrations); +} + +async function postHandler(request, response) { + const migratedMigrations = await migrationRunner({ + ...defaultMigrationsOptions, + dryRun: false, + }); + + if (migratedMigrations.length > 0) { + return response.status(201).json(migratedMigrations); } + + return response.status(200).json(migratedMigrations); } From a7b1d280e02ec6819014ee02b3296d887bbf8036 Mon Sep 17 00:00:00 2001 From: Anthony Cruz Date: Fri, 21 Feb 2025 17:39:41 -0300 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20corre=C3=A7=C3=A3o=20na=20refat?= =?UTF-8?q?ora=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/api/v1/migrations/index.js | 37 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/pages/api/v1/migrations/index.js b/pages/api/v1/migrations/index.js index 3646753..7ee9953 100644 --- a/pages/api/v1/migrations/index.js +++ b/pages/api/v1/migrations/index.js @@ -12,9 +12,7 @@ export default router.handler({ onError: onErrorHandler, }); -const dbClient = await database.getNewClient(); const defaultMigrationsOptions = { - dbClient: dbClient, dryRun: true, dir: resolve("infra", "migrations"), direction: "up", @@ -37,19 +35,34 @@ function onErrorHandler(error, request, response) { } async function getHandler(request, response) { - const pendingMigrations = await migrationRunner(defaultMigrationsOptions); - return response.status(200).json(pendingMigrations); + try { + const dbClient = await database.getNewClient(); + const pendingMigrations = await migrationRunner({ + ...defaultMigrationsOptions, + dbClient, + }); + await dbClient.end(); + return response.status(200).json(pendingMigrations); + } finally { + await dbClient.end(); + } } async function postHandler(request, response) { - const migratedMigrations = await migrationRunner({ - ...defaultMigrationsOptions, - dryRun: false, - }); + try { + const dbClient = await database.getNewClient(); + const migratedMigrations = await migrationRunner({ + ...defaultMigrationsOptions, + dbClient, + dryRun: false, + }); - if (migratedMigrations.length > 0) { - return response.status(201).json(migratedMigrations); - } + if (migratedMigrations.length > 0) { + return response.status(201).json(migratedMigrations); + } - return response.status(200).json(migratedMigrations); + return response.status(200).json(migratedMigrations); + } finally { + await dbClient.end(); + } } From 4a4d1eb25e9f8c531e0c745558c83448c4a4b7c2 Mon Sep 17 00:00:00 2001 From: Anthony Cruz Date: Fri, 21 Feb 2025 17:57:50 -0300 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20padroniza=C3=A7=C3=A3o=20dos=20?= =?UTF-8?q?controllers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/controller.js | 21 ++++++++++ infra/database.js | 8 +++- infra/errors.js | 22 ++++++++++- pages/api/v1/migrations/index.js | 66 ++++++++++---------------------- pages/api/v1/status/index.js | 21 +--------- 5 files changed, 70 insertions(+), 68 deletions(-) create mode 100644 infra/controller.js diff --git a/infra/controller.js b/infra/controller.js new file mode 100644 index 0000000..94eb518 --- /dev/null +++ b/infra/controller.js @@ -0,0 +1,21 @@ +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 bd06fc2..ad2a294 100644 --- a/infra/database.js +++ b/infra/database.js @@ -1,4 +1,5 @@ import { Client } from "pg"; +import { ServiceError } from "./errors.js"; async function getNewClient() { const client = new Client({ @@ -21,8 +22,11 @@ async function query(queryObject) { const result = await client.query(queryObject); return result; } catch (error) { - 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 9ec53e0..7e7d80d 100644 --- a/infra/errors.js +++ b/infra/errors.js @@ -1,9 +1,27 @@ 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() { diff --git a/pages/api/v1/migrations/index.js b/pages/api/v1/migrations/index.js index 7ee9953..9be29bb 100644 --- a/pages/api/v1/migrations/index.js +++ b/pages/api/v1/migrations/index.js @@ -2,15 +2,12 @@ import { resolve } from "node:path"; import migrationRunner from "node-pg-migrate"; import database from "infra/database.js"; import { createRouter } from "next-connect"; -import { InternalServerError, MethodNotAllowedError } from "infra/errors"; +import controller from "infra/controller.js"; const router = createRouter(); router.get(getHandler); router.post(postHandler); -export default router.handler({ - onNoMatch: onNoMatchHandler, - onError: onErrorHandler, -}); +export default router.handler(controller.errorHandlers); const defaultMigrationsOptions = { dryRun: true, @@ -20,49 +17,28 @@ const defaultMigrationsOptions = { migrationsTable: "pgmigrations", }; -function onNoMatchHandler(request, response) { - const publicErrorObject = new MethodNotAllowedError(); - response.status(publicErrorObject.statusCode).json(publicErrorObject); -} - -function onErrorHandler(error, request, response) { - const publicErrorObject = new InternalServerError({ cause: error }); - - console.log("\n Erro dentro do catch do next-connect:"); - console.error(publicErrorObject); - - response.status(500).json(publicErrorObject); -} - async function getHandler(request, response) { - try { - const dbClient = await database.getNewClient(); - const pendingMigrations = await migrationRunner({ - ...defaultMigrationsOptions, - dbClient, - }); - await dbClient.end(); - return response.status(200).json(pendingMigrations); - } finally { - await dbClient.end(); - } + const dbClient = await database.getNewClient(); + const pendingMigrations = await migrationRunner({ + ...defaultMigrationsOptions, + dbClient, + }); + await dbClient.end(); + return response.status(200).json(pendingMigrations); } async function postHandler(request, response) { - try { - const dbClient = await database.getNewClient(); - const migratedMigrations = await migrationRunner({ - ...defaultMigrationsOptions, - dbClient, - dryRun: false, - }); - - if (migratedMigrations.length > 0) { - return response.status(201).json(migratedMigrations); - } - - return response.status(200).json(migratedMigrations); - } finally { - await dbClient.end(); + const dbClient = await database.getNewClient(); + const migratedMigrations = await migrationRunner({ + ...defaultMigrationsOptions, + dbClient, + dryRun: false, + }); + await dbClient.end(); + + if (migratedMigrations.length > 0) { + return response.status(201).json(migratedMigrations); } + + return response.status(200).json(migratedMigrations); } diff --git a/pages/api/v1/status/index.js b/pages/api/v1/status/index.js index d238f47..359bba6 100644 --- a/pages/api/v1/status/index.js +++ b/pages/api/v1/status/index.js @@ -1,27 +1,10 @@ import { createRouter } from "next-connect"; import database from "infra/database.js"; -import { InternalServerError, MethodNotAllowedError } from "infra/errors"; +import controller from "infra/controller"; const router = createRouter(); router.get(getHandler); -export default router.handler({ - onNoMatch: onNoMatchHandler, - onError: onErrorHandler, -}); - -function onNoMatchHandler(request, response) { - const publicErrorObject = new MethodNotAllowedError(); - response.status(publicErrorObject.statusCode).json(publicErrorObject); -} - -function onErrorHandler(error, request, response) { - const publicErrorObject = new InternalServerError({ cause: error }); - - console.log("\n Erro dentro do catch do next-connect:"); - console.error(publicErrorObject); - - response.status(500).json(publicErrorObject); -} +export default router.handler(controller.errorHandlers); async function getHandler(request, response) { const updatedAt = new Date().toISOString(); From e71caf92a31ce01be0641ba19a8b1f1e7e20f857 Mon Sep 17 00:00:00 2001 From: Anthony Cruz Date: Fri, 21 Feb 2025 18:03:14 -0300 Subject: [PATCH 6/6] =?UTF-8?q?style:=20formata=C3=A7=C3=A3o=20com=20`Pret?= =?UTF-8?q?tier`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/controller.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/infra/controller.js b/infra/controller.js index 94eb518..16be0d9 100644 --- a/infra/controller.js +++ b/infra/controller.js @@ -6,7 +6,10 @@ function onNoMatchHandler(request, response) { } function onErrorHandler(error, request, response) { - const publicErrorObject = new InternalServerError({ statusCode: error.statusCode, cause: error }); + const publicErrorObject = new InternalServerError({ + statusCode: error.statusCode, + cause: error, + }); console.error(publicErrorObject); response.status(publicErrorObject.statusCode).json(publicErrorObject); }