Add external object new row command

This commit is contained in:
2026-08-02 18:44:46 +02:00
parent 1a7dd7fe03
commit 347b3717c3
10 changed files with 649 additions and 9 deletions
@@ -5,6 +5,7 @@ import { eq } from "drizzle-orm";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { createDatabaseContext, type AppDatabase } from "../src/db/database-context.js";
import { ExternalObjectRowAssignmentProjectCommandRepository } from "../src/db/repositories/external-object-row-assignment-project-command.repository.js";
import { ExternalObjectNewRowProjectCommandRepository } from "../src/db/repositories/external-object-new-row-project-command.repository.js";
import { toCircuitDeviceRowSnapshot } from "../src/db/repositories/circuit-device-row-structure.persistence.js";
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
import { circuitLists } from "../src/db/schema/circuit-lists.js";
@@ -19,10 +20,14 @@ import { floors } from "../src/db/schema/floors.js";
import { projects } from "../src/db/schema/projects.js";
import { rooms } from "../src/db/schema/rooms.js";
import { createExternalObjectRowAssignmentProjectCommand } from "../src/domain/models/external-object-row-assignment-project-command.model.js";
import {
createExternalObjectNewRowProjectCommand,
externalObjectAssignToNewRowCommandType,
} from "../src/domain/models/external-object-new-row-project-command.model.js";
import type { ExternalModelObjectSnapshot } from "../src/external-model/domain/external-model-contracts.js";
import { externalCsvTestConfiguration } from "./fixtures/revit-csv-fixtures.js";
function createFixture() {
function createFixture(initialObjectRowId: string | null = "row-1") {
const context = createDatabaseContext(":memory:");
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
const database = context.db;
@@ -46,7 +51,7 @@ function createFixture() {
roomId: "room-1",
roomNumberSnapshot: "101",
roomNameSnapshot: "Büro",
quantity: index === 1 ? 3 : 0,
quantity: index === 1 ? (initialObjectRowId === "row-1" ? 3 : 1) : 0,
manualQuantity: index === 1 ? 1 : 0,
powerPerUnit: 0.12,
simultaneityFactor: 1,
@@ -78,7 +83,7 @@ function createFixture() {
roomId: "room-1",
defaultDistributionBoardId: "board-1",
}).run();
database.insert(externalModelObjects).values(objectSnapshot("row-1")).run();
database.insert(externalModelObjects).values(objectSnapshot(initialObjectRowId)).run();
return context;
}
@@ -225,3 +230,149 @@ describe("external object row assignment project command", () => {
}
});
});
describe("external object new-row project command", () => {
it("creates an external-only row and supports exact persisted undo and redo", () => {
const context = createFixture(null);
try {
const base = row(context.db, "row-2");
const newRow = {
...base,
id: "row-new",
quantity: 2,
manualQuantity: 0,
sortOrder: 20,
};
const object = objectSnapshot(null);
const command = createExternalObjectNewRowProjectCommand(
externalObjectAssignToNewRowCommandType,
{
row: newRow,
objects: [{
expected: object,
target: { ...object, circuitDeviceRowId: "row-new" },
}],
confirmedConflictObjectIds: [],
}
);
const repository = new ExternalObjectNewRowProjectCommandRepository(context.db);
const inserted = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
});
assert.equal(row(context.db, "row-new").manualQuantity, 0);
assert.equal(row(context.db, "row-new").quantity, 2);
assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, "row-new");
assert.throws(
() => repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command: inserted.inverse,
}),
/only be removed through project history/
);
const undone = repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: inserted.revision.changeSetId,
command: inserted.inverse,
});
assert.equal(
context.db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, "row-new")).get(),
undefined
);
assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, null);
repository.execute({
projectId: "project-1",
expectedRevision: 2,
source: "redo",
historyTargetChangeSetId: inserted.revision.changeSetId,
command: undone.inverse,
});
assert.equal(row(context.db, "row-new").quantity, 2);
} finally {
context.close();
}
});
it("rolls back the row and object link when history persistence fails", () => {
const context = createFixture(null);
try {
context.sqlite.exec(`
CREATE TRIGGER fail_new_row_history
BEFORE INSERT ON project_change_sets
BEGIN SELECT RAISE(ABORT, 'late history failure'); END;
`);
const base = row(context.db, "row-2");
const object = objectSnapshot(null);
const command = createExternalObjectNewRowProjectCommand(
externalObjectAssignToNewRowCommandType,
{
row: { ...base, id: "row-new", quantity: 2, manualQuantity: 0, sortOrder: 20 },
objects: [{ expected: object, target: { ...object, circuitDeviceRowId: "row-new" } }],
confirmedConflictObjectIds: [],
}
);
assert.throws(
() => new ExternalObjectNewRowProjectCommandRepository(context.db).execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
}),
/late history failure/
);
assert.equal(
context.db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, "row-new")).get(),
undefined
);
assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, null);
} finally {
context.close();
}
});
it("keeps the created row when it changed after assignment", () => {
const context = createFixture(null);
try {
const base = row(context.db, "row-2");
const object = objectSnapshot(null);
const repository = new ExternalObjectNewRowProjectCommandRepository(context.db);
const inserted = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createExternalObjectNewRowProjectCommand(
externalObjectAssignToNewRowCommandType,
{
row: { ...base, id: "row-new", quantity: 2, manualQuantity: 0, sortOrder: 20 },
objects: [{ expected: object, target: { ...object, circuitDeviceRowId: "row-new" } }],
confirmedConflictObjectIds: [],
}
),
});
context.db.update(circuitDeviceRows).set({ displayName: "Geändert" })
.where(eq(circuitDeviceRows.id, "row-new")).run();
assert.throws(
() => repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: inserted.revision.changeSetId,
command: inserted.inverse,
}),
/changed before history removal/
);
assert.equal(row(context.db, "row-new").displayName, "Geändert");
assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, "row-new");
} finally {
context.close();
}
});
});