Logs de la api como middleware
This commit is contained in:
14
deployment/database/migrations/1.1.0_LogOperations.sql
Normal file
14
deployment/database/migrations/1.1.0_LogOperations.sql
Normal file
@@ -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
|
||||
)
|
||||
|
||||
|
||||
49
src/infrastructure/Log.repository.test.ts
Normal file
49
src/infrastructure/Log.repository.test.ts
Normal file
@@ -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)
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
95
src/infrastructure/Log.repository.ts
Normal file
95
src/infrastructure/Log.repository.ts
Normal file
@@ -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<string, any>,
|
||||
}
|
||||
|
||||
export type LogCreateDTO = Pick<Log, "origin" | "endpoint" | "data">
|
||||
|
||||
export class LogRepository {
|
||||
private ctx: ServerContext;
|
||||
constructor(serverContext: ServerContext) {
|
||||
this.ctx = serverContext
|
||||
}
|
||||
|
||||
public async readBetween(start: Date, end: Date): Promise<Result<string, Log[]>> {
|
||||
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<Log>(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<Result<string, Log>> {
|
||||
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<Log>(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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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<E,T>
|
||||
|
||||
|
||||
68
src/main.ts
68
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);
|
||||
|
||||
Reference in New Issue
Block a user