metis-desk/apps/server/src/app.ts
David 37110d87ed feat: add optional registeredOn field to asset model and update related components
- Introduced a new optional field `registeredOn` to the asset input schema.
- Updated asset values and persistence layers to handle the new field.
- Enhanced tests to validate the behavior of the `registeredOn` field, including various date formats and null values.
- Modified UI components to display the registration date in asset details and forms.
- Added localization support for the new field in English and Spanish.
- Implemented helper functions for formatting the registration date.
- Created integration tests for reading and writing assets with the new field in the database.
2026-10-15 18:39:37 +02:00

104 lines
2.9 KiB
TypeScript

import { randomUUID } from "node:crypto";
import type { HttpErrorResponseDTO } from "@metis/core/common";
import { registerCoreRoutes } from "@metis/core/module";
import type { Application, ErrorRequestHandler, RequestHandler } from "express";
import express from "express";
import helmet from "helmet";
import { httpErrorDiagnostics } from "./infrastructure/http/http-error-diagnostic";
import type { HttpLogger } from "./infrastructure/http/http-logger";
import { logHttpRequest } from "./infrastructure/http/http-logger";
export function createApp(
logger: HttpLogger = logHttpRequest,
identity?: { middleware: RequestHandler; register: (app: Application) => void }
) {
const app = express();
app.disable("x-powered-by");
app.use(helmet());
app.use((request, response, next) => {
const requestId = randomUUID();
const startedAt = performance.now();
response.setHeader("x-request-id", requestId);
response.setHeader("cache-control", "no-store");
response.once("finish", () => {
logger({
event: "http_request",
requestId,
method: request.method,
statusCode: response.statusCode,
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
});
});
next();
});
app.use(express.json({ limit: "64kb" }));
if (identity) {
app.use(identity.middleware);
identity.register(app);
}
registerCoreRoutes(app);
app.use((_request, response) => {
const error: HttpErrorResponseDTO = {
code: "NOT_FOUND",
requestId: response.getHeader("x-request-id")?.toString() ?? "",
};
response.status(404).json(error);
});
const errorHandler: ErrorRequestHandler = (failure: unknown, request, response, next) => {
if (response.headersSent) {
next(failure);
return;
}
const status = bodyFailureStatus(failure);
if (status === 500) {
logger({
event: "http_error",
requestId: response.getHeader("x-request-id")?.toString() ?? "",
method: request.method,
statusCode: 500,
errors: httpErrorDiagnostics(failure),
});
}
const error: HttpErrorResponseDTO = {
code: status === 500 ? "INTERNAL_ERROR" : "INVALID_REQUEST",
requestId: response.getHeader("x-request-id")?.toString() ?? "",
};
response.status(status).json(error);
};
app.use(errorHandler);
return app;
}
function bodyFailureStatus(failure: unknown): number {
if (!(failure instanceof Error && "type" in failure)) {
return 500;
}
// Solo se reconocen errores públicos del parser, nunca mensajes o status arbitrarios.
switch (failure.type) {
case "entity.parse.failed":
case "request.aborted":
case "request.size.invalid":
return 400;
case "entity.too.large":
return 413;
case "charset.unsupported":
case "encoding.unsupported":
return 415;
default:
return 500;
}
}