import { protectedProcedure, TRPCError } from './router'; import { z } from 'zod'; import { eq, and, like, sql, inArray } from 'drizzle-orm'; import type { DrizzleDB } from '../../src/db/config/migrations'; import { scripts, revisions, revisionChanges, projects, projectMembers, } from '../../src/db/schema'; function slugify(title: string): string { return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); } // H2 fix: verifies user has access to a project (owner or member) async function verifyProjectAccess( db: DrizzleDB, projectId: number, userId: number ) { const projectRows = await db .select({ id: projects.id, ownerId: projects.ownerId }) .from(projects) .where(eq(projects.id, projectId)); const project = projectRows[0]; if (!project) { throw new TRPCError({ code: 'NOT_FOUND', message: `Project ${projectId} not found` }); } if (project.ownerId === userId) return project; const memberRows = await db .select() .from(projectMembers) .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId))); if (memberRows.length === 0) { throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to project ${projectId}` }); } return project; } async function verifyScriptOwnership( db: DrizzleDB, scriptId: number, userId: number ) { const scriptRows = await db.select({ id: scripts.id, projectId: scripts.projectId }) .from(scripts) .where(eq(scripts.id, scriptId)); const script = scriptRows[0]; if (!script) { throw new TRPCError({ code: 'NOT_FOUND', message: `Script ${scriptId} not found` }); } await verifyProjectAccess(db, script.projectId, userId); return script; } export const scriptsRouter = { listScripts: protectedProcedure .input(z.object({ projectId: z.number().int().positive() })) .query(async ({ input, ctx }) => { await verifyProjectAccess(ctx.db!, input.projectId, ctx.userId!); return await ctx.db!.select() .from(scripts) .where(eq(scripts.projectId, input.projectId)) .orderBy(scripts.updatedAt); }), getScript: protectedProcedure .input(z.object({ id: z.number().int().positive() })) .query(async ({ input, ctx }) => { await verifyScriptOwnership(ctx.db!, input.id, ctx.userId!); const rows = await ctx.db!.select() .from(scripts) .where(eq(scripts.id, input.id)); const script = rows[0]; if (!script) { throw new TRPCError({ code: 'NOT_FOUND', message: `Script ${input.id} not found` }); } return script; }), createScript: protectedProcedure .input(z.object({ title: z.string().min(1).max(255), projectId: z.number().int().positive(), genre: z.string().optional(), logline: z.string().optional(), status: z.enum(['draft', 'revision', 'final', 'published']).optional(), })) .mutation(async ({ input, ctx }) => { await verifyProjectAccess(ctx.db!, input.projectId, ctx.userId!); const result = await ctx.db!.insert(scripts) .values({ title: input.title, slug: slugify(input.title), projectId: input.projectId, genre: input.genre ?? null, logline: input.logline ?? null, status: input.status ?? 'draft', }) .returning(); return result[0]; }), updateScript: protectedProcedure .input(z.object({ id: z.number().int().positive(), title: z.string().min(1).max(255).optional(), genre: z.string().optional(), logline: z.string().optional(), status: z.enum(['draft', 'revision', 'final', 'published']).optional(), })) .mutation(async ({ input, ctx }) => { await verifyScriptOwnership(ctx.db!, input.id, ctx.userId!); const updateData: Record = { updatedAt: new Date() }; if (input.title !== undefined) { updateData.title = input.title; updateData.slug = slugify(input.title); } if (input.genre !== undefined) updateData.genre = input.genre ?? null; if (input.logline !== undefined) updateData.logline = input.logline ?? null; if (input.status !== undefined) updateData.status = input.status; const result = await ctx.db!.update(scripts) .set(updateData) .where(eq(scripts.id, input.id)) .returning(); return result[0]; }), deleteScript: protectedProcedure .input(z.object({ id: z.number().int().positive() })) .mutation(async ({ input, ctx }) => { await verifyScriptOwnership(ctx.db!, input.id, ctx.userId!); // Get revision IDs for this script const scriptRevisions = await ctx.db!.select({ id: revisions.id }) .from(revisions) .where(eq(revisions.scriptId, input.id)); // Delete revision changes for each revision for (const rev of scriptRevisions) { await ctx.db!.delete(revisionChanges) .where(eq(revisionChanges.revisionId, rev.id)); } // Delete revisions await ctx.db!.delete(revisions) .where(eq(revisions.scriptId, input.id)); // Delete script await ctx.db!.delete(scripts) .where(eq(scripts.id, input.id)); return { success: true }; }), searchScripts: protectedProcedure .input(z.object({ projectId: z.number().int().positive(), query: z.string().optional(), })) .query(async ({ input, ctx }) => { await verifyProjectAccess(ctx.db!, input.projectId, ctx.userId!); const conditions: any[] = [eq(scripts.projectId, input.projectId)]; if (input.query) { const q = `%${input.query.toLowerCase()}%`; conditions.push( like(sql`LOWER(${scripts.title})`, q), ); } return await ctx.db!.select() .from(scripts) .where(and(...conditions)) .orderBy(scripts.title); }), };