metis-desk/modules/companies/src/api/infrastructure/http/access.routes.ts

92 lines
3.4 KiB
TypeScript

import type { Result } from "@repo/rdx-utils";
import type { Application, Request, RequestHandler, Response } from "express";
import { Router } from "express";
import { z } from "zod/v4";
import {
AccessItemParamsSchema,
AccessSaveRequestSchema,
AccessScopeParamsSchema,
} from "../../../common/dto/request/access.request.dto";
import type { AccessSnapshot } from "../../application/access.repository";
import type { AccessUseCases } from "../../application/access.use-cases";
import { buildAccessResponse } from "../../application/access-response.builder";
import type { AccessError } from "../../domain/access/access-error";
function errorStatus(code: string) {
if (code === "INVALID_REQUEST") {
return 400;
}
return code.endsWith("NOT_FOUND") ? 404 : 409;
}
function sendError(response: Response, code: string, status: number) {
response.status(status).json({ code, requestId: response.getHeader("x-request-id") });
}
function visibleSnapshot(snapshot: AccessSnapshot, scope: string) {
// Las adiciones locales se cargan para detectar colisiones, pero nunca se exponen en el catálogo global.
return scope === "global"
? { ...snapshot, context: { ...snapshot.context, catalog: [] } }
: snapshot;
}
function sendResult(
response: Response,
result: Result<AccessSnapshot, AccessError>,
scope: string,
status: number
) {
if (!result.isSuccess) {
sendError(response, result.error.code, errorStatus(result.error.code));
return;
}
response.status(status).json(buildAccessResponse(visibleSnapshot(result.data, scope)));
}
function actorId(request: Request) {
if (!("session" in request)) {
return undefined;
}
const parsed = z.object({ userId: z.uuid() }).safeParse(request.session);
return parsed.success ? parsed.data.userId : undefined;
}
export function registerAccessRoutes(
app: Application,
useCases: AccessUseCases,
authenticate: RequestHandler,
legacyResourceWrites = false
) {
const router = Router();
router.get("/:scope", (request, response, next) => {
const scope = AccessScopeParamsSchema.safeParse(request.params);
if (!(scope.success && z.strictObject({}).safeParse(request.query).success)) {
sendError(response, "INVALID_REQUEST", 400);
return;
}
useCases
.get(scope.data.scope)
.then((result) => sendResult(response, result, scope.data.scope, 200))
.catch(next);
});
for (const path of ["/:scope", "/:scope/:id/update"]) {
const schema = path.endsWith("update") ? AccessItemParamsSchema : AccessScopeParamsSchema;
router.post(path, (request, response, next) => {
const params = schema.safeParse(request.params);
const body = AccessSaveRequestSchema.safeParse(request.body);
const actor = actorId(request);
if (!(params.success && body.success && actor)) {
sendError(response, "INVALID_REQUEST", 400);
return;
}
if (!legacyResourceWrites && body.data.values.kind === "resource") {
sendError(response, "RESOURCE_MOVED", 409);
return;
}
const id = z.object({ id: z.uuid() }).safeParse(params.data);
const identity = id.success ? id.data.id : undefined;
useCases
.save(params.data.scope, identity, body.data.values, actor)
.then((result) => sendResult(response, result, params.data.scope, identity ? 200 : 201))
.catch(next);
});
}
app.use("/api/access", authenticate, router);
}