import { BlueprintTarget, CreateBlueprintSchema, RolePresetsSchema, UpdateBlueprintSchema, } from "@capakraken/shared"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { makeAuditLogger } from "../lib/audit-helpers.js"; import type { TRPCContext } from "../trpc.js"; import { buildBlueprintCreateData, buildBlueprintRolePresetsUpdateData, buildBlueprintUpdateData, expandGlobalBlueprintFieldDefs, findBlueprintByIdentifier, } from "./blueprint-support.js"; type BlueprintProcedureContext = Pick; type BlueprintIdentifierReadModel = { id: string; name: string; target: BlueprintTarget; isActive: boolean; }; type BlueprintDetailReadModel = { id: string; name: string; target: BlueprintTarget; description: string | null; fieldDefs: unknown; defaults: unknown; validationRules: unknown; rolePresets: unknown; isActive: boolean; }; async function getBlueprintOrThrow(ctx: BlueprintProcedureContext, id: string) { return findUniqueOrThrow(ctx.db.blueprint.findUnique({ where: { id } }), "Blueprint"); } export const blueprintIdInputSchema = z.object({ id: z.string() }); export const blueprintIdentifierInputSchema = z.object({ identifier: z.string().trim().min(1), }); export const blueprintListInputSchema = z.object({ target: z.nativeEnum(BlueprintTarget).optional(), isActive: z.boolean().optional().default(true), }); export const blueprintUpdateInputSchema = z.object({ id: z.string(), data: UpdateBlueprintSchema, }); export const blueprintRolePresetsInputSchema = z.object({ id: z.string(), rolePresets: RolePresetsSchema.max(100), }); export const blueprintBatchDeleteInputSchema = z.object({ ids: z.array(z.string()).min(1).max(100), }); export const blueprintGlobalFieldDefsInputSchema = z.object({ target: z.nativeEnum(BlueprintTarget), }); export const blueprintSetGlobalInputSchema = z.object({ id: z.string(), isGlobal: z.boolean(), }); type BlueprintIdInput = z.infer; type BlueprintIdentifierInput = z.infer; type BlueprintListInput = z.infer; type BlueprintCreateInput = z.infer; type BlueprintUpdateInput = z.infer; type BlueprintRolePresetsInput = z.infer; type BlueprintBatchDeleteInput = z.infer; type BlueprintGlobalFieldDefsInput = z.infer; type BlueprintSetGlobalInput = z.infer; export async function listBlueprintSummaries(ctx: BlueprintProcedureContext) { return ctx.db.blueprint.findMany({ select: { id: true, name: true, _count: { select: { projects: true } }, }, orderBy: { name: "asc" }, }); } export async function listBlueprints(ctx: BlueprintProcedureContext, input: BlueprintListInput) { return ctx.db.blueprint.findMany({ where: { ...(input.target ? { target: input.target } : {}), isActive: input.isActive, }, orderBy: { name: "asc" }, }); } export async function getBlueprintById(ctx: BlueprintProcedureContext, input: BlueprintIdInput) { return getBlueprintOrThrow(ctx, input.id); } export async function resolveBlueprintByIdentifier( ctx: BlueprintProcedureContext, input: BlueprintIdentifierInput, ) { return findBlueprintByIdentifier(ctx.db, input.identifier, { select: { id: true, name: true, target: true, isActive: true, }, }); } export async function getBlueprintDetailByIdentifier( ctx: BlueprintProcedureContext, input: BlueprintIdentifierInput, ) { return findBlueprintByIdentifier(ctx.db, input.identifier, {}); } export async function createBlueprint(ctx: BlueprintProcedureContext, input: BlueprintCreateInput) { const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id); const blueprint = await ctx.db.blueprint.create({ data: buildBlueprintCreateData(input), }); audit({ entityType: "Blueprint", entityId: blueprint.id, entityName: blueprint.name, action: "CREATE", after: { name: input.name, target: input.target, description: input.description }, }); return blueprint; } export async function updateBlueprint(ctx: BlueprintProcedureContext, input: BlueprintUpdateInput) { const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id); const before = await getBlueprintOrThrow(ctx, input.id); const updated = await ctx.db.blueprint.update({ where: { id: input.id }, data: buildBlueprintUpdateData(input.data), }); audit({ entityType: "Blueprint", entityId: input.id, entityName: updated.name, action: "UPDATE", before: before as unknown as Record, after: updated as unknown as Record, }); return updated; } export async function updateBlueprintRolePresets( ctx: BlueprintProcedureContext, input: BlueprintRolePresetsInput, ) { const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id); const before = await getBlueprintOrThrow(ctx, input.id); const updated = await ctx.db.blueprint.update({ where: { id: input.id }, data: buildBlueprintRolePresetsUpdateData(input.rolePresets), }); audit({ entityType: "Blueprint", entityId: input.id, entityName: updated.name, action: "UPDATE", before: { rolePresets: before.rolePresets }, after: { rolePresets: input.rolePresets }, summary: "Updated role presets", }); return updated; } export async function deleteBlueprint(ctx: BlueprintProcedureContext, input: BlueprintIdInput) { const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id); const deleted = await ctx.db.blueprint.update({ where: { id: input.id }, data: { isActive: false, deletedAt: new Date() }, }); audit({ entityType: "Blueprint", entityId: input.id, entityName: deleted.name, action: "DELETE", }); return deleted; } export async function batchDeleteBlueprints( ctx: BlueprintProcedureContext, input: BlueprintBatchDeleteInput, ) { const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id); const updated = await ctx.db.$transaction( input.ids.map((id) => ctx.db.blueprint.update({ where: { id }, data: { isActive: false, deletedAt: new Date() }, }), ), ); for (const blueprint of updated) { audit({ entityType: "Blueprint", entityId: blueprint.id, entityName: blueprint.name, action: "DELETE", }); } return { count: updated.length }; } export async function getGlobalBlueprintFieldDefs( ctx: BlueprintProcedureContext, input: BlueprintGlobalFieldDefsInput, ) { const blueprints = await ctx.db.blueprint.findMany({ where: { target: input.target, isGlobal: true, isActive: true }, select: { id: true, name: true, fieldDefs: true }, }); return expandGlobalBlueprintFieldDefs(blueprints); } export async function setBlueprintGlobal( ctx: BlueprintProcedureContext, input: BlueprintSetGlobalInput, ) { const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id); const updated = await ctx.db.blueprint.update({ where: { id: input.id }, data: { isGlobal: input.isGlobal }, }); audit({ entityType: "Blueprint", entityId: input.id, entityName: updated.name, action: "UPDATE", after: { isGlobal: input.isGlobal }, summary: input.isGlobal ? "Set blueprint as global" : "Removed global flag from blueprint", }); return updated; }