import { initTRPC, TRPCError } from "@trpc/server"; import { z } from "zod"; import superjson from "superjson"; import type { Session } from "next-auth"; import { db } from "@tasks/database"; import { auth } from "@/lib/auth"; import { resolveWorkspace, type WorkspaceContext } from "@/server/lib/resolve-workspace"; export type Context = { db: typeof db; session: Session | null; }; export async function createContext(): Promise { try { const session = await auth(); return { db, session }; } catch (error) { console.error("[trpc] createContext failed:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to create request context", cause: error, }); } } const t = initTRPC.context().create({ transformer: superjson, // Expose `error.cause` to the client. Procedures that need to surface // structured failure modes (e.g. `invites.accept` returning the invited // email so the explainer page can render) pass a `{ reason, ... }` object // as the cause and the client reads it from `error.shape.data.cause`. // Anything thrown should still be safe to serialize (no class instances, // no DB rows, no secrets) — keep cause payloads small and pure data. errorFormatter: ({ shape, error }) => ({ ...shape, data: { ...shape.data, cause: error.cause && typeof error.cause === "object" && !(error.cause instanceof Error) ? (error.cause as Record) : undefined, }, }), }); export const isAuthed = t.middleware(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx: { ...ctx, session: ctx.session, }, }); }); export const router = t.router; export const createCallerFactory = t.createCallerFactory; export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(isAuthed); /** * Procedure for any tenant-scoped operation. Caller must: * - Be authenticated. * - Pass `workspace` (UUID or slug) in the input. The middleware resolves it * to a full `WorkspaceContext` (id, slug, name, owner, role) and exposes it * on `ctx.workspace`. Procedures can then scope queries by `ctx.workspace.id`. * * Example: * workspaceProcedure * .input(z.object({ workspace: z.string(), title: z.string() })) * .mutation(({ ctx, input }) => { * return ctx.db.insert(objects).values({ * workspaceId: ctx.workspace.id, * title: input.title, * type: "task", * }); * }); */ export const workspaceProcedure = protectedProcedure .input(z.object({ workspace: z.string().min(1) })) .use(async ({ ctx, input, next }) => { const ws = await resolveWorkspace({ handle: input.workspace, userId: ctx.session.user.id, db: ctx.db, }); return next({ ctx: { ...ctx, workspace: ws, }, }); }); export type WorkspaceProcedureContext = Context & { session: Session; workspace: WorkspaceContext };