From 1794ab0416701d19ff3c1077c351eb87a1549bce Mon Sep 17 00:00:00 2001 From: Roberto Musso Date: Thu, 19 Feb 2026 16:56:14 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20US-005=20=E2=80=94=20Client=20tRPC=20pr?= =?UTF-8?q?ocedures=20(CRUD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented full CRUD for the clients router: - clients.list: returns all clients ordered by name - clients.create: inserts with UUID + createdAt timestamp - clients.update: partial update of name and/or industry - clients.delete: guard check — returns error payload if client has sub-clients or projects (does not delete) - clients.deleteWithCascade: BFS recursion to collect all descendant clients, nulls projectId on orphaned tasks, then deletes projects and all collected clients in order Imports added: eq, asc, inArray from drizzle-orm; getDb and schema tables (clients, projects, tasks) from db module. Co-Authored-By: Claude Sonnet 4.6 --- src/main/router/index.ts | 81 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/src/main/router/index.ts b/src/main/router/index.ts index 5ed411b..f1f8de6 100644 --- a/src/main/router/index.ts +++ b/src/main/router/index.ts @@ -1,5 +1,8 @@ import { initTRPC } from '@trpc/server'; import { z } from 'zod'; +import { eq, asc, inArray } from 'drizzle-orm'; +import { getDb } from '../db'; +import { clients, projects, tasks } from '../db/schema'; import { getStore } from '../store'; const t = initTRPC.create(); @@ -12,21 +15,87 @@ const healthRouter = router({ ping: publicProcedure.query(() => 'pong' as const), }); -// Stub routers — full implementations come in later user stories const clientsRouter = router({ - list: publicProcedure.query(() => []), + list: publicProcedure.query(() => { + return getDb().select().from(clients).orderBy(asc(clients.name)).all(); + }), + create: publicProcedure .input(z.object({ name: z.string(), parentId: z.string().optional(), industry: z.string().optional() })) - .mutation(() => null), + .mutation(({ input }) => { + const id = crypto.randomUUID(); + const now = Date.now(); + getDb().insert(clients).values({ + id, + name: input.name, + parentId: input.parentId ?? null, + industry: input.industry ?? null, + createdAt: now, + }).run(); + return { id }; + }), + update: publicProcedure .input(z.object({ id: z.string(), name: z.string().optional(), industry: z.string().optional() })) - .mutation(() => null), + .mutation(({ input }) => { + const set: Partial<{ name: string; industry: string | null }> = {}; + if (input.name !== undefined) set.name = input.name; + if (input.industry !== undefined) set.industry = input.industry; + if (Object.keys(set).length > 0) { + getDb().update(clients).set(set).where(eq(clients.id, input.id)).run(); + } + return null; + }), + delete: publicProcedure .input(z.object({ id: z.string() })) - .mutation(() => null), + .mutation(({ input }) => { + const db = getDb(); + const childClients = db.select({ id: clients.id }).from(clients).where(eq(clients.parentId, input.id)).all(); + if (childClients.length > 0) { + return { error: 'This client has sub-clients. Use cascade delete to remove all.' }; + } + const childProjects = db.select({ id: projects.id }).from(projects).where(eq(projects.clientId, input.id)).all(); + if (childProjects.length > 0) { + return { error: 'This client has projects. Use cascade delete to remove all.' }; + } + db.delete(clients).where(eq(clients.id, input.id)).run(); + return { success: true as const }; + }), + deleteWithCascade: publicProcedure .input(z.object({ id: z.string() })) - .mutation(() => null), + .mutation(({ input }) => { + const db = getDb(); + + // Recursively collect all descendant client IDs (BFS) + const allClientIds: string[] = [input.id]; + const queue: string[] = [input.id]; + while (queue.length > 0) { + const currentId = queue.shift()!; + const children = db.select({ id: clients.id }).from(clients).where(eq(clients.parentId, currentId)).all(); + for (const child of children) { + allClientIds.push(child.id); + queue.push(child.id); + } + } + + // Find all projects belonging to these clients + const clientProjects = db.select({ id: projects.id }).from(projects).where(inArray(projects.clientId, allClientIds)).all(); + const projectIds = clientProjects.map((p) => p.id); + + if (projectIds.length > 0) { + // Null out projectId on orphaned tasks + db.update(tasks).set({ projectId: null }).where(inArray(tasks.projectId, projectIds)).run(); + // Delete the projects + db.delete(projects).where(inArray(projects.id, projectIds)).run(); + } + + // Delete all collected clients + db.delete(clients).where(inArray(clients.id, allClientIds)).run(); + + return { success: true as const }; + }), }); const projectsRouter = router({