diff --git a/deployment/database/migrations/1.1.0_LogOperations.sql b/deployment/database/migrations/1.1.0_LogOperations.sql new file mode 100644 index 0000000..3312e8e --- /dev/null +++ b/deployment/database/migrations/1.1.0_LogOperations.sql @@ -0,0 +1,14 @@ +/** +* Loguear las operaciones de entrada-salida de la api para trazabilidad +* de errores cliente-servidor +*/ + +CREATE TABLE log_operations ( + id BIGINT GENERATED ALWAYS AS IDENTITY, + date TIMESTAMPTZ default now(), + origin TEXT NOT NULL, + endpoint TEXT, + data JSONB +) + + diff --git a/src/infrastructure/Log.repository.test.ts b/src/infrastructure/Log.repository.test.ts new file mode 100644 index 0000000..2917e1c --- /dev/null +++ b/src/infrastructure/Log.repository.test.ts @@ -0,0 +1,49 @@ +import { httpclient } from "config/httpclient.config"; +import { pgClient } from "config/pgclient.config"; +import type { ServerContext } from "domain/ServerContext"; +import { after, before, describe, test } from "node:test"; +import { LogRepository, type LogCreateDTO } from "./Log.repository"; +import assert from "node:assert"; + +describe("LogRepository Integration Tests", async () => { + const startDate = new Date(); + + const serverContext: ServerContext = { + PostgresClient: pgClient, + HttpClient: httpclient + }; + + const repo = new LogRepository(serverContext); + + // Clean up before and after tests to ensure isolation + const cleanup = async () => { + await pgClient.query("DELETE FROM log_operations WHERE date >= $1 AND date <= $2", + [startDate.toISOString(), new Date().toISOString()]); + }; + + before(async () => { + await cleanup(); + }); + + after(async () => { + await cleanup(); + }); + + test("Log must log", async () => { + const newLog: LogCreateDTO = { + data: { + card_id: "019cdd39-fc08-7417-b16d-a78794a24c01", + override: false + }, + endpoint: "/nfc/generate", + origin: "serverSide" + } + + const res = await repo.createLog(newLog) + + assert.ok(res) + + }) + + +}) diff --git a/src/infrastructure/Log.repository.ts b/src/infrastructure/Log.repository.ts new file mode 100644 index 0000000..4caa636 --- /dev/null +++ b/src/infrastructure/Log.repository.ts @@ -0,0 +1,95 @@ +import { tryCatch, type Result } from "domain/Result"; +import type { ServerContext } from "domain/ServerContext"; + +export type Log = { + id: string, + date: Date, + origin?: string, + endpoint: string, + data: Record, +} + +export type LogCreateDTO = Pick + +export class LogRepository { + private ctx: ServerContext; + constructor(serverContext: ServerContext) { + this.ctx = serverContext + } + + public async readBetween(start: Date, end: Date): Promise> { + const query = ` + SELECT id, date, origin, endpint, data + FROM log_operations + WHERE date >= $1 AND date <= $2 + ORDER BY date DESC + `; + + const values = [ + start.toISOString(), // ISOString asegura compatibilidad con TIMESTAMPTZ + end.toISOString(), + ]; + + const prom = this.ctx.PostgresClient.query(query, values) + const res = await tryCatch(prom) + + if (res.error != undefined) { + return { + error: res.error.msg.message + } + } + + return { + data: res.data.rows + } + } + + public async createLog(newLog: LogCreateDTO): Promise> { + const conn = await this.ctx.PostgresClient.connect() + await conn.query("BEGIN") + + const { origin, endpoint, data } = newLog + + const query = ` + INSERT INTO log_operations ( + origin, + endpoint, + data + ) VALUES ( + $1, + $2, + $3 + ) RETURNING + id, date, origin, endpoint, data + ` + const values = [origin, endpoint, JSON.stringify(data)] + const promiseRes = conn.query(query, values) + const res = await tryCatch(promiseRes) + + if (res.error != undefined) { + console.error("[x] Error logueando", res.error.msg) + await conn.query("ROLLBACK") + conn.release(); + return { + error: String(res.error.msg) + } + } + + const createdLogs = res.data.rows + + if (createdLogs.length == 0 || createdLogs[0] == undefined) { + await conn.query("ROLLBACK") + conn.release(); + return { + error: "Error creating the Log, insert response was empty" + } + } + + await conn.query("COMMIT") + conn.release() + return { + data: createdLogs[0] + } + } +} + diff --git a/src/infrastructure/Nfc.repository.test.ts b/src/infrastructure/Nfc.repository.test.ts index 4b6ed32..0a1a559 100644 --- a/src/infrastructure/Nfc.repository.test.ts +++ b/src/infrastructure/Nfc.repository.test.ts @@ -73,7 +73,6 @@ await describe("NfcRepository Integration Tests", async () => { assert.fail(`findActivationCodes failed: ${result.error}`); } - assert.ok(result.data, "Data should be returned"); assert.strictEqual(result.data.length, 0); }); @@ -114,7 +113,6 @@ await describe("NfcRepository Integration Tests", async () => { } assert.ok(result.data, "Data should be returned"); - assert.ok(Array.isArray(result.data)); const first = result.data[0]; diff --git a/src/infrastructure/Nfc.repository.ts b/src/infrastructure/Nfc.repository.ts index 6e6eae4..72a274f 100644 --- a/src/infrastructure/Nfc.repository.ts +++ b/src/infrastructure/Nfc.repository.ts @@ -1,7 +1,6 @@ import type { ActivationCodeDTO } from "domain/NfcDTOs.js"; import type { Result } from "domain/Result.js"; import type { ServerContext } from "domain/ServerContext.js"; -import { constrainedMemory } from "node:process"; // TODO: Pasar a Result diff --git a/src/main.ts b/src/main.ts index bf456d9..8d9d663 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,27 +11,13 @@ import { pgClient } from 'config/pgclient.config.js'; import { NfcController } from 'aplication/Nfc.controller.js'; import { NfcUsecases } from 'aplication/Nfc.usecases.js'; import { NfcRepository } from 'infrastructure/Nfc.repository.js'; +import { LogRepository, type LogCreateDTO } from 'infrastructure/Log.repository.js'; const PORT = env.PORT const HOSTNAME = env.HOST assert(HOSTNAME != undefined) -// test -const requestLogger = (req: Request, res: Response, next: NextFunction) => { - const timestamp = new Date().toISOString(); - const method = req.method; - const url = req.url; - const body = req.body; - - // 'origin' comes from the request headers - const origin = req.get('origin') || 'No Origin (likely direct request/Server-side)'; - - console.log(`[${timestamp}] ${method} ${url} - Origin: ${origin} - Body: ${JSON.stringify(body)}`); - - // Crucial: Call next() so the request doesn't hang! - next(); -}; // Instancias de las dependencias @@ -41,6 +27,7 @@ const serverContext: ServerContext = { } const nfcRepository = new NfcRepository(serverContext) +const logRepository = new LogRepository(serverContext) const nfcUsecases = new NfcUsecases({ serverContext, @@ -52,6 +39,56 @@ const ncfControllers = new NfcController({ nfcUsecases }) +// middleware + +const requestLogger = (req: Request, res: Response, next: NextFunction) => { + const timestamp = new Date().toISOString(); + const method = req.method; + const url = req.url; + const body = req.body; + + // 'origin' comes from the request headers + const origin = req.get('origin') || 'No Origin (likely direct request/Server-side)'; + const log: LogCreateDTO = { + data: req.body, + endpoint: method + " " + req.url, + origin: origin + } + logRepository.createLog(log).then().catch(e => console.error("Error logueando ", log, e)) + console.log(`[${timestamp}] ${method} ${url} - Origin: ${origin} - Body: ${JSON.stringify(body)}`); + + // Crucial: Call next() so the request doesn't hang! + next(); +}; + +const responseLogger = (req: Request, res: Response, next: NextFunction) => { + const timestamp = new Date().toISOString(); + const start = Date.now(); + const oldSend = res.send; + + + res.send = function (data: any): Response { + // @ts-ignore: Si, es poco ortodoxo + res.sendBody = data; // Store the result data + return oldSend.apply(res, [data]); + }; + + // Listen for the 'finish' event to log after the response is sent + res.on('finish', () => { + const duration = Date.now() - start; + const log: LogCreateDTO = { + // @ts-ignore: Por lo de antes + data: res.sendBody, + endpoint: req.method + " " + req.originalUrl, + origin: "SERVER" + } + logRepository.createLog(log) + console.log(`[${timestamp} ${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`); + }); + + next(); +} + // Rutas const router = Router(); @@ -76,6 +113,7 @@ app.use(cors({ })) app.use(requestLogger) +app.use(responseLogger) // Routes app.use('/', router);