diff --git a/src/db/repositories/external-object-assignment-plan-project-command.repository.ts b/src/db/repositories/external-object-assignment-plan-project-command.repository.ts index d34c4c0..cce586b 100644 --- a/src/db/repositories/external-object-assignment-plan-project-command.repository.ts +++ b/src/db/repositories/external-object-assignment-plan-project-command.repository.ts @@ -164,6 +164,14 @@ export class ExternalObjectAssignmentPlanProjectCommandRepository command.payload.objects, "External object changed before plan assignment." ); + // A new plan may only link objects present in the latest import; the + // generic command route must not bypass the dialog route's rule. + if ( + source === "user" && + command.payload.objects.some(({ expected }) => expected.presenceStatus !== "present") + ) { + throw new Error("Missing external objects cannot be assigned."); + } for (const circuit of newCircuits) { assertEquipmentIdentifierAvailable( diff --git a/src/shared/validation/external-object-assignment-plan.schemas.ts b/src/shared/validation/external-object-assignment-plan.schemas.ts index 56cd31e..23aee62 100644 --- a/src/shared/validation/external-object-assignment-plan.schemas.ts +++ b/src/shared/validation/external-object-assignment-plan.schemas.ts @@ -1,8 +1,11 @@ import { z } from "zod"; import { expectedProjectRevisionSchema } from "./project-command.schemas.js"; +// Upper bound for all objects of one plan; the 5,000-object scale test stays +// well inside it. +export const maxAssignmentPlanObjects = 10000; const idSchema = z.string().min(1).max(200); -const objectIdsSchema = z.array(idSchema).min(1).max(10000); +const objectIdsSchema = z.array(idSchema).min(1).max(maxAssignmentPlanObjects); const newCircuitKeySchema = z.string().min(1).max(100); /** Request of the Revit assignment dialog (requirements §11.7). */ @@ -38,7 +41,13 @@ export const applyExternalObjectAssignmentPlanSchema = z confirmedConflictObjectIds: z.array(idSchema).max(10000), confirmedOverLimitCircuitKeys: z.array(z.string().min(1).max(200)).max(5000), }) - .strict(); + .strict() + .refine( + (plan) => + plan.rows.reduce((sum, row) => sum + row.objectIds.length, 0) <= + maxAssignmentPlanObjects, + { message: `Ein Plan darf höchstens ${maxAssignmentPlanObjects} Revit-Objekte enthalten.`, path: ["rows"] } + ); export type ApplyExternalObjectAssignmentPlanInput = z.infer< typeof applyExternalObjectAssignmentPlanSchema diff --git a/tests/external-object-assignment-plan-project-command.repository.test.ts b/tests/external-object-assignment-plan-project-command.repository.test.ts index 5a4a9c5..2053791 100644 --- a/tests/external-object-assignment-plan-project-command.repository.test.ts +++ b/tests/external-object-assignment-plan-project-command.repository.test.ts @@ -215,6 +215,35 @@ describe("external object assignment-plan project command", () => { } }); + it("rejects user plans for objects missing in the latest import", () => { + const context = createFixture(); + try { + context.db.update(externalModelObjects).set({ presenceStatus: "missing" }) + .where(eq(externalModelObjects.id, "object-b1")).run(); + const command = createExternalObjectAssignmentPlanProjectCommand( + externalObjectApplyAssignmentPlanCommandType, + { + ...planCommand(context.db).payload, + objects: planCommand(context.db).payload.objects.map(({ expected, target }) => + expected.id === "object-b1" + ? { expected: { ...expected, presenceStatus: "missing" as const }, target: { ...target, presenceStatus: "missing" as const } } + : { expected, target } + ), + } + ); + const before = readState(context.db); + assert.throws( + () => new ExternalObjectAssignmentPlanProjectCommandRepository(context.db).execute({ + projectId: "project-1", expectedRevision: 0, source: "user", command, + }), + /Missing external objects cannot be assigned/ + ); + assert.deepEqual(readState(context.db), before); + } finally { + context.close(); + } + }); + it("refuses undo after a created row was changed", () => { const context = createFixture(); try { diff --git a/tests/external-object-assignment-plan-route.test.ts b/tests/external-object-assignment-plan-route.test.ts index eecc1a7..b3916c7 100644 --- a/tests/external-object-assignment-plan-route.test.ts +++ b/tests/external-object-assignment-plan-route.test.ts @@ -184,6 +184,14 @@ describe("Revit assignment plan route", () => { 400 ); assert.equal((await call("POST", path, user, { ...plan(circuitListId), rows: [] })).status, 400); + const tooManyObjects = Array.from({ length: 10 }, (_, row) => ({ + kind: "new-row", + circuitId: "c", + objectIds: Array.from({ length: 1001 }, (_, index) => `o-${row}-${index}`), + })); + const oversized = await call("POST", path, user, { ...plan(circuitListId), rows: tooManyObjects }); + assert.equal(oversized.status, 400); + assert.match(await oversized.text(), /höchstens 10000 Revit-Objekte/); }); it("rejects a circuit list of another project and unknown objects", async () => {