Files
FrenoCorp/server/trpc/scripts-router.ts
Michael Freno 67c3881dcf Add waitlist schema for marketing (FRE-635)
- Created waitlist_signups and waitlist_events tables
- Supports email, name, source tracking, and status management
- Enables VIP supporter list for Product Hunt launch
- Migration 0002_chemical_shocker.sql generated
- Fixed brand color in product-hunt-assets-brief.md (#518ac8)
2026-04-26 06:21:20 -04:00

178 lines
5.4 KiB
TypeScript

import { protectedProcedure } 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,
} from '../../src/db/schema';
function slugify(title: string): string {
return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
}
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 Error(`Script ${scriptId} not found`);
}
const projectRows = await db.select({ ownerId: projects.ownerId })
.from(projects)
.where(eq(projects.id, script.projectId));
const project = projectRows[0];
if (!project || project.ownerId !== userId) {
throw new Error(`You do not have access to script ${scriptId}`);
}
return script;
}
export const scriptsRouter = {
listScripts: protectedProcedure
.input(z.object({ projectId: z.number().int().positive() }))
.query(async ({ input, ctx }) => {
await ctx.db!.select()
.from(projects)
.where(eq(projects.id, input.projectId));
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 }) => {
const rows = await ctx.db!.select()
.from(scripts)
.where(eq(scripts.id, input.id));
const script = rows[0];
if (!script) {
throw new Error(`Script ${input.id} not found`);
}
await verifyScriptOwnership(ctx.db!, input.id, ctx.userId!);
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 }) => {
const projectRows = await ctx.db!.select()
.from(projects)
.where(eq(projects.id, input.projectId));
if (!projectRows[0]) {
throw new Error(`Project ${input.projectId} not found`);
}
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<string, any> = { 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 ctx.db!.select()
.from(projects)
.where(eq(projects.id, input.projectId));
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);
}),
};