Add Linear XLSX initial import

This commit is contained in:
Julian Appel 2026-09-30 09:50:58 +02:00
parent c633e78cd4
commit bfdd048269
20 changed files with 932 additions and 85 deletions

View file

@ -0,0 +1,105 @@
import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js";
import { assertExternalModelConfiguration } from "../../external-model/configuration/external-model-configuration.js";
import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js";
import type { ExternalModelConfigurationSnapshot } from "./external-model-configuration-project-command.model.js";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const externalInitialImportCommandType = "external-model.apply-initial-import" as const;
export const externalInitialImportCommandSchemaVersion = 1 as const;
export interface ExternalInitialImportPayload {
expectedConfiguration: ExternalModelConfigurationSnapshot | null;
targetConfiguration: ExternalModelConfigurationSnapshot | null;
expectedState: ExternalModelStateSnapshot;
targetState: ExternalModelStateSnapshot;
}
export interface ExternalInitialImportProjectCommand extends SerializedProjectCommand<ExternalInitialImportPayload> {
schemaVersion: typeof externalInitialImportCommandSchemaVersion;
type: typeof externalInitialImportCommandType;
}
export function createExternalInitialImportProjectCommand(
expectedConfiguration: ExternalModelConfigurationSnapshot | null,
targetConfiguration: ExternalModelConfigurationSnapshot | null,
expectedState: ExternalModelStateSnapshot,
targetState: ExternalModelStateSnapshot
): ExternalInitialImportProjectCommand {
const command: ExternalInitialImportProjectCommand = {
schemaVersion: externalInitialImportCommandSchemaVersion,
type: externalInitialImportCommandType,
payload: { expectedConfiguration, targetConfiguration, expectedState, targetState },
};
assertExternalInitialImportProjectCommand(command);
return command;
}
export function assertExternalInitialImportProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is ExternalInitialImportProjectCommand {
if (command.schemaVersion !== 1 || command.type !== externalInitialImportCommandType || !isRecord(command.payload)) {
throw new Error("Nicht unterstützter Revit-Erstimport-Command.");
}
const keys = Object.keys(command.payload).sort().join(",");
if (keys !== "expectedConfiguration,expectedState,targetConfiguration,targetState") {
throw new Error("Der Revit-Erstimport-Command ist unvollständig.");
}
assertConfigurationSnapshot(command.payload.expectedConfiguration);
assertConfigurationSnapshot(command.payload.targetConfiguration);
const expected = parseExternalModelStateSnapshot(command.payload.expectedState);
const target = parseExternalModelStateSnapshot(command.payload.targetState);
if (isEmptyExternalModelState(expected) === isEmptyExternalModelState(target)) {
throw new Error("Der Erstimport muss zwischen leerem und gefülltem Modellzustand wechseln.");
}
assertPopulatedInitialState(isEmptyExternalModelState(expected) ? target : expected);
}
export function createEmptyExternalModelState(): ExternalModelStateSnapshot {
return { source: null, importBatches: [], roomMappings: [], objects: [] };
}
export function isEmptyExternalModelState(state: ExternalModelStateSnapshot) {
return state.source === null && state.importBatches.length === 0 && state.roomMappings.length === 0 && state.objects.length === 0;
}
function assertConfigurationSnapshot(value: unknown): asserts value is ExternalModelConfigurationSnapshot | null {
if (value === null) return;
if (!isRecord(value) || typeof value.id !== "string" || typeof value.projectId !== "string") {
throw new Error("Ungültiger Konfigurationszustand im Revit-Erstimport.");
}
assertExternalModelConfiguration(value.configuration);
}
function assertPopulatedInitialState(state: ExternalModelStateSnapshot) {
if (!state.source || state.source.sourceType !== "revit_xlsx" || state.importBatches.length !== 1 || state.objects.length === 0) {
throw new Error("Der Erstimport benötigt eine Quelle, einen Batch und mindestens ein Objekt.");
}
const source = state.source;
const batch = state.importBatches[0];
if (batch.importKind !== "initial" || batch.projectId !== source.projectId || batch.sourceId !== source.id || batch.configurationSnapshot === null || batch.originalContentBase64 === null || batch.document === null || batch.objectCount !== state.objects.length) {
throw new Error("Der Erstimport-Batch ist unvollständig.");
}
const mappingIds = uniqueIds(state.roomMappings);
const roomKeys = new Set<string>();
for (const mapping of state.roomMappings) {
if (mapping.projectId !== source.projectId || mapping.sourceId !== source.id || roomKeys.has(mapping.normalizedSourceRoomKey)) throw new Error("Ungültige oder doppelte Quellraumzuordnung.");
roomKeys.add(mapping.normalizedSourceRoomKey);
}
uniqueIds(state.objects);
const worksharingIds = new Set<string>();
for (const object of state.objects) {
if (object.projectId !== source.projectId || object.sourceId !== source.id || object.lastSeenImportBatchId !== batch.id || object.lastAcceptedImportBatchId !== batch.id) throw new Error("Ungültige Objektidentität im Erstimport.");
if (worksharingIds.has(object.worksharingId)) throw new Error("Doppelte Worksharing-ID im Erstimport.");
worksharingIds.add(object.worksharingId);
if (object.externalRoomMappingId !== null && !mappingIds.has(object.externalRoomMappingId)) throw new Error("Ungültige Quellraumzuordnung am Revit-Objekt.");
if (object.circuitDeviceRowId !== null) throw new Error("Der Erstimport darf keine Gerätezeile zuordnen.");
if (object.presenceStatus !== "present") throw new Error("Erstimportobjekte müssen vorhanden sein.");
}
}
function uniqueIds(entries: ReadonlyArray<{ id: string }>) {
const ids = new Set<string>();
for (const entry of entries) { if (ids.has(entry.id)) throw new Error("Doppelte ID im Revit-Erstimport."); ids.add(entry.id); }
return ids;
}
function isRecord(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }

View file

@ -0,0 +1,15 @@
import type { ExternalInitialImportProjectCommand } from "../models/external-initial-import-project-command.model.js";
import type { AppendedProjectRevision, ProjectRevisionSource } from "./project-revision.store.js";
export interface ExecuteExternalInitialImportCommandInput {
projectId: string;
expectedRevision: number;
source: ProjectRevisionSource;
description?: string;
actorId?: string;
historyTargetChangeSetId?: string;
command: ExternalInitialImportProjectCommand;
}
export interface ExternalInitialImportProjectCommandStore {
execute(input: ExecuteExternalInitialImportCommandInput): { revision: AppendedProjectRevision; inverse: ExternalInitialImportProjectCommand };
}

View file

@ -177,6 +177,11 @@ import {
externalModelConfigurationUpdateCommandType,
} from "../models/external-model-configuration-project-command.model.js";
import type { ExternalModelConfigurationProjectCommandStore } from "../ports/external-model-configuration-project-command.store.js";
import {
assertExternalInitialImportProjectCommand,
externalInitialImportCommandType,
} from "../models/external-initial-import-project-command.model.js";
import type { ExternalInitialImportProjectCommandStore } from "../ports/external-initial-import-project-command.store.js";
import {
assertExternalObjectRowAssignmentProjectCommand,
externalObjectRowAssignmentCommandType,
@ -242,7 +247,8 @@ export class ProjectCommandService implements ProjectCommandExecutor {
private readonly externalObjectRowAssignmentStore?: ExternalObjectRowAssignmentProjectCommandStore,
private readonly externalObjectNewRowStore?: ExternalObjectNewRowProjectCommandStore,
private readonly externalObjectNewCircuitStore?: ExternalObjectNewCircuitProjectCommandStore,
private readonly estimateBalanceStore?: EstimateBalanceProjectCommandStore
private readonly estimateBalanceStore?: EstimateBalanceProjectCommandStore,
private readonly externalInitialImportStore?: ExternalInitialImportProjectCommandStore
) {}
executeUser(
@ -631,6 +637,16 @@ export class ProjectCommandService implements ProjectCommandExecutor {
command: input.command,
}).revision;
}
case externalInitialImportCommandType: {
assertExternalInitialImportProjectCommand(input.command);
if (!this.externalInitialImportStore) {
throw new Error("External initial import store is not available.");
}
return this.externalInitialImportStore.execute({
...input,
command: input.command,
}).revision;
}
case externalObjectRowAssignmentCommandType: {
assertExternalObjectRowAssignmentProjectCommand(input.command);
if (!this.externalObjectRowAssignmentStore) {