Normalize external model state comparisons

This commit is contained in:
Julian Appel 2026-09-30 21:19:16 +02:00
parent 4ff01b2ccd
commit 431f9ff059
5 changed files with 34 additions and 3 deletions

View file

@ -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 { ExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration.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 { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { circuitLists } from "../schema/circuit-lists.js";
@ -30,7 +31,7 @@ export class ExternalFollowUpImportProjectCommandRepository implements ExternalF
return executeProjectCommandTransaction(this.database, input, (tx) => {
const currentState = new ExternalModelStateRepository(tx).getByProject(input.projectId);
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 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.");

View file

@ -8,6 +8,7 @@ import {
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 { 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 { distributionBoards } from "../schema/distribution-boards.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.");
const currentConfiguration = tx.select().from(externalModelConfigurations).where(eq(externalModelConfigurations.projectId, input.projectId)).get() ?? null;
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.");
}
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);
const persistedState = new ExternalModelStateRepository(tx).getByProject(input.projectId).state;
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);
});
}

View 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);
}