/** * Page Router — CRUD, tree operations */ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import type { ProcedureBuilder } from '../trpc.js'; import { requirePageRepository } from './_helpers.js'; export function pageProcedures(publicProcedure: ProcedureBuilder) { return { getRootPage: publicProcedure .input(z.object({ initiativeId: z.string().min(1) })) .query(async ({ ctx, input }) => { const repo = requirePageRepository(ctx); return repo.getOrCreateRootPage(input.initiativeId); }), getPage: publicProcedure .input(z.object({ id: z.string().min(1) })) .query(async ({ ctx, input }) => { const repo = requirePageRepository(ctx); const page = await repo.findById(input.id); if (!page) { throw new TRPCError({ code: 'NOT_FOUND', message: `Page '${input.id}' not found`, }); } return page; }), getPageUpdatedAtMap: publicProcedure .input(z.object({ ids: z.array(z.string().min(1)) })) .query(async ({ ctx, input }) => { const repo = requirePageRepository(ctx); const foundPages = await repo.findByIds(input.ids); const map: Record = {}; for (const p of foundPages) { map[p.id] = p.updatedAt instanceof Date ? p.updatedAt.toISOString() : String(p.updatedAt); } return map; }), listPages: publicProcedure .input(z.object({ initiativeId: z.string().min(1) })) .query(async ({ ctx, input }) => { const repo = requirePageRepository(ctx); return repo.findByInitiativeId(input.initiativeId); }), listChildPages: publicProcedure .input(z.object({ parentPageId: z.string().min(1) })) .query(async ({ ctx, input }) => { const repo = requirePageRepository(ctx); return repo.findByParentPageId(input.parentPageId); }), createPage: publicProcedure .input(z.object({ initiativeId: z.string().min(1), parentPageId: z.string().min(1).nullable(), title: z.string().min(1), })) .mutation(async ({ ctx, input }) => { const repo = requirePageRepository(ctx); const page = await repo.create({ initiativeId: input.initiativeId, parentPageId: input.parentPageId, title: input.title, content: null, sortOrder: 0, }); ctx.eventBus.emit({ type: 'page:created', timestamp: new Date(), payload: { pageId: page.id, initiativeId: input.initiativeId, title: input.title }, }); return page; }), updatePage: publicProcedure .input(z.object({ id: z.string().min(1), title: z.string().min(1).optional(), content: z.string().nullable().optional(), sortOrder: z.number().int().optional(), })) .mutation(async ({ ctx, input }) => { const repo = requirePageRepository(ctx); const { id, ...data } = input; const page = await repo.update(id, data); ctx.eventBus.emit({ type: 'page:updated', timestamp: new Date(), payload: { pageId: id, initiativeId: page.initiativeId, title: input.title }, }); return page; }), deletePage: publicProcedure .input(z.object({ id: z.string().min(1) })) .mutation(async ({ ctx, input }) => { const repo = requirePageRepository(ctx); const page = await repo.findById(input.id); await repo.delete(input.id); if (page) { ctx.eventBus.emit({ type: 'page:deleted', timestamp: new Date(), payload: { pageId: input.id, initiativeId: page.initiativeId }, }); } return { success: true }; }), }; }