forked from jappel/leistungsbilanz-ts
Harden the Revit assignment plan against missing objects and huge plans
- The command repository rejects user plans that link objects missing in the latest import, so the generic command route cannot bypass the rule. - The plan schema limits the total number of objects to 10,000. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
191455c745
commit
fd7dcbb100
4 changed files with 56 additions and 2 deletions
|
|
@ -164,6 +164,14 @@ export class ExternalObjectAssignmentPlanProjectCommandRepository
|
||||||
command.payload.objects,
|
command.payload.objects,
|
||||||
"External object changed before plan assignment."
|
"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) {
|
for (const circuit of newCircuits) {
|
||||||
assertEquipmentIdentifierAvailable(
|
assertEquipmentIdentifierAvailable(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { expectedProjectRevisionSchema } from "./project-command.schemas.js";
|
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 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);
|
const newCircuitKeySchema = z.string().min(1).max(100);
|
||||||
|
|
||||||
/** Request of the Revit assignment dialog (requirements §11.7). */
|
/** Request of the Revit assignment dialog (requirements §11.7). */
|
||||||
|
|
@ -38,7 +41,13 @@ export const applyExternalObjectAssignmentPlanSchema = z
|
||||||
confirmedConflictObjectIds: z.array(idSchema).max(10000),
|
confirmedConflictObjectIds: z.array(idSchema).max(10000),
|
||||||
confirmedOverLimitCircuitKeys: z.array(z.string().min(1).max(200)).max(5000),
|
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<
|
export type ApplyExternalObjectAssignmentPlanInput = z.infer<
|
||||||
typeof applyExternalObjectAssignmentPlanSchema
|
typeof applyExternalObjectAssignmentPlanSchema
|
||||||
|
|
|
||||||
|
|
@ -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", () => {
|
it("refuses undo after a created row was changed", () => {
|
||||||
const context = createFixture();
|
const context = createFixture();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,14 @@ describe("Revit assignment plan route", () => {
|
||||||
400
|
400
|
||||||
);
|
);
|
||||||
assert.equal((await call("POST", path, user, { ...plan(circuitListId), rows: [] })).status, 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 () => {
|
it("rejects a circuit list of another project and unknown objects", async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue