Files
agentic-dev/packages/core-dsr/src/dsr.router.ts
Danijel Martinek 46e575a5a6 feat(core-dsr): handlers, dsrRouter, integration tests
Add four protocol-agnostic handlers (export, delete, rectify, restrict)
returning normalized { status, body, headers } responses, and a tRPC
dsrRouter via createDsrRouter(binding) following the factory pattern.

Auth checks: requireAuthenticated middleware gates all four procedures;
cascade-hard delete additionally requires admin role. Integration tests
assert happy-path response shapes, UNAUTHORIZED/FORBIDDEN error codes,
and error passthrough from the DSR service layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:34:51 +00:00

140 lines
4.1 KiB
TypeScript

import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { t } from "@repo/core-shared/trpc/init";
import { defineErrorMiddleware } from "@repo/core-shared/trpc/define-error-middleware";
import type { DsrBinding } from "./di/bind-production";
import { createExportHandler } from "./handlers/export-handler";
import { createDeleteHandler } from "./handlers/delete-handler";
import { createRectifyHandler } from "./handlers/rectify-handler";
import type { RectifyHandlerInput } from "./handlers/rectify-handler";
import { createRestrictHandler } from "./handlers/restrict-handler";
export type DsrTrpcUser = {
id?: string;
roles?: string[];
};
const requireAuthenticated = t.middleware(({ ctx, next }) => {
const user = (ctx as { user?: DsrTrpcUser }).user;
if (!user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
});
}
return next({ ctx: { ...ctx, user } });
});
const dsrProcedure = t.procedure
.use(requireAuthenticated)
.use(defineErrorMiddleware([]));
/**
* Creates the DSR tRPC router.
*
* Capture `binding` at router-creation time. Apps that mount this router
* must pass the `DsrBinding` returned by `bindProductionDsr` or `bindDevSeedDsr`.
*
* @example
* ```ts
* const binding = bindProductionDsr({ config, auditLog });
* const appRouter = t.router({ ..., dsr: createDsrRouter(binding) });
* ```
*/
export function createDsrRouter(binding: DsrBinding) {
// Handlers are created lazily (inside procedure closures) so that the
// dsrRouter singleton proxy doesn't trigger at module init time.
return t.router({
export: dsrProcedure
.input(
z
.object({
subjectId: z.string().min(1),
format: z.enum(["json", "json-ld"]),
})
.strict(),
)
.query(async ({ input }) => {
const res = await createExportHandler(binding.dataExport)(input);
return res.body;
}),
delete: dsrProcedure
.input(
z
.object({
subjectId: z.string().min(1),
mode: z.enum(["soft", "cascade-hard"]),
})
.strict(),
)
.mutation(async ({ ctx, input }) => {
if (input.mode === "cascade-hard") {
const user = (ctx as { user: DsrTrpcUser }).user;
if (!user.roles?.includes("admin")) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Admin role required for cascade-hard deletion",
});
}
}
const res = await createDeleteHandler(binding.dataDelete)(input);
return res.body;
}),
rectify: dsrProcedure
.input(
z
.object({
subjectId: z.string().min(1),
collection: z.string().min(1),
field: z.string().min(1),
value: z.unknown(),
})
.strict(),
)
.mutation(async ({ input }) => {
// tRPC infers z.unknown() as value?: unknown; cast to assert presence
const res = await createRectifyHandler(binding.dataRectify)(
input as RectifyHandlerInput,
);
return res.body;
}),
restrict: dsrProcedure
.input(
z
.object({
subjectId: z.string().min(1),
granted: z.boolean(),
})
.strict(),
)
.mutation(async ({ input }) => {
const res = await createRestrictHandler(binding.processingRestriction)(
input,
);
return res.body;
}),
});
}
/**
* Convenience singleton for projects with a single DSR binding instance.
* Most callers should use `createDsrRouter(binding)` and pass the binding
* explicitly. This export exists for type inference (`DsrRouter`) only.
*/
export const dsrRouter = createDsrRouter(
new Proxy({} as DsrBinding, {
get(_target, prop) {
if (prop === "then") return undefined; // not a Promise
throw new Error(
`dsrRouter singleton used without providing a DsrBinding. ` +
`Use createDsrRouter(binding) instead.`,
);
},
}),
);
export type DsrRouter = ReturnType<typeof createDsrRouter>;