refactor(api): extract blueprint procedures

This commit is contained in:
2026-03-31 20:15:19 +02:00
parent 05c07c6b6a
commit e2ba131926
4 changed files with 798 additions and 221 deletions
@@ -0,0 +1,189 @@
import { BlueprintTarget, FieldType } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { createAuditEntry } = vi.hoisted(() => ({
createAuditEntry: vi.fn(),
}));
vi.mock("../lib/audit.js", () => ({
createAuditEntry,
}));
import {
batchDeleteBlueprints,
createBlueprint,
getGlobalBlueprintFieldDefs,
updateBlueprintRolePresets,
} from "../router/blueprint-procedure-support.js";
function createContext(db: Record<string, unknown>) {
return {
db: db as never,
dbUser: { id: "user_admin" } as never,
};
}
describe("blueprint procedure support", () => {
beforeEach(() => {
createAuditEntry.mockReset();
});
it("creates a blueprint and audits it", async () => {
const create = vi.fn().mockResolvedValue({
id: "bp_1",
name: "Consulting Blueprint",
target: BlueprintTarget.PROJECT,
});
const result = await createBlueprint(
createContext({
blueprint: { create },
}),
{
name: "Consulting Blueprint",
target: BlueprintTarget.PROJECT,
description: "Default setup",
fieldDefs: [],
defaults: { market: "EU" },
validationRules: [],
},
);
expect(create).toHaveBeenCalledWith({
data: {
name: "Consulting Blueprint",
target: BlueprintTarget.PROJECT,
description: "Default setup",
fieldDefs: [],
defaults: { market: "EU" },
validationRules: [],
},
});
expect(result).toEqual({
id: "bp_1",
name: "Consulting Blueprint",
target: BlueprintTarget.PROJECT,
});
expect(createAuditEntry).toHaveBeenCalledWith(
expect.objectContaining({
entityType: "Blueprint",
action: "CREATE",
entityId: "bp_1",
userId: "user_admin",
}),
);
});
it("updates only role presets with the dedicated audit summary", async () => {
const findUnique = vi.fn().mockResolvedValue({
id: "bp_1",
name: "Consulting Blueprint",
rolePresets: [],
});
const update = vi.fn().mockResolvedValue({
id: "bp_1",
name: "Consulting Blueprint",
rolePresets: [{ roleId: "role_1", allocation: 0.5 }],
});
const rolePresets = [{ roleId: "role_1", allocation: 0.5 }];
const result = await updateBlueprintRolePresets(
createContext({
blueprint: { findUnique, update },
}),
{
id: "bp_1",
rolePresets,
},
);
expect(findUnique).toHaveBeenCalledWith({
where: { id: "bp_1" },
});
expect(update).toHaveBeenCalledWith({
where: { id: "bp_1" },
data: { rolePresets },
});
expect(result.rolePresets).toEqual(rolePresets);
expect(createAuditEntry).toHaveBeenCalledWith(
expect.objectContaining({
entityType: "Blueprint",
summary: "Updated role presets",
before: { rolePresets: [] },
after: { rolePresets },
}),
);
});
it("soft deletes multiple blueprints inside one transaction and audits each record", async () => {
const update = vi
.fn()
.mockResolvedValueOnce({ id: "bp_1", name: "Consulting Blueprint", isActive: false })
.mockResolvedValueOnce({ id: "bp_2", name: "Animation Blueprint", isActive: false });
const $transaction = vi
.fn()
.mockImplementation(async (operations: Promise<unknown>[]) => Promise.all(operations));
const result = await batchDeleteBlueprints(
createContext({
$transaction,
blueprint: { update },
}),
{
ids: ["bp_1", "bp_2"],
},
);
expect($transaction).toHaveBeenCalledTimes(1);
expect(update).toHaveBeenNthCalledWith(1, {
where: { id: "bp_1" },
data: { isActive: false },
});
expect(update).toHaveBeenNthCalledWith(2, {
where: { id: "bp_2" },
data: { isActive: false },
});
expect(createAuditEntry).toHaveBeenCalledTimes(2);
expect(result).toEqual({ count: 2 });
});
it("expands global field definitions from active global blueprints", async () => {
const findMany = vi.fn().mockResolvedValue([
{
id: "bp_global",
name: "Global Project Blueprint",
fieldDefs: [
{
id: "field_market",
key: "market",
label: "Market",
order: 0,
type: FieldType.TEXT,
required: false,
},
],
},
]);
const result = await getGlobalBlueprintFieldDefs(
createContext({
blueprint: { findMany },
}),
{
target: BlueprintTarget.PROJECT,
},
);
expect(findMany).toHaveBeenCalledWith({
where: { target: BlueprintTarget.PROJECT, isGlobal: true, isActive: true },
select: { id: true, name: true, fieldDefs: true },
});
expect(result).toEqual([
expect.objectContaining({
blueprintId: "bp_global",
blueprintName: "Global Project Blueprint",
key: "market",
}),
]);
});
});
@@ -0,0 +1,277 @@
import { BlueprintTarget, FieldType, SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
import { blueprintRouter } from "../router/blueprint.js";
import { createCallerFactory } from "../trpc.js";
const createCaller = createCallerFactory(blueprintRouter);
function createPlanningCaller(db: Record<string, unknown>) {
return createCaller({
session: {
user: { email: "planning@example.com", name: "Planning", image: null },
expires: "2099-01-01T00:00:00.000Z",
},
db: db as never,
dbUser: {
id: "user_planning",
systemRole: SystemRole.MANAGER,
permissionOverrides: null,
},
permissions: new Set(["view:planning"]),
});
}
function createAdminCaller(db: Record<string, unknown>) {
return createCaller({
session: {
user: { email: "admin@example.com", name: "Admin", image: null },
expires: "2099-01-01T00:00:00.000Z",
},
db: db as never,
dbUser: {
id: "user_admin",
systemRole: SystemRole.ADMIN,
permissionOverrides: null,
},
});
}
function sampleBlueprint(overrides: Record<string, unknown> = {}) {
return {
id: "bp_1",
name: "Consulting Blueprint",
target: BlueprintTarget.PROJECT,
description: "Default setup",
fieldDefs: [],
defaults: {},
validationRules: [],
rolePresets: [],
isActive: true,
isGlobal: false,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
};
}
describe("blueprint router", () => {
it("lists active blueprints with the expected filters", async () => {
const findMany = vi.fn().mockResolvedValue([sampleBlueprint()]);
const caller = createPlanningCaller({
blueprint: { findMany },
});
const result = await caller.list({ target: BlueprintTarget.PROJECT });
expect(findMany).toHaveBeenCalledWith({
where: {
target: BlueprintTarget.PROJECT,
isActive: true,
},
orderBy: { name: "asc" },
});
expect(result).toHaveLength(1);
});
it("resolves a blueprint by identifier through the protected router query", async () => {
const findUnique = vi.fn().mockResolvedValue(null);
const findFirst = vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce({
id: "bp_1",
name: "Consulting Blueprint",
target: BlueprintTarget.PROJECT,
isActive: true,
});
const caller = createPlanningCaller({
blueprint: { findUnique, findFirst },
});
const result = await caller.resolveByIdentifier({ identifier: " consulting " });
expect(findUnique).toHaveBeenCalledWith({
where: { id: "consulting" },
select: {
id: true,
name: true,
target: true,
isActive: true,
},
});
expect(findFirst).toHaveBeenNthCalledWith(2, {
where: { name: { contains: "consulting", mode: "insensitive" } },
select: {
id: true,
name: true,
target: true,
isActive: true,
},
});
expect(result.name).toBe("Consulting Blueprint");
});
it("creates a blueprint and writes an audit entry", async () => {
const create = vi.fn().mockResolvedValue(sampleBlueprint());
const auditCreate = vi.fn().mockResolvedValue({});
const caller = createAdminCaller({
blueprint: { create },
auditLog: { create: auditCreate },
});
const result = await caller.create({
name: "Consulting Blueprint",
target: BlueprintTarget.PROJECT,
description: "Default setup",
fieldDefs: [],
defaults: { market: "EU" },
validationRules: [],
});
expect(create).toHaveBeenCalledWith({
data: {
name: "Consulting Blueprint",
target: BlueprintTarget.PROJECT,
description: "Default setup",
fieldDefs: [],
defaults: { market: "EU" },
validationRules: [],
},
});
expect(auditCreate).toHaveBeenCalledTimes(1);
expect(result.id).toBe("bp_1");
});
it("updates a blueprint through the router and preserves the before snapshot", async () => {
const update = vi.fn().mockResolvedValue(
sampleBlueprint({
name: "Updated Blueprint",
description: "Updated setup",
}),
);
const auditCreate = vi.fn().mockResolvedValue({});
const caller = createAdminCaller({
blueprint: {
findUnique: vi.fn().mockResolvedValue(sampleBlueprint()),
update,
},
auditLog: { create: auditCreate },
});
const result = await caller.update({
id: "bp_1",
data: {
name: "Updated Blueprint",
description: "Updated setup",
},
});
expect(update).toHaveBeenCalledWith({
where: { id: "bp_1" },
data: {
name: "Updated Blueprint",
description: "Updated setup",
},
});
expect(auditCreate).toHaveBeenCalledTimes(1);
expect(result.name).toBe("Updated Blueprint");
});
it("updates role presets with the dedicated mutation payload", async () => {
const update = vi.fn().mockResolvedValue(
sampleBlueprint({
rolePresets: [{ roleId: "role_1" }],
}),
);
const auditCreate = vi.fn().mockResolvedValue({});
const caller = createAdminCaller({
blueprint: {
findUnique: vi.fn().mockResolvedValue(sampleBlueprint({ rolePresets: [] })),
update,
},
auditLog: { create: auditCreate },
});
const rolePresets = [{ roleId: "role_1", allocation: 0.5 }];
const result = await caller.updateRolePresets({
id: "bp_1",
rolePresets,
});
expect(update).toHaveBeenCalledWith({
where: { id: "bp_1" },
data: { rolePresets },
});
expect(auditCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
summary: "Updated role presets",
}),
}),
);
expect(result.rolePresets).toEqual([{ roleId: "role_1" }]);
});
it("soft deletes blueprints in batch and audits each deleted blueprint", async () => {
const update = vi
.fn()
.mockResolvedValueOnce(sampleBlueprint({ id: "bp_1" }))
.mockResolvedValueOnce(sampleBlueprint({ id: "bp_2", name: "Animation Blueprint" }));
const auditCreate = vi.fn().mockResolvedValue({});
const transaction = vi
.fn()
.mockImplementation(async (operations: Promise<unknown>[]) => Promise.all(operations));
const caller = createAdminCaller({
blueprint: { update },
auditLog: { create: auditCreate },
$transaction: transaction,
});
const result = await caller.batchDelete({ ids: ["bp_1", "bp_2"] });
expect(transaction).toHaveBeenCalledTimes(1);
expect(update).toHaveBeenNthCalledWith(1, {
where: { id: "bp_1" },
data: { isActive: false },
});
expect(update).toHaveBeenNthCalledWith(2, {
where: { id: "bp_2" },
data: { isActive: false },
});
expect(auditCreate).toHaveBeenCalledTimes(2);
expect(result).toEqual({ count: 2 });
});
it("expands global field definitions with blueprint metadata", async () => {
const findMany = vi.fn().mockResolvedValue([
{
id: "bp_global",
name: "Global Project Blueprint",
fieldDefs: [
{
id: "field_market",
key: "market",
label: "Market",
order: 0,
type: FieldType.TEXT,
required: false,
},
],
},
]);
const caller = createPlanningCaller({
blueprint: { findMany },
});
const result = await caller.getGlobalFieldDefs({ target: BlueprintTarget.PROJECT });
expect(findMany).toHaveBeenCalledWith({
where: { target: BlueprintTarget.PROJECT, isGlobal: true, isActive: true },
select: { id: true, name: true, fieldDefs: true },
});
expect(result).toEqual([
expect.objectContaining({
blueprintId: "bp_global",
blueprintName: "Global Project Blueprint",
key: "market",
}),
]);
});
});