import { validateCustomFields } from "@capakraken/engine"; import { BlueprintTarget, type BlueprintFieldDefinition } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { findUniqueOrThrow } from "../db/helpers.js"; interface BlueprintLookup { blueprint: { findUnique: (args: { where: { id: string }; select: { fieldDefs: true; target: true }; }) => Promise<{ fieldDefs: unknown; target: string } | null>; findMany: (args: { where: { target: BlueprintTarget; isGlobal: boolean; isActive: boolean }; select: { fieldDefs: true }; }) => Promise>; }; } interface AssertBlueprintDynamicFieldsInput { db: BlueprintLookup; blueprintId: string | undefined; dynamicFields: Record; target: BlueprintTarget; } export async function assertBlueprintDynamicFields({ db, blueprintId, dynamicFields, target, }: AssertBlueprintDynamicFieldsInput): Promise { // Collect field defs from the entity's specific blueprint (if any) let specificFieldDefs: BlueprintFieldDefinition[] = []; if (blueprintId) { const blueprint = await findUniqueOrThrow( db.blueprint.findUnique({ where: { id: blueprintId }, select: { fieldDefs: true, target: true }, }), "Blueprint", ); if (blueprint.target !== target) { throw new TRPCError({ code: "BAD_REQUEST", message: `${target} entities require a ${target.toLowerCase()} blueprint`, }); } specificFieldDefs = blueprint.fieldDefs as BlueprintFieldDefinition[]; } // Also collect field defs from all active global blueprints for this target const globalBlueprints = await db.blueprint.findMany({ where: { target, isGlobal: true, isActive: true }, select: { fieldDefs: true }, }); const globalFieldDefs = globalBlueprints.flatMap( (bp) => bp.fieldDefs as BlueprintFieldDefinition[], ); // Merge: specific blueprint fields + global fields (specific takes precedence for same key) const specificKeys = new Set(specificFieldDefs.map((f) => f.key)); const mergedFieldDefs = [ ...specificFieldDefs, ...globalFieldDefs.filter((f) => !specificKeys.has(f.key)), ]; if (mergedFieldDefs.length === 0) return; const errors = validateCustomFields(mergedFieldDefs, dynamicFields); if (errors.length > 0) { throw new TRPCError({ code: "UNPROCESSABLE_CONTENT", message: errors.map((error) => error.message).join("; "), }); } }