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 956989f..db4295d 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 { if (client) { await client?.end(); diff --git a/infra/errors.js b/infra/errors.js new file mode 100644 index 0000000..30a6387 --- /dev/null +++ b/infra/errors.js @@ -0,0 +1,58 @@ +export class InternalServerError extends Error { + constructor({ cause, statusCode }) { + super("Um error interno não esperando aconteceu.", { + cause, + }); + this.name = "InternalServerError"; + this.action = "Entre em contato com o suporte"; + this.statusCode = statusCode || 500; + } + + toJSON() { + return { + name: this.name, + message: this.message, + action: this.action, + statusCode: this.statusCode, + }; + } +} + +export class ServiceError extends Error { + constructor({ cause, message }) { + super(message || "Serviço indisponivel no momento.", { + cause, + }); + this.name = "InternalServerError"; + this.action = "Entre em contato com o suporte"; + this.statusCode = 503; + } + + toJSON() { + return { + name: this.name, + message: this.message, + action: this.action, + statusCode: this.statusCode, + }; + } +} + +export class MethodNotAllowedError extends Error { + constructor() { + super("Metodo 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, + statusCode: this.statusCode, + }; + } +} diff --git a/infra/erros.js b/infra/erros.js deleted file mode 100644 index 8e0bb79..0000000 --- a/infra/erros.js +++ /dev/null @@ -1,19 +0,0 @@ -export class InternalServerError extends Error { - constructor({ cause }) { - super("Um error interno não esperando aconteceu.", { - cause, - }); - this.name = "InternalServerError"; - this.action = "Entre em contato com o suporte"; - this.statusCode = 500; - } - - toJSON() { - return { - name: this.name, - message: this.message, - action: this.action, - statusCode: this.statusCode, - }; - } -} diff --git a/package-lock.json b/package-lock.json index 18be2eb..31e6879 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "dotenv": "17.4.2", "dotenv-expand": "13.0.0", "next": "16.2.6", + "next-connect": "1.0.0", "node-pg-migrate": "8.0.4", "pg": "8.20.0", "react": "19.2.6", @@ -2519,6 +2520,12 @@ "tslib": "^2.8.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/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -8545,6 +8552,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/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -9917,6 +9937,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 e889f8c..ac4dd81 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dotenv": "17.4.2", "dotenv-expand": "13.0.0", "next": "16.2.6", + "next-connect": "1.0.0", "node-pg-migrate": "8.0.4", "pg": "8.20.0", "react": "19.2.6", diff --git a/pages/api/v1/migrations/index.js b/pages/api/v1/migrations/index.js index 6e6d38b..97c0d94 100644 --- a/pages/api/v1/migrations/index.js +++ b/pages/api/v1/migrations/index.js @@ -1,54 +1,60 @@ +import { createRouter } from "next-connect"; import { runner as migrationRunner } from "node-pg-migrate"; import { resolve } from "node:path"; import database from "infra/database"; +import controller from "infra/controller"; -export default async function migrations(request, response) { - const allowedMethods = ["GET", "POST"]; +const router = createRouter(); - if (!allowedMethods.includes(request.method)) { - return response.status(405).json({ - error: `Method "${request.method}" not allowed`, - }); - } +router.get(getHandler); +router.post(postHandler); + +export default router.handler(controller.errorHandlers); +const defaultMigrationsOptions = { + 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 defaultMigrationsOptions = { + const pendingMigrations = await migrationRunner({ + ...defaultMigrationsOptions, 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); + return response.status(200).json(pendingMigrations); + } finally { + if (dbClient) { + await dbClient.end(); } + } +} - if (request.method === "POST") { - const migratedMigrations = await migrationRunner({ - ...defaultMigrationsOptions, - dryRun: false, - }); +async function postHandler(request, response) { + let dbClient; - if (migratedMigrations.length > 0) { - return response.status(201).json(migratedMigrations); - } + try { + dbClient = await database.getNewClient(); - return response.status(200).json(migratedMigrations); + const migratedMigrations = await migrationRunner({ + ...defaultMigrationsOptions, + dbClient, + dryRun: false, + }); + + if (migratedMigrations.length > 0) { + return response.status(201).json(migratedMigrations); } - } catch (error) { - console.error(error); - return response.status(500).json({ - error: "Internal Server Error", - }); + return response.status(200).json(migratedMigrations); } finally { if (dbClient) { await dbClient.end(); diff --git a/pages/api/v1/status/index.js b/pages/api/v1/status/index.js index bed99a5..bccf2a1 100644 --- a/pages/api/v1/status/index.js +++ b/pages/api/v1/status/index.js @@ -1,50 +1,43 @@ // status/api - +import { createRouter } from "next-connect"; import database from "infra/database.js"; -import { InternalServerError } from "infra/erros"; - -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 databaseOpenConnectionsResult = await database.query({ - text: "SELECT count(*)::int FROM pg_stat_activity WHERE datname = $1;", - values: [databaseName], - }); - const databaseOpenConnectionsValue = - databaseOpenConnectionsResult.rows[0].count; - - response.status(200).json({ - updated_at: updatedAt, - dependencies: { - database: { - version: databaseVersionValue, - max_connections: parseInt(databaseMaxConnectionsValue), - open_connections: databaseOpenConnectionsValue, - }, - }, - }); - } catch (error) { - const publicErrorObject = new InternalServerError({ - cause: error, - }); +import controller from "infra/controller"; - 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 databaseOpenConnectionsResult = await database.query({ + text: "SELECT count(*)::int FROM pg_stat_activity WHERE datname = $1;", + values: [databaseName], + }); + const databaseOpenConnectionsValue = + databaseOpenConnectionsResult.rows[0].count; + + response.status(200).json({ + updated_at: updatedAt, + dependencies: { + database: { + version: databaseVersionValue, + max_connections: parseInt(databaseMaxConnectionsValue), + open_connections: databaseOpenConnectionsValue, + }, + }, + }); +} diff --git a/teste/integration/api/v1/status/post.test.js b/teste/integration/api/v1/status/post.test.js new file mode 100644 index 0000000..1d6316a --- /dev/null +++ b/teste/integration/api/v1/status/post.test.js @@ -0,0 +1,26 @@ +import orchestrator from "../orchestrator.js"; + +beforeAll(async () => { + await orchestrator.waitForAllServices(); +}); + +describe("POST to /api/v1/status", () => { + describe("Anonymous user", () => { + test("Rertieving 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: "Metodo não permitido para este endpoint.", + action: + "Verifique se o método HTTP enviado é válido para este endpoint.", + statusCode: 405, + }); + }); + }); +});