Normalize external model state comparisons
This commit is contained in:
parent
4ff01b2ccd
commit
431f9ff059
5 changed files with 34 additions and 3 deletions
|
|
@ -6,6 +6,7 @@ import type { ExternalFollowUpImportProjectCommandStore } from "../../domain/por
|
||||||
import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js";
|
import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js";
|
||||||
import type { ExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration.js";
|
import type { ExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration.js";
|
||||||
import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js";
|
import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js";
|
||||||
|
import { externalModelStatesEqual } from "../../external-model/domain/external-model-state-comparison.js";
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuitLists } from "../schema/circuit-lists.js";
|
import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
|
|
@ -30,7 +31,7 @@ export class ExternalFollowUpImportProjectCommandRepository implements ExternalF
|
||||||
return executeProjectCommandTransaction(this.database, input, (tx) => {
|
return executeProjectCommandTransaction(this.database, input, (tx) => {
|
||||||
const currentState = new ExternalModelStateRepository(tx).getByProject(input.projectId);
|
const currentState = new ExternalModelStateRepository(tx).getByProject(input.projectId);
|
||||||
const currentConfiguration = tx.select().from(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, input.projectId)).get();
|
const currentConfiguration = tx.select().from(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, input.projectId)).get();
|
||||||
if (!currentState.projectExists || !currentConfiguration || !same(currentState.state, input.command.payload.expectedState) || !same(currentConfiguration, input.command.payload.expectedConfiguration)) throw new Error("Revit-Zustand oder Konfiguration wurde zwischenzeitlich geändert.");
|
if (!currentState.projectExists || !currentConfiguration || !externalModelStatesEqual(currentState.state, input.command.payload.expectedState) || !same(currentConfiguration, input.command.payload.expectedConfiguration)) throw new Error("Revit-Zustand oder Konfiguration wurde zwischenzeitlich geändert.");
|
||||||
const rowIds = input.command.payload.rows.map(({ expected }) => expected.id);
|
const rowIds = input.command.payload.rows.map(({ expected }) => expected.id);
|
||||||
const currentRows = rowIds.length ? tx.select().from(circuitDeviceRows).where(inArray(circuitDeviceRows.id, rowIds)).all() : [];
|
const currentRows = rowIds.length ? tx.select().from(circuitDeviceRows).where(inArray(circuitDeviceRows.id, rowIds)).all() : [];
|
||||||
if (currentRows.length !== rowIds.length) throw new Error("Eine Gerätezeile des Folgeimports wurde nicht gefunden.");
|
if (currentRows.length !== rowIds.length) throw new Error("Eine Gerätezeile des Folgeimports wurde nicht gefunden.");
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import type { ExternalModelConfigurationSnapshot } from "../../domain/models/external-model-configuration-project-command.model.js";
|
import type { ExternalModelConfigurationSnapshot } from "../../domain/models/external-model-configuration-project-command.model.js";
|
||||||
import type { ExternalInitialImportProjectCommandStore } from "../../domain/ports/external-initial-import-project-command.store.js";
|
import type { ExternalInitialImportProjectCommandStore } from "../../domain/ports/external-initial-import-project-command.store.js";
|
||||||
import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js";
|
import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js";
|
||||||
|
import { externalModelStatesEqual } from "../../external-model/domain/external-model-state-comparison.js";
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import { externalImportBatches } from "../schema/external-import-batches.js";
|
import { externalImportBatches } from "../schema/external-import-batches.js";
|
||||||
|
|
@ -32,7 +33,7 @@ export class ExternalInitialImportProjectCommandRepository implements ExternalIn
|
||||||
if (!currentStateResult.projectExists) throw new Error("Projekt nicht gefunden.");
|
if (!currentStateResult.projectExists) throw new Error("Projekt nicht gefunden.");
|
||||||
const currentConfiguration = tx.select().from(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, input.projectId)).get() ?? null;
|
const currentConfiguration = tx.select().from(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, input.projectId)).get() ?? null;
|
||||||
const payload = input.command.payload;
|
const payload = input.command.payload;
|
||||||
if (!same(payload.expectedState, currentStateResult.state) || !same(payload.expectedConfiguration, currentConfiguration)) {
|
if (!externalModelStatesEqual(payload.expectedState, currentStateResult.state) || !same(payload.expectedConfiguration, currentConfiguration)) {
|
||||||
throw new Error("Revit-Zustand oder Konfiguration wurde zwischenzeitlich geändert.");
|
throw new Error("Revit-Zustand oder Konfiguration wurde zwischenzeitlich geändert.");
|
||||||
}
|
}
|
||||||
if (!isEmptyExternalModelState(payload.targetState)) validateTarget(tx, input.projectId, input.expectedRevision, input.source, payload.targetConfiguration, payload.targetState);
|
if (!isEmptyExternalModelState(payload.targetState)) validateTarget(tx, input.projectId, input.expectedRevision, input.source, payload.targetConfiguration, payload.targetState);
|
||||||
|
|
@ -40,7 +41,7 @@ export class ExternalInitialImportProjectCommandRepository implements ExternalIn
|
||||||
replaceState(tx, input.projectId, payload.targetState);
|
replaceState(tx, input.projectId, payload.targetState);
|
||||||
const persistedState = new ExternalModelStateRepository(tx).getByProject(input.projectId).state;
|
const persistedState = new ExternalModelStateRepository(tx).getByProject(input.projectId).state;
|
||||||
const persistedConfiguration = tx.select().from(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, input.projectId)).get() ?? null;
|
const persistedConfiguration = tx.select().from(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, input.projectId)).get() ?? null;
|
||||||
if (!same(payload.targetState, persistedState) || !same(payload.targetConfiguration, persistedConfiguration)) throw new Error("Gespeicherter Revit-Erstimport weicht vom Zielzustand ab.");
|
if (!externalModelStatesEqual(payload.targetState, persistedState) || !same(payload.targetConfiguration, persistedConfiguration)) throw new Error("Gespeicherter Revit-Erstimport weicht vom Zielzustand ab.");
|
||||||
return createExternalInitialImportProjectCommand(payload.targetConfiguration, currentConfiguration, payload.targetState, currentStateResult.state);
|
return createExternalInitialImportProjectCommand(payload.targetConfiguration, currentConfiguration, payload.targetState, currentStateResult.state);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
src/external-model/domain/external-model-state-comparison.ts
Normal file
27
src/external-model/domain/external-model-state-comparison.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { ExternalModelStateSnapshot } from "./external-model-contracts.js";
|
||||||
|
|
||||||
|
export function externalModelStatesEqual(left: ExternalModelStateSnapshot, right: ExternalModelStateSnapshot) {
|
||||||
|
return canonical(normalize(left)) === canonical(normalize(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(state: ExternalModelStateSnapshot): ExternalModelStateSnapshot {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
importBatches: [...state.importBatches].sort((left, right) => compareNullableNumber(left.appliedProjectRevision, right.appliedProjectRevision) || compareText(left.id, right.id)),
|
||||||
|
roomMappings: [...state.roomMappings].sort((left, right) => compareText(left.normalizedSourceRoomKey, right.normalizedSourceRoomKey) || compareText(left.id, right.id)),
|
||||||
|
objects: [...state.objects].sort((left, right) => compareText(left.worksharingId, right.worksharingId) || compareText(left.id, right.id)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareNullableNumber(left: number | null, right: number | null) {
|
||||||
|
if (left === right) return 0;
|
||||||
|
if (left === null) return -1;
|
||||||
|
if (right === null) return 1;
|
||||||
|
return left - right;
|
||||||
|
}
|
||||||
|
function compareText(left: string, right: string) { return left < right ? -1 : left > right ? 1 : 0; }
|
||||||
|
function canonical(value: unknown): string {
|
||||||
|
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
||||||
|
if (value !== null && typeof value === "object") { const record = value as Record<string, unknown>; return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(",")}}`; }
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,7 @@ describe("external follow-up import project command", () => {
|
||||||
const followParsed = await createExternalXlsxFollowUpImportPlan({ fileName: "follow.xlsx", bytes: followBytes, configuration, catalogs, currentState: current, currentReturnValuesByObjectId: returnValues });
|
const followParsed = await createExternalXlsxFollowUpImportPlan({ fileName: "follow.xlsx", bytes: followBytes, configuration, catalogs, currentState: current, currentReturnValuesByObjectId: returnValues });
|
||||||
const expectedRow = toCircuitDeviceRowSnapshot(context.db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, "row-1")).get()!);
|
const expectedRow = toCircuitDeviceRowSnapshot(context.db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, "row-1")).get()!);
|
||||||
const built = buildExternalXlsxFollowUpTarget({ projectId: "project-1", expectedRevision: 1, importedAtIso: "2026-09-30T09:00:00.000Z", configuration, originalContentBase64: Buffer.from(followBytes).toString("base64"), document: followParsed.document, plan: followParsed.plan, currentState: current, sourceChangeDecisions: followParsed.plan.changes.map((change) => ({ objectId: change.objectId, accept: true })), roomDecisions: [], familyProjectDeviceDecisions: [], objectsWithoutRoomDefaultDistributionBoardId: null, rowsById: new Map([["row-1", expectedRow]]), createId: () => `40000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
const built = buildExternalXlsxFollowUpTarget({ projectId: "project-1", expectedRevision: 1, importedAtIso: "2026-09-30T09:00:00.000Z", configuration, originalContentBase64: Buffer.from(followBytes).toString("base64"), document: followParsed.document, plan: followParsed.plan, currentState: current, sourceChangeDecisions: followParsed.plan.changes.map((change) => ({ objectId: change.objectId, accept: true })), roomDecisions: [], familyProjectDeviceDecisions: [], objectsWithoutRoomDefaultDistributionBoardId: null, rowsById: new Map([["row-1", expectedRow]]), createId: () => `40000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
||||||
|
built.targetState.objects.reverse();
|
||||||
const repository = new ExternalFollowUpImportProjectCommandRepository(context.db);
|
const repository = new ExternalFollowUpImportProjectCommandRepository(context.db);
|
||||||
const commandWithoutRequiredRowTransition = createExternalFollowUpImportProjectCommand({ expectedConfiguration: configurationSnapshot, targetConfiguration: configurationSnapshot, expectedState: current, targetState: built.targetState, rows: [] });
|
const commandWithoutRequiredRowTransition = createExternalFollowUpImportProjectCommand({ expectedConfiguration: configurationSnapshot, targetConfiguration: configurationSnapshot, expectedState: current, targetState: built.targetState, rows: [] });
|
||||||
assert.throws(
|
assert.throws(
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ describe("Linear XLSX initial import", () => {
|
||||||
const { plan, document } = await createExternalXlsxInitialImportPlan({ fileName: "linear.xlsx", bytes, configuration, catalogs: { floors: [], rooms: [], distributionBoards: [], projectDevices: [] } });
|
const { plan, document } = await createExternalXlsxInitialImportPlan({ fileName: "linear.xlsx", bytes, configuration, catalogs: { floors: [], rooms: [], distributionBoards: [], projectDevices: [] } });
|
||||||
let sequence = 0;
|
let sequence = 0;
|
||||||
const target = buildExternalXlsxInitialImportTarget({ projectId: "project-1", expectedRevision: 0, sourceName: "Revit", importedAtIso: "2026-09-30T10:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document, plan, roomDecisions: plan.sourceRooms.map((room) => ({ sourceRoomKey: room.sourceRoomKey, roomId: null, defaultDistributionBoardId: null })), familyProjectDeviceDecisions: plan.familyGroups.map((family) => ({ familyAndType: family.familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: null, createId: () => `30000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
const target = buildExternalXlsxInitialImportTarget({ projectId: "project-1", expectedRevision: 0, sourceName: "Revit", importedAtIso: "2026-09-30T10:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document, plan, roomDecisions: plan.sourceRooms.map((room) => ({ sourceRoomKey: room.sourceRoomKey, roomId: null, defaultDistributionBoardId: null })), familyProjectDeviceDecisions: plan.familyGroups.map((family) => ({ familyAndType: family.familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: null, createId: () => `30000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
||||||
|
target.objects.reverse();
|
||||||
const repository = new ExternalInitialImportProjectCommandRepository(context.db);
|
const repository = new ExternalInitialImportProjectCommandRepository(context.db);
|
||||||
const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createExternalInitialImportProjectCommand(configurationSnapshot, configurationSnapshot, createEmptyExternalModelState(), target) });
|
const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createExternalInitialImportProjectCommand(configurationSnapshot, configurationSnapshot, createEmptyExternalModelState(), target) });
|
||||||
assert.equal(inserted.revision.revisionNumber, 1);
|
assert.equal(inserted.revision.revisionNumber, 1);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue