Persist Revit CSV configuration
This commit is contained in:
parent
da689af518
commit
219da5ec3b
22 changed files with 2508 additions and 27 deletions
9
src/db/migrations/0002_awesome_madripoor.sql
Normal file
9
src/db/migrations/0002_awesome_madripoor.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
CREATE TABLE `external_csv_configurations` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`project_id` text NOT NULL,
|
||||
`configuration_version` integer NOT NULL,
|
||||
`configuration` text NOT NULL,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `external_csv_configurations_project_id_unique` ON `external_csv_configurations` (`project_id`);
|
||||
1808
src/db/migrations/meta/0002_snapshot.json
Normal file
1808
src/db/migrations/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -15,6 +15,13 @@
|
|||
"when": 1785511894897,
|
||||
"tag": "0001_add_public_building_setting",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1785680771603,
|
||||
"tag": "0002_awesome_madripoor",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
assertExternalCsvConfigurationUpdateProjectCommand,
|
||||
createExternalCsvConfigurationUpdateProjectCommand,
|
||||
type ExternalCsvConfigurationSnapshot,
|
||||
} from "../../domain/models/external-csv-configuration-project-command.model.js";
|
||||
import type {
|
||||
ExecuteExternalCsvConfigurationCommandInput,
|
||||
ExternalCsvConfigurationProjectCommandStore,
|
||||
} from "../../domain/ports/external-csv-configuration-project-command.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { externalCsvConfigurations } from "../schema/external-csv-configurations.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
||||
export class ExternalCsvConfigurationProjectCommandRepository
|
||||
implements ExternalCsvConfigurationProjectCommandStore
|
||||
{
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
execute(input: ExecuteExternalCsvConfigurationCommandInput) {
|
||||
assertExternalCsvConfigurationUpdateProjectCommand(input.command);
|
||||
return executeProjectCommandTransaction(this.database, input, (tx) => {
|
||||
const project = tx
|
||||
.select({ id: projects.id })
|
||||
.from(projects)
|
||||
.where(eq(projects.id, input.projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
const current = tx
|
||||
.select()
|
||||
.from(externalCsvConfigurations)
|
||||
.where(eq(externalCsvConfigurations.projectId, input.projectId))
|
||||
.get() ?? null;
|
||||
assertSameSnapshot(current, input.command.payload.expected);
|
||||
const target = input.command.payload.target;
|
||||
if (target !== null && target.projectId !== input.projectId) {
|
||||
throw new Error("External CSV configuration belongs to a different project.");
|
||||
}
|
||||
if (sameSnapshot(current, target)) {
|
||||
throw new Error("External CSV configuration did not change.");
|
||||
}
|
||||
if (
|
||||
input.source === "user" &&
|
||||
target !== null &&
|
||||
target.configurationVersion !== (current?.configurationVersion ?? 0) + 1
|
||||
) {
|
||||
throw new Error("External CSV configuration version must advance by one.");
|
||||
}
|
||||
|
||||
if (target === null) {
|
||||
tx.delete(externalCsvConfigurations)
|
||||
.where(eq(externalCsvConfigurations.projectId, input.projectId))
|
||||
.run();
|
||||
} else if (current === null) {
|
||||
tx.insert(externalCsvConfigurations).values(target).run();
|
||||
} else {
|
||||
const updated = tx
|
||||
.update(externalCsvConfigurations)
|
||||
.set({
|
||||
configurationVersion: target.configurationVersion,
|
||||
configuration: target.configuration,
|
||||
})
|
||||
.where(eq(externalCsvConfigurations.id, current.id))
|
||||
.run();
|
||||
if (updated.changes !== 1) {
|
||||
throw new Error("External CSV configuration changed before update.");
|
||||
}
|
||||
}
|
||||
return createExternalCsvConfigurationUpdateProjectCommand(target, current);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function assertSameSnapshot(
|
||||
current: ExternalCsvConfigurationSnapshot | null,
|
||||
expected: ExternalCsvConfigurationSnapshot | null
|
||||
) {
|
||||
if (!sameSnapshot(current, expected)) {
|
||||
throw new Error("External CSV configuration changed before update.");
|
||||
}
|
||||
}
|
||||
|
||||
function sameSnapshot(
|
||||
left: ExternalCsvConfigurationSnapshot | null,
|
||||
right: ExternalCsvConfigurationSnapshot | null
|
||||
) {
|
||||
return canonicalJson(left) === canonicalJson(right);
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(canonicalJson).join(",")}]`;
|
||||
}
|
||||
if (value !== null && typeof value === "object") {
|
||||
const record = value as Record<string, unknown>;
|
||||
return `{${Object.keys(record)
|
||||
.sort()
|
||||
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import { projectDevices } from "../schema/project-devices.js";
|
|||
import { projectChangeSets } from "../schema/project-change-sets.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import { externalCsvConfigurations } from "../schema/external-csv-configurations.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
import {
|
||||
readProjectStateSnapshot,
|
||||
|
|
@ -161,6 +162,7 @@ function canonicalProjectState(state: ProjectStateSnapshot) {
|
|||
projectDevices: byId(state.projectDevices),
|
||||
floors: byId(state.floors),
|
||||
rooms: byId(state.rooms),
|
||||
externalCsvConfiguration: state.externalCsvConfiguration,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +185,10 @@ function replaceProjectState(
|
|||
database: AppDatabase,
|
||||
state: ProjectStateSnapshot
|
||||
) {
|
||||
database
|
||||
.delete(externalCsvConfigurations)
|
||||
.where(eq(externalCsvConfigurations.projectId, state.project.id))
|
||||
.run();
|
||||
database
|
||||
.delete(distributionBoards)
|
||||
.where(eq(distributionBoards.projectId, state.project.id))
|
||||
|
|
@ -218,6 +224,12 @@ function replaceProjectState(
|
|||
.run();
|
||||
|
||||
insertMany(database, floors, state.floors);
|
||||
if (state.externalCsvConfiguration !== null) {
|
||||
database
|
||||
.insert(externalCsvConfigurations)
|
||||
.values(state.externalCsvConfiguration)
|
||||
.run();
|
||||
}
|
||||
insertMany(database, rooms, state.rooms);
|
||||
insertMany(database, projectDevices, state.projectDevices);
|
||||
insertMany(database, distributionBoards, state.distributionBoards);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { floors } from "../schema/floors.js";
|
|||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import { externalCsvConfigurations } from "../schema/external-csv-configurations.js";
|
||||
|
||||
export interface PersistedProjectStateSnapshot {
|
||||
currentRevision: number;
|
||||
|
|
@ -59,6 +60,11 @@ export function readProjectStateSnapshot(
|
|||
.where(eq(distributionBoards.projectId, projectId))
|
||||
.orderBy(asc(distributionBoards.name), asc(distributionBoards.id))
|
||||
.all();
|
||||
const externalCsvConfiguration = database
|
||||
.select()
|
||||
.from(externalCsvConfigurations)
|
||||
.where(eq(externalCsvConfigurations.projectId, projectId))
|
||||
.get() ?? null;
|
||||
const listRows = database
|
||||
.select()
|
||||
.from(circuitLists)
|
||||
|
|
@ -241,6 +247,7 @@ export function readProjectStateSnapshot(
|
|||
project.enabledDistributionBoardSupplyTypes,
|
||||
},
|
||||
distributionBoards: boardRows,
|
||||
externalCsvConfiguration,
|
||||
circuitLists: listRows,
|
||||
circuitSections: sectionRows,
|
||||
distributionBoardComponents: componentRows,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { floors } from "../schema/floors.js";
|
|||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import { externalCsvConfigurations } from "../schema/external-csv-configurations.js";
|
||||
import {
|
||||
hashProjectStatePayload,
|
||||
readProjectStateSnapshot,
|
||||
|
|
@ -116,6 +117,12 @@ function insertProjectState(
|
|||
.insert(projects)
|
||||
.values({ ...state.project, currentRevision: 0 })
|
||||
.run();
|
||||
if (state.externalCsvConfiguration !== null) {
|
||||
database
|
||||
.insert(externalCsvConfigurations)
|
||||
.values(state.externalCsvConfiguration)
|
||||
.run();
|
||||
}
|
||||
insertMany(database, floors, state.floors);
|
||||
insertMany(database, rooms, state.rooms);
|
||||
insertMany(database, projectDevices, state.projectDevices);
|
||||
|
|
|
|||
22
src/db/schema/external-csv-configurations.ts
Normal file
22
src/db/schema/external-csv-configurations.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
||||
import type { ExternalCsvConfiguration } from "../../external-model/csv/external-csv-contracts.js";
|
||||
import { projects } from "./projects.js";
|
||||
|
||||
export const externalCsvConfigurations = sqliteTable(
|
||||
"external_csv_configurations",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
configurationVersion: integer("configuration_version").notNull(),
|
||||
configuration: text("configuration", { mode: "json" })
|
||||
.$type<ExternalCsvConfiguration>()
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("external_csv_configurations_project_id_unique").on(
|
||||
table.projectId
|
||||
),
|
||||
]
|
||||
);
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { assertExternalCsvConfiguration } from "../../external-model/csv/external-csv-configuration.js";
|
||||
import type { ExternalCsvConfiguration } from "../../external-model/csv/external-csv-contracts.js";
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
|
||||
export const externalCsvConfigurationUpdateCommandType =
|
||||
"external-csv-configuration.update" as const;
|
||||
export const externalCsvConfigurationCommandSchemaVersion = 1 as const;
|
||||
|
||||
export interface ExternalCsvConfigurationSnapshot {
|
||||
id: string;
|
||||
projectId: string;
|
||||
configurationVersion: number;
|
||||
configuration: ExternalCsvConfiguration;
|
||||
}
|
||||
|
||||
export interface ExternalCsvConfigurationUpdatePayload {
|
||||
expected: ExternalCsvConfigurationSnapshot | null;
|
||||
target: ExternalCsvConfigurationSnapshot | null;
|
||||
}
|
||||
|
||||
export interface ExternalCsvConfigurationUpdateProjectCommand
|
||||
extends SerializedProjectCommand<ExternalCsvConfigurationUpdatePayload> {
|
||||
schemaVersion: typeof externalCsvConfigurationCommandSchemaVersion;
|
||||
type: typeof externalCsvConfigurationUpdateCommandType;
|
||||
}
|
||||
|
||||
export function createExternalCsvConfigurationUpdateProjectCommand(
|
||||
expected: ExternalCsvConfigurationSnapshot | null,
|
||||
target: ExternalCsvConfigurationSnapshot | null
|
||||
): ExternalCsvConfigurationUpdateProjectCommand {
|
||||
const command: ExternalCsvConfigurationUpdateProjectCommand = {
|
||||
schemaVersion: externalCsvConfigurationCommandSchemaVersion,
|
||||
type: externalCsvConfigurationUpdateCommandType,
|
||||
payload: { expected, target },
|
||||
};
|
||||
assertExternalCsvConfigurationUpdateProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function assertExternalCsvConfigurationUpdateProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is ExternalCsvConfigurationUpdateProjectCommand {
|
||||
if (
|
||||
command.schemaVersion !== externalCsvConfigurationCommandSchemaVersion ||
|
||||
command.type !== externalCsvConfigurationUpdateCommandType ||
|
||||
!isRecord(command.payload)
|
||||
) {
|
||||
throw new Error("Unsupported external CSV configuration update command.");
|
||||
}
|
||||
assertSnapshot(command.payload.expected, "expected");
|
||||
assertSnapshot(command.payload.target, "target");
|
||||
if (command.payload.expected === null && command.payload.target === null) {
|
||||
throw new Error("External CSV configuration command must change state.");
|
||||
}
|
||||
if (
|
||||
command.payload.expected !== null &&
|
||||
command.payload.target !== null &&
|
||||
(command.payload.expected.id !== command.payload.target.id ||
|
||||
command.payload.expected.projectId !== command.payload.target.projectId)
|
||||
) {
|
||||
throw new Error("External CSV configuration identity must remain stable.");
|
||||
}
|
||||
}
|
||||
|
||||
function assertSnapshot(
|
||||
value: unknown,
|
||||
field: string
|
||||
): asserts value is ExternalCsvConfigurationSnapshot | null {
|
||||
if (value === null) {
|
||||
return;
|
||||
}
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${field} external CSV configuration must be an object or null.`);
|
||||
}
|
||||
assertId(value.id, `${field}.id`);
|
||||
assertId(value.projectId, `${field}.projectId`);
|
||||
if (!Number.isSafeInteger(value.configurationVersion) || (value.configurationVersion as number) < 1) {
|
||||
throw new Error(`${field}.configurationVersion must be a positive integer.`);
|
||||
}
|
||||
assertExternalCsvConfiguration(value.configuration);
|
||||
if (Object.keys(value).length !== 4) {
|
||||
throw new Error(`${field} external CSV configuration contains unknown fields.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertId(value: unknown, field: string) {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new Error(`${field} must be a non-empty string.`);
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
|
@ -18,9 +18,12 @@ import {
|
|||
resolveCircuitPhaseType,
|
||||
resolveProjectVoltage,
|
||||
} from "../services/project-voltage.service.js";
|
||||
import { validateExternalCsvConfiguration } from "../../external-model/csv/external-csv-configuration.js";
|
||||
import type { ExternalCsvConfiguration } from "../../external-model/csv/external-csv-contracts.js";
|
||||
|
||||
export const projectStateSnapshotSchemaVersion = 2 as const;
|
||||
const previousProjectStateSnapshotSchemaVersion = 1 as const;
|
||||
export const projectStateSnapshotSchemaVersion = 3 as const;
|
||||
const previousProjectStateSnapshotSchemaVersion = 2 as const;
|
||||
const baselineProjectStateSnapshotSchemaVersion = 1 as const;
|
||||
|
||||
const idSchema = z.string().trim().min(1);
|
||||
const nullableStringSchema = z.string().nullable();
|
||||
|
|
@ -249,6 +252,17 @@ const componentProtectionDeviceSchema = z
|
|||
.strict()
|
||||
.superRefine(validatePersistedProtectionDevice);
|
||||
|
||||
const externalCsvConfigurationSnapshotSchema = z
|
||||
.object({
|
||||
id: idSchema,
|
||||
projectId: idSchema,
|
||||
configurationVersion: z.number().int().positive(),
|
||||
configuration: z.custom<ExternalCsvConfiguration>(
|
||||
(value) => validateExternalCsvConfiguration(value).success
|
||||
),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const projectStateSnapshotContents = {
|
||||
circuitLists: z.array(circuitListSchema),
|
||||
circuitSections: z.array(circuitSectionSchema),
|
||||
|
|
@ -268,6 +282,7 @@ export const projectStateSnapshotSchema = z
|
|||
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
||||
project: projectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||
...projectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
|
@ -275,6 +290,15 @@ export const projectStateSnapshotSchema = z
|
|||
const previousProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion),
|
||||
project: projectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
...projectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
const baselineProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(baselineProjectStateSnapshotSchemaVersion),
|
||||
project: previousProjectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
...projectStateSnapshotContents,
|
||||
|
|
@ -301,20 +325,29 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
|||
value === null ||
|
||||
typeof value !== "object" ||
|
||||
Array.isArray(value) ||
|
||||
(value as { schemaVersion?: unknown }).schemaVersion !==
|
||||
previousProjectStateSnapshotSchemaVersion
|
||||
typeof (value as { schemaVersion?: unknown }).schemaVersion !== "number"
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
const previous = previousProjectStateSnapshotSchema.parse(value);
|
||||
return {
|
||||
...previous,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
project: {
|
||||
...previous.project,
|
||||
isPublicBuilding: false,
|
||||
},
|
||||
};
|
||||
const schemaVersion = (value as { schemaVersion: number }).schemaVersion;
|
||||
if (schemaVersion === previousProjectStateSnapshotSchemaVersion) {
|
||||
const previous = previousProjectStateSnapshotSchema.parse(value);
|
||||
return {
|
||||
...previous,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
externalCsvConfiguration: null,
|
||||
};
|
||||
}
|
||||
if (schemaVersion === baselineProjectStateSnapshotSchemaVersion) {
|
||||
const baseline = baselineProjectStateSnapshotSchema.parse(value);
|
||||
return {
|
||||
...baseline,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
project: { ...baseline.project, isPublicBuilding: false },
|
||||
externalCsvConfiguration: null,
|
||||
};
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeSnapshotPhaseTypes(
|
||||
|
|
@ -377,6 +410,12 @@ function assertProjectStateSnapshotRelations(
|
|||
snapshot: ProjectStateSnapshot
|
||||
) {
|
||||
const projectId = snapshot.project.id;
|
||||
if (
|
||||
snapshot.externalCsvConfiguration !== null &&
|
||||
snapshot.externalCsvConfiguration.projectId !== projectId
|
||||
) {
|
||||
throw new Error("Snapshot external CSV configuration belongs to a different project.");
|
||||
}
|
||||
const boardIds = uniqueIds(
|
||||
snapshot.distributionBoards,
|
||||
"distribution board"
|
||||
|
|
|
|||
|
|
@ -94,6 +94,14 @@ export function remapProjectState(
|
|||
return parseProjectStateSnapshot({
|
||||
...source,
|
||||
project: { ...source.project, id: targetProjectId, name },
|
||||
externalCsvConfiguration:
|
||||
source.externalCsvConfiguration === null
|
||||
? null
|
||||
: {
|
||||
...source.externalCsvConfiguration,
|
||||
id: createId(),
|
||||
projectId: targetProjectId,
|
||||
},
|
||||
distributionBoards: source.distributionBoards.map((board) => ({
|
||||
...board,
|
||||
id: requiredId(boardIds, board.id),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import type { ExternalCsvConfigurationUpdateProjectCommand } from "../models/external-csv-configuration-project-command.model.js";
|
||||
import type { AppendedProjectRevision, ProjectRevisionSource } from "./project-revision.store.js";
|
||||
|
||||
export interface ExecuteExternalCsvConfigurationCommandInput {
|
||||
projectId: string;
|
||||
expectedRevision: number;
|
||||
source: ProjectRevisionSource;
|
||||
description?: string;
|
||||
actorId?: string;
|
||||
historyTargetChangeSetId?: string;
|
||||
command: ExternalCsvConfigurationUpdateProjectCommand;
|
||||
}
|
||||
|
||||
export interface ExternalCsvConfigurationProjectCommandStore {
|
||||
execute(input: ExecuteExternalCsvConfigurationCommandInput): {
|
||||
revision: AppendedProjectRevision;
|
||||
inverse: ExternalCsvConfigurationUpdateProjectCommand;
|
||||
};
|
||||
}
|
||||
|
|
@ -161,6 +161,11 @@ import type { ProjectDeviceStructureProjectCommandStore } from "../ports/project
|
|||
import type { ProjectRevisionSource } from "../ports/project-revision.store.js";
|
||||
import type { ProjectSettingsProjectCommandStore } from "../ports/project-settings-project-command.store.js";
|
||||
import type { ProjectStateRestoreCommandStore } from "../ports/project-state-restore-command.store.js";
|
||||
import {
|
||||
assertExternalCsvConfigurationUpdateProjectCommand,
|
||||
externalCsvConfigurationUpdateCommandType,
|
||||
} from "../models/external-csv-configuration-project-command.model.js";
|
||||
import type { ExternalCsvConfigurationProjectCommandStore } from "../ports/external-csv-configuration-project-command.store.js";
|
||||
|
||||
interface DispatchProjectCommandInput {
|
||||
projectId: string;
|
||||
|
|
@ -195,7 +200,8 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||
private readonly circuitGroupMoveStore: CircuitGroupMoveProjectCommandStore,
|
||||
private readonly circuitGroupSubtreeStore: CircuitGroupSubtreeProjectCommandStore,
|
||||
private readonly circuitProtectionStore: CircuitProtectionProjectCommandStore,
|
||||
private readonly historyStore: ProjectHistoryStore
|
||||
private readonly historyStore: ProjectHistoryStore,
|
||||
private readonly externalCsvConfigurationStore?: ExternalCsvConfigurationProjectCommandStore
|
||||
) {}
|
||||
|
||||
executeUser(
|
||||
|
|
@ -552,6 +558,16 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case externalCsvConfigurationUpdateCommandType: {
|
||||
assertExternalCsvConfigurationUpdateProjectCommand(input.command);
|
||||
if (!this.externalCsvConfigurationStore) {
|
||||
throw new Error("External CSV configuration store is not available.");
|
||||
}
|
||||
return this.externalCsvConfigurationStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectStateRestoreCommandType: {
|
||||
assertProjectStateRestoreCommand(input.command);
|
||||
return this.projectStateRestoreStore.execute({
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export function assertExternalCsvConfiguration(
|
|||
if (!isRecord(value)) {
|
||||
throw new Error("External CSV configuration must be an object.");
|
||||
}
|
||||
assertExactKeyCount(value, 9, "External CSV configuration");
|
||||
if (value.schemaVersion !== externalCsvConfigurationSchemaVersion) {
|
||||
throw new Error("Unsupported external CSV configuration schema version.");
|
||||
}
|
||||
|
|
@ -47,6 +48,7 @@ export function assertExternalCsvConfiguration(
|
|||
if (!isRecord(value.columns)) {
|
||||
throw new Error("External CSV columns must be an object.");
|
||||
}
|
||||
assertExactKeyCount(value.columns, 8, "External CSV columns");
|
||||
const mappedColumns = new Set<string>();
|
||||
for (const key of requiredColumnKeys) {
|
||||
const columnName = assertTrimmedString(value.columns[key], `columns.${key}`);
|
||||
|
|
@ -74,6 +76,7 @@ export function assertExternalCsvConfiguration(
|
|||
if (!isRecord(mapping)) {
|
||||
throw new Error(`additionalSourceMappings.${index} must be an object.`);
|
||||
}
|
||||
assertExactKeyCount(mapping, 2, `additionalSourceMappings.${index}`);
|
||||
const sourceColumn = assertTrimmedString(
|
||||
mapping.sourceColumn,
|
||||
`additionalSourceMappings.${index}.sourceColumn`
|
||||
|
|
@ -100,6 +103,7 @@ export function assertExternalCsvConfiguration(
|
|||
if (!isRecord(rule)) {
|
||||
throw new Error(`familyTypeRules.${index} must be an object.`);
|
||||
}
|
||||
assertExactKeyCount(rule, 6, `familyTypeRules.${index}`);
|
||||
const exactFamilyAndType = assertTrimmedString(
|
||||
rule.exactFamilyAndType,
|
||||
`familyTypeRules.${index}.exactFamilyAndType`
|
||||
|
|
@ -152,9 +156,11 @@ function assertQuantityRule(value: unknown, index: number): asserts value is Ext
|
|||
throw new Error(`familyTypeRules.${index}.quantityRule must be an object.`);
|
||||
}
|
||||
if (value.kind === "mapped-column") {
|
||||
assertExactKeyCount(value, 1, `familyTypeRules.${index}.quantityRule`);
|
||||
return;
|
||||
}
|
||||
if (value.kind === "fixed") {
|
||||
assertExactKeyCount(value, 2, `familyTypeRules.${index}.quantityRule`);
|
||||
assertPositiveFiniteNumber(value.quantity, `familyTypeRules.${index}.quantityRule.quantity`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -172,9 +178,11 @@ function assertDisplayNameSuggestion(
|
|||
throw new Error(`familyTypeRules.${index}.displayNameSuggestion must be an object or null.`);
|
||||
}
|
||||
if (value.kind === "selection-marker" || value.kind === "family-and-type") {
|
||||
assertExactKeyCount(value, 1, `familyTypeRules.${index}.displayNameSuggestion`);
|
||||
return;
|
||||
}
|
||||
if (value.kind === "fixed") {
|
||||
assertExactKeyCount(value, 2, `familyTypeRules.${index}.displayNameSuggestion`);
|
||||
assertTrimmedString(value.value, `familyTypeRules.${index}.displayNameSuggestion.value`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -197,3 +205,9 @@ function assertTrimmedString(value: unknown, field: string) {
|
|||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function assertExactKeyCount(value: Record<string, unknown>, count: number, field: string) {
|
||||
if (Object.keys(value).length !== count) {
|
||||
throw new Error(`${field} contains unknown or missing fields.`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { ProjectDeviceStructureProjectCommandRepository } from "../../db/reposit
|
|||
import { ProjectSettingsProjectCommandRepository } from "../../db/repositories/project-settings-project-command.repository.js";
|
||||
import { ProjectStateRestoreCommandRepository } from "../../db/repositories/project-state-restore-command.repository.js";
|
||||
import { ProjectCommandService } from "../../domain/services/project-command.service.js";
|
||||
import { ExternalCsvConfigurationProjectCommandRepository } from "../../db/repositories/external-csv-configuration-project-command.repository.js";
|
||||
|
||||
export const circuitProjectCommandStore = new CircuitProjectCommandRepository(db);
|
||||
export const circuitDeviceRowProjectCommandStore =
|
||||
|
|
@ -65,6 +66,8 @@ export const projectSettingsProjectCommandStore =
|
|||
export const projectStateRestoreCommandStore =
|
||||
new ProjectStateRestoreCommandRepository(db);
|
||||
export const projectHistoryStore = new ProjectHistoryRepository(db);
|
||||
export const externalCsvConfigurationProjectCommandStore =
|
||||
new ExternalCsvConfigurationProjectCommandRepository(db);
|
||||
export const projectCommandService = new ProjectCommandService(
|
||||
circuitProjectCommandStore,
|
||||
circuitDeviceRowProjectCommandStore,
|
||||
|
|
@ -87,5 +90,6 @@ export const projectCommandService = new ProjectCommandService(
|
|||
circuitGroupMoveProjectCommandStore,
|
||||
circuitGroupSubtreeProjectCommandStore,
|
||||
circuitProtectionProjectCommandStore,
|
||||
projectHistoryStore
|
||||
projectHistoryStore,
|
||||
externalCsvConfigurationProjectCommandStore
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue