92 lines
3.4 KiB
TypeScript
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);
|
|
}
|