import type { ZodIssue } from "zod" import { CrudNotFoundError, CrudValidationError } from "./errors" import type { CrudAction, CrudAuditHook, CrudMutationContext, CrudRepository } from "./types" type SchemaSafeParseResult = | { success: true data: TInput } | { success: false error: { issues: ZodIssue[] } } type CrudSchema = { safeParse: (input: unknown) => SchemaSafeParseResult } type CrudSchemas = { create: CrudSchema update: CrudSchema } type CreateCrudServiceOptions = { resource: string repository: CrudRepository schemas: CrudSchemas auditHooks?: Array> } async function emitAuditHooks( hooks: Array>, event: { resource: string action: CrudAction actor: CrudMutationContext["actor"] metadata: CrudMutationContext["metadata"] before: TRecord | null after: TRecord | null }, ): Promise { if (hooks.length === 0) { return } const payload = { ...event, actor: event.actor ?? null, at: new Date(), } for (const hook of hooks) { await hook(payload) } } function parseOrThrow(params: { schema: CrudSchema input: unknown resource: string operation: "create" | "update" }): TInput { const parsed = params.schema.safeParse(params.input) if (parsed.success) { return parsed.data } throw new CrudValidationError({ resource: params.resource, operation: params.operation, issues: parsed.error.issues, }) } export function createCrudService( options: CreateCrudServiceOptions, ) { const auditHooks = options.auditHooks ?? [] return { list: () => options.repository.list(), getById: (id: TId) => options.repository.findById(id), create: async (input: unknown, context: CrudMutationContext = {}) => { const payload = parseOrThrow({ schema: options.schemas.create, input, resource: options.resource, operation: "create", }) const created = await options.repository.create(payload) await emitAuditHooks(auditHooks, { resource: options.resource, action: "create", actor: context.actor, metadata: context.metadata, before: null, after: created, }) return created }, update: async (id: TId, input: unknown, context: CrudMutationContext = {}) => { const payload = parseOrThrow({ schema: options.schemas.update, input, resource: options.resource, operation: "update", }) const existing = await options.repository.findById(id) if (!existing) { throw new CrudNotFoundError({ resource: options.resource, id, }) } const updated = await options.repository.update(id, payload) await emitAuditHooks(auditHooks, { resource: options.resource, action: "update", actor: context.actor, metadata: context.metadata, before: existing, after: updated, }) return updated }, delete: async (id: TId, context: CrudMutationContext = {}) => { const existing = await options.repository.findById(id) if (!existing) { throw new CrudNotFoundError({ resource: options.resource, id, }) } const deleted = await options.repository.delete(id) await emitAuditHooks(auditHooks, { resource: options.resource, action: "delete", actor: context.actor, metadata: context.metadata, before: existing, after: null, }) return deleted }, } }