From 46e575a5a67113d19a4f6ac6fdb642093066585c Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Tue, 19 May 2026 20:34:51 +0000 Subject: [PATCH] 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 --- .../2026-05-14-@trpc/server.md | 2 +- docs/library-decisions/2026-05-14-zod.md | 2 +- packages/core-dsr/package.json | 7 +- .../core-dsr/src/__tests__/dsr.router.test.ts | 209 ++++++++++++++++++ .../core-dsr/src/__tests__/handlers.test.ts | 124 +++++++++++ packages/core-dsr/src/dsr.router.ts | 139 ++++++++++++ .../core-dsr/src/handlers/delete-handler.ts | 20 ++ .../core-dsr/src/handlers/export-handler.ts | 24 ++ .../core-dsr/src/handlers/handler-types.ts | 5 + .../core-dsr/src/handlers/rectify-handler.ts | 23 ++ .../core-dsr/src/handlers/restrict-handler.ts | 18 ++ packages/core-dsr/src/index.ts | 13 ++ pnpm-lock.yaml | 6 + 13 files changed, 588 insertions(+), 4 deletions(-) create mode 100644 packages/core-dsr/src/__tests__/dsr.router.test.ts create mode 100644 packages/core-dsr/src/__tests__/handlers.test.ts create mode 100644 packages/core-dsr/src/dsr.router.ts create mode 100644 packages/core-dsr/src/handlers/delete-handler.ts create mode 100644 packages/core-dsr/src/handlers/export-handler.ts create mode 100644 packages/core-dsr/src/handlers/handler-types.ts create mode 100644 packages/core-dsr/src/handlers/rectify-handler.ts create mode 100644 packages/core-dsr/src/handlers/restrict-handler.ts diff --git a/docs/library-decisions/2026-05-14-@trpc/server.md b/docs/library-decisions/2026-05-14-@trpc/server.md index fb6d200..d599e08 100644 --- a/docs/library-decisions/2026-05-14-@trpc/server.md +++ b/docs/library-decisions/2026-05-14-@trpc/server.md @@ -69,7 +69,7 @@ Actively maintained by the tRPC team. The 11.x line is the current major. Regula -All five feature packages export a tRPC router that uses `@trpc/server`. `@repo/core-api` aggregates these routers. `@repo/core-shared` provides the tRPC base instance and error middleware utilities. Named, non-hypothetical consumers exist today. +All five feature packages export a tRPC router that uses `@trpc/server`. `@repo/core-api` aggregates these routers. `@repo/core-shared` provides the tRPC base instance and error middleware utilities. `@repo/core-dsr` exposes the `dsrRouter` via `createDsrRouter`. Named, non-hypothetical consumers exist today. ## Prompt: replaces diff --git a/docs/library-decisions/2026-05-14-zod.md b/docs/library-decisions/2026-05-14-zod.md index e2bf665..6222d14 100644 --- a/docs/library-decisions/2026-05-14-zod.md +++ b/docs/library-decisions/2026-05-14-zod.md @@ -71,7 +71,7 @@ Zod is a pure runtime validation library with no network communication, telemetr -All five feature packages use Zod for use-case input/output schemas. `core-shared` uses Zod for tRPC input validation and error schemas. `core-audit` uses Zod for audit event schemas. Named, non-hypothetical consumers exist today. +All five feature packages use Zod for use-case input/output schemas. `core-shared` uses Zod for tRPC input validation and error schemas. `core-audit` uses Zod for audit event schemas. `core-dsr` uses Zod for `dsrRouter` procedure input schemas. Named, non-hypothetical consumers exist today. ## Prompt: replaces diff --git a/packages/core-dsr/package.json b/packages/core-dsr/package.json index 51eb7e2..a2f52ab 100644 --- a/packages/core-dsr/package.json +++ b/packages/core-dsr/package.json @@ -4,7 +4,8 @@ "private": true, "type": "module", "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./api": "./src/dsr.router.ts" }, "scripts": { "build": "tsc --noEmit", @@ -14,7 +15,9 @@ }, "dependencies": { "@repo/core-shared": "workspace:*", - "payload": "^3.0.0" + "@trpc/server": "^11.0.0", + "payload": "^3.0.0", + "zod": "^3.24.0" }, "devDependencies": { "@repo/core-eslint": "workspace:*", diff --git a/packages/core-dsr/src/__tests__/dsr.router.test.ts b/packages/core-dsr/src/__tests__/dsr.router.test.ts new file mode 100644 index 0000000..1c8ef40 --- /dev/null +++ b/packages/core-dsr/src/__tests__/dsr.router.test.ts @@ -0,0 +1,209 @@ +import { describe, it, expect, vi } from "vitest"; +import { createDsrRouter } from "@/dsr.router"; +import type { DsrTrpcUser } from "@/dsr.router"; +import type { DsrBinding } from "@/di/bind-production"; +import { + RecordingDataExport, + RecordingDataDelete, + RecordingDataRectify, + RecordingProcessingRestriction, +} from "@repo/core-testing/instrumentation"; + +// Recording doubles use local type aliases to avoid circular deps with core-dsr. +// The alias types are structurally compatible at runtime; cast via unknown to +// satisfy the DsrBinding constraint without modifying core-testing. +function makeBinding() { + return { + dataExport: new RecordingDataExport(), + dataDelete: new RecordingDataDelete(), + dataRectify: new RecordingDataRectify(), + processingRestriction: new RecordingProcessingRestriction(), + }; +} + +type TestBinding = ReturnType; + +function makeCaller(binding: TestBinding, user?: DsrTrpcUser) { + const router = createDsrRouter(binding as unknown as DsrBinding); + return router.createCaller({ user } as Record); +} + +const authenticatedUser: DsrTrpcUser = { id: "alice", roles: ["user"] }; +const adminUser: DsrTrpcUser = { id: "admin-user", roles: ["admin"] }; + +describe("dsrRouter.export", () => { + it("returns UserDataBundle body for authenticated user", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, authenticatedUser); + const result = await caller.export({ subjectId: "alice", format: "json" }); + + expect(result.subjectId).toBe("alice"); + expect(result.format).toBe("json"); + expect(binding.dataExport.calls).toHaveLength(1); + }); + + it("works with json-ld format", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, authenticatedUser); + const result = await caller.export({ + subjectId: "alice", + format: "json-ld", + }); + expect(result.format).toBe("json-ld"); + }); + + it("throws UNAUTHORIZED when ctx.user is absent", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, undefined); + await expect( + caller.export({ subjectId: "alice", format: "json" }), + ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + }); + + it("propagates errors from dataExport", async () => { + const binding = makeBinding(); + vi.spyOn(binding.dataExport, "exportSubjectData").mockRejectedValue( + new Error("export failed"), + ); + const caller = makeCaller(binding, authenticatedUser); + await expect( + caller.export({ subjectId: "alice", format: "json" }), + ).rejects.toThrow("export failed"); + }); +}); + +describe("dsrRouter.delete", () => { + it("returns DeletionCertificate for soft mode (any authenticated user)", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, authenticatedUser); + const result = await caller.delete({ subjectId: "alice", mode: "soft" }); + + expect(result.subjectId).toBe("alice"); + expect(result.mode).toBe("soft"); + expect(binding.dataDelete.calls).toHaveLength(1); + }); + + it("returns DeletionCertificate for cascade-hard mode with admin user", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, adminUser); + const result = await caller.delete({ + subjectId: "alice", + mode: "cascade-hard", + }); + + expect(result.mode).toBe("cascade-hard"); + expect(binding.dataDelete.calls[0]?.mode).toBe("cascade-hard"); + }); + + it("throws FORBIDDEN for cascade-hard mode with non-admin user", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, authenticatedUser); + await expect( + caller.delete({ subjectId: "alice", mode: "cascade-hard" }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }); + expect(binding.dataDelete.calls).toHaveLength(0); + }); + + it("throws FORBIDDEN for cascade-hard when user has no roles", async () => { + const binding = makeBinding(); + const noRolesUser: DsrTrpcUser = { id: "alice" }; + const caller = makeCaller(binding, noRolesUser); + await expect( + caller.delete({ subjectId: "alice", mode: "cascade-hard" }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }); + }); + + it("throws UNAUTHORIZED when ctx.user is absent", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, undefined); + await expect( + caller.delete({ subjectId: "alice", mode: "soft" }), + ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + }); +}); + +describe("dsrRouter.rectify", () => { + it("returns { ok: true } for authenticated user", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, authenticatedUser); + const result = await caller.rectify({ + subjectId: "alice", + collection: "users", + field: "name", + value: "Alice New", + }); + + expect(result).toEqual({ ok: true }); + expect(binding.dataRectify.calls[0]).toEqual({ + subjectId: "alice", + collection: "users", + field: "name", + value: "Alice New", + }); + }); + + it("throws UNAUTHORIZED when ctx.user is absent", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, undefined); + await expect( + caller.rectify({ + subjectId: "alice", + collection: "users", + field: "name", + value: "x", + }), + ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + }); + + it("propagates errors from dataRectify", async () => { + const binding = makeBinding(); + vi.spyOn(binding.dataRectify, "updateSubjectField").mockRejectedValue( + new Error('Field "secret" is not tagged as PII'), + ); + const caller = makeCaller(binding, authenticatedUser); + await expect( + caller.rectify({ + subjectId: "alice", + collection: "users", + field: "secret", + value: "x", + }), + ).rejects.toThrow(); + }); +}); + +describe("dsrRouter.restrict", () => { + it("returns { ok: true } when granting restriction", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, authenticatedUser); + const result = await caller.restrict({ + subjectId: "alice", + granted: true, + }); + + expect(result).toEqual({ ok: true }); + expect(binding.processingRestriction.sets[0]).toEqual({ + subjectId: "alice", + granted: true, + }); + }); + + it("returns { ok: true } when lifting restriction", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, authenticatedUser); + const result = await caller.restrict({ + subjectId: "alice", + granted: false, + }); + expect(result).toEqual({ ok: true }); + expect(binding.processingRestriction.sets[0]?.granted).toBe(false); + }); + + it("throws UNAUTHORIZED when ctx.user is absent", async () => { + const binding = makeBinding(); + const caller = makeCaller(binding, undefined); + await expect( + caller.restrict({ subjectId: "alice", granted: true }), + ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + }); +}); diff --git a/packages/core-dsr/src/__tests__/handlers.test.ts b/packages/core-dsr/src/__tests__/handlers.test.ts new file mode 100644 index 0000000..53ad316 --- /dev/null +++ b/packages/core-dsr/src/__tests__/handlers.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from "vitest"; +import { createExportHandler } from "@/handlers/export-handler"; +import { createDeleteHandler } from "@/handlers/delete-handler"; +import { createRectifyHandler } from "@/handlers/rectify-handler"; +import { createRestrictHandler } from "@/handlers/restrict-handler"; +import type { IDataExport } from "@/data-export.interface"; +import type { IDataDelete } from "@/data-delete.interface"; +import type { IDataRectify } from "@/data-rectify.interface"; +import type { IProcessingRestriction } from "@/processing-restriction.interface"; +import { + RecordingDataExport, + RecordingDataDelete, + RecordingDataRectify, + RecordingProcessingRestriction, +} from "@repo/core-testing/instrumentation"; + +// Recording doubles use local type aliases to avoid circular deps with core-dsr. +// Cast via unknown so they satisfy the interface at the TypeScript level while +// remaining structurally compatible at runtime. + +describe("createExportHandler", () => { + it("calls exportSubjectData and returns status 200 with bundle body", async () => { + const dataExport = new RecordingDataExport(); + const handler = createExportHandler(dataExport as unknown as IDataExport); + const res = await handler({ subjectId: "alice", format: "json" }); + + expect(res.status).toBe(200); + expect(res.body.subjectId).toBe("alice"); + expect(res.body.format).toBe("json"); + expect(dataExport.calls).toHaveLength(1); + expect(dataExport.calls[0]).toEqual({ subjectId: "alice", format: "json" }); + }); + + it("sets Content-Type: application/json for json format", async () => { + const dataExport = new RecordingDataExport(); + const handler = createExportHandler(dataExport as unknown as IDataExport); + const res = await handler({ subjectId: "alice", format: "json" }); + expect(res.headers?.["Content-Type"]).toBe("application/json"); + }); + + it("sets Content-Type: application/ld+json for json-ld format", async () => { + const dataExport = new RecordingDataExport(); + const handler = createExportHandler(dataExport as unknown as IDataExport); + const res = await handler({ subjectId: "alice", format: "json-ld" }); + expect(res.headers?.["Content-Type"]).toBe("application/ld+json"); + }); +}); + +describe("createDeleteHandler", () => { + it("calls deleteSubjectData and returns status 200 with certificate body", async () => { + const dataDelete = new RecordingDataDelete(); + const handler = createDeleteHandler(dataDelete as unknown as IDataDelete); + const res = await handler({ subjectId: "alice", mode: "soft" }); + + expect(res.status).toBe(200); + expect(res.body.subjectId).toBe("alice"); + expect(res.body.mode).toBe("soft"); + expect(dataDelete.calls).toHaveLength(1); + expect(dataDelete.calls[0]).toEqual({ subjectId: "alice", mode: "soft" }); + }); + + it("passes cascade-hard mode to deleteSubjectData", async () => { + const dataDelete = new RecordingDataDelete(); + const handler = createDeleteHandler(dataDelete as unknown as IDataDelete); + const res = await handler({ subjectId: "alice", mode: "cascade-hard" }); + expect(res.body.mode).toBe("cascade-hard"); + expect(dataDelete.calls[0]?.mode).toBe("cascade-hard"); + }); +}); + +describe("createRectifyHandler", () => { + it("calls updateSubjectField and returns status 200 with ok body", async () => { + const dataRectify = new RecordingDataRectify(); + const handler = createRectifyHandler( + dataRectify as unknown as IDataRectify, + ); + const res = await handler({ + subjectId: "alice", + collection: "users", + field: "name", + value: "Alice New", + }); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ ok: true }); + expect(dataRectify.calls).toHaveLength(1); + expect(dataRectify.calls[0]).toEqual({ + subjectId: "alice", + collection: "users", + field: "name", + value: "Alice New", + }); + }); +}); + +describe("createRestrictHandler", () => { + it("calls setRestriction with granted=true and returns status 200", async () => { + const processingRestriction = new RecordingProcessingRestriction(); + const handler = createRestrictHandler( + processingRestriction as unknown as IProcessingRestriction, + ); + const res = await handler({ subjectId: "alice", granted: true }); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ ok: true }); + expect(processingRestriction.sets).toHaveLength(1); + expect(processingRestriction.sets[0]).toEqual({ + subjectId: "alice", + granted: true, + }); + }); + + it("calls setRestriction with granted=false", async () => { + const processingRestriction = new RecordingProcessingRestriction(); + const handler = createRestrictHandler( + processingRestriction as unknown as IProcessingRestriction, + ); + await handler({ subjectId: "alice", granted: false }); + expect(processingRestriction.sets[0]).toEqual({ + subjectId: "alice", + granted: false, + }); + }); +}); diff --git a/packages/core-dsr/src/dsr.router.ts b/packages/core-dsr/src/dsr.router.ts new file mode 100644 index 0000000..2600baa --- /dev/null +++ b/packages/core-dsr/src/dsr.router.ts @@ -0,0 +1,139 @@ +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; diff --git a/packages/core-dsr/src/handlers/delete-handler.ts b/packages/core-dsr/src/handlers/delete-handler.ts new file mode 100644 index 0000000..03ea066 --- /dev/null +++ b/packages/core-dsr/src/handlers/delete-handler.ts @@ -0,0 +1,20 @@ +import type { IDataDelete } from "../data-delete.interface"; +import type { DeletionMode, DeletionCertificate } from "../dsr-types"; +import type { HandlerResponse } from "./handler-types"; + +export type DeleteHandlerInput = { + subjectId: string; + mode: DeletionMode; +}; + +export function createDeleteHandler(dataDelete: IDataDelete) { + return async ( + input: DeleteHandlerInput, + ): Promise> => { + const cert = await dataDelete.deleteSubjectData( + input.subjectId, + input.mode, + ); + return { status: 200, body: cert }; + }; +} diff --git a/packages/core-dsr/src/handlers/export-handler.ts b/packages/core-dsr/src/handlers/export-handler.ts new file mode 100644 index 0000000..1447dd9 --- /dev/null +++ b/packages/core-dsr/src/handlers/export-handler.ts @@ -0,0 +1,24 @@ +import type { IDataExport } from "../data-export.interface"; +import type { DsrFormat, UserDataBundle } from "../dsr-types"; +import type { HandlerResponse } from "./handler-types"; + +export type ExportHandlerInput = { + subjectId: string; + format: DsrFormat; +}; + +export function createExportHandler(dataExport: IDataExport) { + return async ( + input: ExportHandlerInput, + ): Promise> => { + const bundle = await dataExport.exportSubjectData( + input.subjectId, + input.format, + ); + const headers: Record = + input.format === "json-ld" + ? { "Content-Type": "application/ld+json" } + : { "Content-Type": "application/json" }; + return { status: 200, body: bundle, headers }; + }; +} diff --git a/packages/core-dsr/src/handlers/handler-types.ts b/packages/core-dsr/src/handlers/handler-types.ts new file mode 100644 index 0000000..5f45d1d --- /dev/null +++ b/packages/core-dsr/src/handlers/handler-types.ts @@ -0,0 +1,5 @@ +export type HandlerResponse = { + status: number; + body: T; + headers?: Record; +}; diff --git a/packages/core-dsr/src/handlers/rectify-handler.ts b/packages/core-dsr/src/handlers/rectify-handler.ts new file mode 100644 index 0000000..649861f --- /dev/null +++ b/packages/core-dsr/src/handlers/rectify-handler.ts @@ -0,0 +1,23 @@ +import type { IDataRectify } from "../data-rectify.interface"; +import type { HandlerResponse } from "./handler-types"; + +export type RectifyHandlerInput = { + subjectId: string; + collection: string; + field: string; + value: unknown; +}; + +export function createRectifyHandler(dataRectify: IDataRectify) { + return async ( + input: RectifyHandlerInput, + ): Promise> => { + await dataRectify.updateSubjectField( + input.subjectId, + input.collection, + input.field, + input.value, + ); + return { status: 200, body: { ok: true as const } }; + }; +} diff --git a/packages/core-dsr/src/handlers/restrict-handler.ts b/packages/core-dsr/src/handlers/restrict-handler.ts new file mode 100644 index 0000000..e6f980f --- /dev/null +++ b/packages/core-dsr/src/handlers/restrict-handler.ts @@ -0,0 +1,18 @@ +import type { IProcessingRestriction } from "../processing-restriction.interface"; +import type { HandlerResponse } from "./handler-types"; + +export type RestrictHandlerInput = { + subjectId: string; + granted: boolean; +}; + +export function createRestrictHandler( + processingRestriction: IProcessingRestriction, +) { + return async ( + input: RestrictHandlerInput, + ): Promise> => { + await processingRestriction.setRestriction(input.subjectId, input.granted); + return { status: 200, body: { ok: true as const } }; + }; +} diff --git a/packages/core-dsr/src/index.ts b/packages/core-dsr/src/index.ts index b671184..15c5aa0 100644 --- a/packages/core-dsr/src/index.ts +++ b/packages/core-dsr/src/index.ts @@ -35,3 +35,16 @@ export { InMemoryProcessingRestriction } from "./in-memory-processing-restrictio export { bindProductionDsr } from "./di/bind-production"; export type { DsrBinding, BindProductionDsrOpts } from "./di/bind-production"; export { bindDevSeedDsr } from "./di/bind-dev-seed"; + +export { createDsrRouter, dsrRouter } from "./dsr.router"; +export type { DsrRouter, DsrTrpcUser } from "./dsr.router"; + +export type { HandlerResponse } from "./handlers/handler-types"; +export { createExportHandler } from "./handlers/export-handler"; +export type { ExportHandlerInput } from "./handlers/export-handler"; +export { createDeleteHandler } from "./handlers/delete-handler"; +export type { DeleteHandlerInput } from "./handlers/delete-handler"; +export { createRectifyHandler } from "./handlers/rectify-handler"; +export type { RectifyHandlerInput } from "./handlers/rectify-handler"; +export { createRestrictHandler } from "./handlers/restrict-handler"; +export type { RestrictHandlerInput } from "./handlers/restrict-handler"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de16662..13aeaaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -638,9 +638,15 @@ importers: "@repo/core-shared": specifier: workspace:* version: link:../core-shared + "@trpc/server": + specifier: ^11.0.0 + version: 11.16.0(typescript@5.9.3) payload: specifier: ^3.0.0 version: 3.81.0(graphql@16.13.2)(typescript@5.9.3) + zod: + specifier: ^3.24.0 + version: 3.25.76 devDependencies: "@repo/core-eslint": specifier: workspace:*