diff --git a/infra/controller.js b/infra/controller.js new file mode 100644 index 0000000..16be0d9 --- /dev/null +++ b/infra/controller.js @@ -0,0 +1,24 @@ +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 cdfe39e..7e7d80d 100644 --- a/infra/errors.js +++ b/infra/errors.js @@ -1,9 +1,46 @@ 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/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", diff --git a/pages/api/v1/migrations/index.js b/pages/api/v1/migrations/index.js index f53b6dd..9be29bb 100644 --- a/pages/api/v1/migrations/index.js +++ b/pages/api/v1/migrations/index.js @@ -1,52 +1,44 @@ import { resolve } from "node:path"; import migrationRunner from "node-pg-migrate"; import database from "infra/database.js"; +import { createRouter } from "next-connect"; +import controller from "infra/controller.js"; + +const router = createRouter(); +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) { + const dbClient = await database.getNewClient(); + const pendingMigrations = await migrationRunner({ + ...defaultMigrationsOptions, + dbClient, + }); + await dbClient.end(); + return response.status(200).json(pendingMigrations); +} -export default async function migrations(request, response) { - let dbClient; - const allowedMethods = ["GET", "POST"]; - - if (!allowedMethods.includes(request.method)) { - return response.status(405).json({ - error: `Method "${request.method}" not allowed`, - }); +async function postHandler(request, response) { + 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); } - 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(); - } + return response.status(200).json(migratedMigrations); } diff --git a/pages/api/v1/status/index.js b/pages/api/v1/status/index.js index 597c6ae..359bba6 100644 --- a/pages/api/v1/status/index.js +++ b/pages/api/v1/status/index.js @@ -1,43 +1,35 @@ +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(); +import controller from "infra/controller"; - const postgresVersionResult = await database.query("SHOW server_version;"); - const postgresVersion = postgresVersionResult.rows[0].server_version; +const router = createRouter(); +router.get(getHandler); +export default router.handler(controller.errorHandlers); - const maxConnectionsResult = await database.query("SHOW max_connections;"); - const maxConnections = parseInt( - maxConnectionsResult.rows[0].max_connections, - ); +async function getHandler(request, response) { + const updatedAt = new Date().toISOString(); - 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; + const postgresVersionResult = await database.query("SHOW server_version;"); + const postgresVersion = postgresVersionResult.rows[0].server_version; - 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 }); + const maxConnectionsResult = await database.query("SHOW max_connections;"); + const maxConnections = parseInt(maxConnectionsResult.rows[0].max_connections); - console.log("\n Erro dentro do catch do controller:"); - console.error(publicErrorObject); + 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(500).json(publicErrorObject); - } + response.status(200).json({ + updated_at: updatedAt, + dependencies: { + database: { + version: postgresVersion, + max_connections: maxConnections, + opened_connections: openedConnections, + }, + }, + }); } - -export default status; 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, + }); + }); + }); +});