Files
CapaKraken/packages/api/src/router/blueprint-procedure-support.ts
T
Hartmut e3551fb78f fix(api): validate rolePresets with RolePresetsSchema before DB cast
Replace z.array(z.unknown()) with RolePresetsSchema for blueprint
role presets mutation input, ensuring structural validation before
Prisma JSON cast. Also adds SECURITY.md for vulnerability disclosure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 08:35:02 +02:00

272 lines
7.5 KiB
TypeScript

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<TRPCContext, "db" | "dbUser">;
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<typeof blueprintIdInputSchema>;
type BlueprintIdentifierInput = z.infer<typeof blueprintIdentifierInputSchema>;
type BlueprintListInput = z.infer<typeof blueprintListInputSchema>;
type BlueprintCreateInput = z.infer<typeof CreateBlueprintSchema>;
type BlueprintUpdateInput = z.infer<typeof blueprintUpdateInputSchema>;
type BlueprintRolePresetsInput = z.infer<typeof blueprintRolePresetsInputSchema>;
type BlueprintBatchDeleteInput = z.infer<typeof blueprintBatchDeleteInputSchema>;
type BlueprintGlobalFieldDefsInput = z.infer<typeof blueprintGlobalFieldDefsInputSchema>;
type BlueprintSetGlobalInput = z.infer<typeof blueprintSetGlobalInputSchema>;
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<BlueprintIdentifierReadModel>(ctx.db, input.identifier, {
select: {
id: true,
name: true,
target: true,
isActive: true,
},
});
}
export async function getBlueprintDetailByIdentifier(
ctx: BlueprintProcedureContext,
input: BlueprintIdentifierInput,
) {
return findBlueprintByIdentifier<BlueprintDetailReadModel>(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<string, unknown>,
after: updated as unknown as Record<string, unknown>,
});
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;
}