Create clean release database baseline

This commit is contained in:
2026-07-31 14:07:26 +02:00
parent 5bee3cb103
commit 734a2bcfc4
129 changed files with 2121 additions and 30607 deletions
@@ -382,9 +382,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository
equipmentIdentifier: snapshot.equipmentIdentifier,
displayName: snapshot.displayName,
sortOrder: snapshot.sortOrder,
protectionType: snapshot.protectionType,
protectionRatedCurrent: snapshot.protectionRatedCurrent,
protectionCharacteristic: snapshot.protectionCharacteristic,
cableType: snapshot.cableType,
cableCrossSection: snapshot.cableCrossSection,
cableLength: snapshot.cableLength,
@@ -423,11 +420,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository
snapshot.equipmentIdentifier ||
current.displayName !== snapshot.displayName ||
current.sortOrder !== snapshot.sortOrder ||
current.protectionType !== snapshot.protectionType ||
current.protectionRatedCurrent !==
snapshot.protectionRatedCurrent ||
current.protectionCharacteristic !==
snapshot.protectionCharacteristic ||
current.cableType !== snapshot.cableType ||
current.cableCrossSection !== snapshot.cableCrossSection ||
current.cableLength !== snapshot.cableLength ||
@@ -77,11 +77,6 @@ export class CircuitDeviceRowStructureProjectCommandRepository
source: ExecuteCircuitDeviceRowStructureCommandInput["source"],
row: CircuitDeviceRowSnapshot
) {
if (source === "user" && row.legacyConsumerId !== null) {
throw new Error(
"User commands cannot assign legacy consumer ids."
);
}
if (source === "user" && !isElectricalPhaseType(row.phaseType)) {
throw new Error("Device-row phase type is invalid.");
}
@@ -49,7 +49,6 @@ export function toCircuitDeviceRowSnapshot(
id: row.id,
circuitId: row.circuitId,
linkedProjectDeviceId: row.linkedProjectDeviceId,
legacyConsumerId: row.legacyConsumerId,
sortOrder: row.sortOrder,
name: row.name,
displayName: row.displayName,
@@ -44,7 +44,6 @@ export interface CircuitDeviceRowPatchInput {
export interface CircuitDeviceRowCreateInput extends CircuitDeviceRowUpdateInput {
circuitId: string;
linkedProjectDeviceId?: string;
legacyConsumerId?: string;
sortOrder: number;
}
@@ -110,7 +109,6 @@ export function toCircuitDeviceRowCreateValues(
return {
id,
circuitId: input.circuitId,
legacyConsumerId: input.legacyConsumerId ?? null,
sortOrder: input.sortOrder,
...toCircuitDeviceRowUpdateValues(input),
};
@@ -25,7 +25,6 @@ export class CircuitDeviceRowRepository {
id: circuitDeviceRows.id,
circuitId: circuitDeviceRows.circuitId,
linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId,
legacyConsumerId: circuitDeviceRows.legacyConsumerId,
sortOrder: circuitDeviceRows.sortOrder,
name: circuitDeviceRows.name,
displayName: circuitDeviceRows.displayName,
@@ -133,11 +133,6 @@ export class CircuitStructureProjectCommandRepository
throw new Error("Circuit device-row id already exists.");
}
for (const row of snapshot.deviceRows) {
if (source === "user" && row.legacyConsumerId !== null) {
throw new Error(
"User commands cannot assign legacy consumer ids."
);
}
if (source === "user" && !isElectricalPhaseType(row.phaseType)) {
throw new Error("Device-row phase type is invalid.");
}
@@ -158,9 +153,6 @@ export class CircuitStructureProjectCommandRepository
equipmentIdentifier: snapshot.equipmentIdentifier,
displayName: snapshot.displayName,
sortOrder: snapshot.sortOrder,
protectionType: snapshot.protectionType,
protectionRatedCurrent: snapshot.protectionRatedCurrent,
protectionCharacteristic: snapshot.protectionCharacteristic,
cableType: snapshot.cableType,
cableCrossSection: snapshot.cableCrossSection,
cableLength: snapshot.cableLength,
@@ -238,9 +230,6 @@ export class CircuitStructureProjectCommandRepository
equipmentIdentifier: circuit.equipmentIdentifier,
displayName: circuit.displayName,
sortOrder: circuit.sortOrder,
protectionType: circuit.protectionType,
protectionRatedCurrent: circuit.protectionRatedCurrent,
protectionCharacteristic: circuit.protectionCharacteristic,
cableType: circuit.cableType,
cableCrossSection: circuit.cableCrossSection,
cableLength: circuit.cableLength,
@@ -4,9 +4,6 @@ export interface CircuitCreatePersistenceInput {
equipmentIdentifier: string;
displayName?: string;
sortOrder: number;
protectionType?: string;
protectionRatedCurrent?: number;
protectionCharacteristic?: string;
cableType?: string;
cableCrossSection?: string;
cableLength?: number;
@@ -24,9 +21,6 @@ export interface CircuitPatchPersistenceInput {
equipmentIdentifier?: string;
displayName?: string | null;
sortOrder?: number;
protectionType?: string | null;
protectionRatedCurrent?: number | null;
protectionCharacteristic?: string | null;
cableType?: string | null;
cableCrossSection?: string | null;
cableLength?: number | null;
@@ -47,9 +41,6 @@ export function toCircuitCreateValues(id: string, input: CircuitCreatePersistenc
equipmentIdentifier: input.equipmentIdentifier,
displayName: input.displayName ?? null,
sortOrder: input.sortOrder,
protectionType: input.protectionType ?? null,
protectionRatedCurrent: input.protectionRatedCurrent ?? null,
protectionCharacteristic: input.protectionCharacteristic ?? null,
cableType: input.cableType ?? null,
cableCrossSection: input.cableCrossSection ?? null,
cableLength: input.cableLength ?? null,
@@ -74,13 +65,6 @@ export function toCircuitPatchValues(input: CircuitPatchPersistenceInput) {
}
if (has("displayName")) values.displayName = input.displayName ?? null;
if (input.sortOrder !== undefined) values.sortOrder = input.sortOrder;
if (has("protectionType")) values.protectionType = input.protectionType ?? null;
if (has("protectionRatedCurrent")) {
values.protectionRatedCurrent = input.protectionRatedCurrent ?? null;
}
if (has("protectionCharacteristic")) {
values.protectionCharacteristic = input.protectionCharacteristic ?? null;
}
if (has("cableType")) values.cableType = input.cableType ?? null;
if (has("cableCrossSection")) {
values.cableCrossSection = input.cableCrossSection ?? null;
@@ -12,7 +12,6 @@ import {
import {
assertDistributionBoardUpdateProjectCommand,
createDistributionBoardUpdateProjectCommand,
legacyDistributionBoardUpdateCommandSchemaVersion,
type DistributionBoardUpdateField,
type DistributionBoardUpdatePatch,
type DistributionBoardUpdateProjectCommand,
@@ -332,19 +331,10 @@ export class DistributionBoardStructureProjectCommandRepository
getDistributionBoardFieldValue(current, change.field),
])
) as DistributionBoardUpdatePatch;
const currentInverse = createDistributionBoardUpdateProjectCommand(
const inverse = createDistributionBoardUpdateProjectCommand(
current.id,
inversePatch
);
const inverse =
input.command.schemaVersion ===
legacyDistributionBoardUpdateCommandSchemaVersion
? ({
...currentInverse,
schemaVersion:
legacyDistributionBoardUpdateCommandSchemaVersion,
} as DistributionBoardUpdateProjectCommand)
: currentInverse;
const updated = database
.update(distributionBoards)
.set(patch)
@@ -1,142 +0,0 @@
import crypto from "node:crypto";
import { eq, inArray, isNull } from "drizzle-orm";
import type {
LegacyMigrationCircuitInput,
LegacyMigrationReportInput,
} from "../../domain/ports/legacy-consumer-migration.store.js";
import type { AppDatabase } from "../database-context.js";
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { circuits } from "../schema/circuits.js";
import { consumers } from "../schema/consumers.js";
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js";
import { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js";
import { toCircuitCreateValues } from "./circuit.persistence.js";
export class LegacyConsumerMigrationRepository {
constructor(private readonly database: AppDatabase) {}
async listSourceConsumersByCircuitList(circuitListId: string) {
return this.database
.select()
.from(consumers)
.where(eq(consumers.circuitListId, circuitListId));
}
async listUnmigratedConsumers() {
return this.database
.select({
id: consumers.id,
projectId: consumers.projectId,
circuitListId: consumers.circuitListId,
})
.from(consumers)
.leftJoin(
legacyConsumerCircuitMigrations,
eq(legacyConsumerCircuitMigrations.consumerId, consumers.id)
)
.where(isNull(legacyConsumerCircuitMigrations.consumerId));
}
async listMigratedConsumerIds(circuitListId: string) {
const rows = await this.database
.select({ consumerId: legacyConsumerCircuitMigrations.consumerId })
.from(legacyConsumerCircuitMigrations)
.where(eq(legacyConsumerCircuitMigrations.circuitListId, circuitListId));
return rows.map((row) => row.consumerId);
}
persistCircuitListMigration(input: {
circuitListId: string;
circuits: LegacyMigrationCircuitInput[];
report: LegacyMigrationReportInput;
}) {
if (input.circuits.some((entry) => entry.circuit.circuitListId !== input.circuitListId)) {
throw new Error("All migrated circuits must belong to the target circuit list.");
}
const consumerIds = input.circuits.flatMap((entry) =>
entry.deviceRows.map((row) => row.legacyConsumerId)
);
if (new Set(consumerIds).size !== consumerIds.length) {
throw new Error("A legacy consumer may only be migrated once per operation.");
}
const preparedCircuits = input.circuits.map((entry) => {
const circuitId = crypto.randomUUID();
return {
circuitId,
circuitValues: toCircuitCreateValues(circuitId, entry.circuit),
rows: entry.deviceRows.map((row) => {
const rowId = crypto.randomUUID();
return {
consumerId: row.legacyConsumerId,
rowId,
rowValues: toCircuitDeviceRowCreateValues(rowId, {
...row,
circuitId,
}),
};
}),
};
});
const createdAtIso = new Date().toISOString();
this.database.transaction((tx) => {
if (consumerIds.length > 0) {
const existingMappings = tx
.select({ consumerId: legacyConsumerCircuitMigrations.consumerId })
.from(legacyConsumerCircuitMigrations)
.where(inArray(legacyConsumerCircuitMigrations.consumerId, consumerIds))
.all();
if (existingMappings.length > 0) {
throw new Error("A legacy consumer was migrated before this operation completed.");
}
}
for (const preparedCircuit of preparedCircuits) {
tx.insert(circuits).values(preparedCircuit.circuitValues).run();
for (const row of preparedCircuit.rows) {
tx.insert(circuitDeviceRows).values(row.rowValues).run();
tx
.insert(legacyConsumerCircuitMigrations)
.values({
consumerId: row.consumerId,
circuitId: preparedCircuit.circuitId,
circuitDeviceRowId: row.rowId,
circuitListId: input.circuitListId,
createdAtIso,
})
.run();
}
}
const reportValues = {
...input.report,
createdAtIso,
};
const existingReport = tx
.select({ id: legacyConsumerMigrationReports.id })
.from(legacyConsumerMigrationReports)
.where(eq(legacyConsumerMigrationReports.circuitListId, input.circuitListId))
.limit(1)
.all();
if (existingReport.length > 0) {
tx
.update(legacyConsumerMigrationReports)
.set(reportValues)
.where(eq(legacyConsumerMigrationReports.id, existingReport[0].id))
.run();
} else {
tx
.insert(legacyConsumerMigrationReports)
.values({
id: crypto.randomUUID(),
circuitListId: input.circuitListId,
...reportValues,
})
.run();
}
});
}
}
@@ -34,7 +34,6 @@ const circuitDeviceRowSnapshotFields = [
"id",
"circuitId",
"linkedProjectDeviceId",
"legacyConsumerId",
"sortOrder",
"name",
"displayName",
@@ -22,7 +22,6 @@ import type {
} from "../../domain/ports/project-location-structure-project-command.store.js";
import type { AppDatabase } from "../database-context.js";
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { consumers } from "../schema/consumers.js";
import { floors } from "../schema/floors.js";
import { distributionBoards } from "../schema/distribution-boards.js";
import { projects } from "../schema/projects.js";
@@ -216,13 +215,7 @@ export class ProjectLocationStructureProjectCommandRepository
.where(eq(circuitDeviceRows.roomId, expected.id))
.limit(1)
.get();
const referencedLegacyConsumer = database
.select({ id: consumers.id })
.from(consumers)
.where(eq(consumers.roomId, expected.id))
.limit(1)
.get();
if (referencedDeviceRow || referencedLegacyConsumer) {
if (referencedDeviceRow) {
throw new Error(
"A referenced project room cannot be removed by history."
);
@@ -2,9 +2,6 @@ import { and, eq, isNotNull } from "drizzle-orm";
import {
assertProjectSettingsUpdateProjectCommand,
createProjectSettingsUpdateProjectCommand,
legacyProjectSettingsUpdateCommandSchemaVersion,
normalizeProjectSettingsValues,
previousProjectSettingsUpdateCommandSchemaVersion,
type ProjectSettingsValues,
} from "../../domain/models/project-settings-project-command.model.js";
import type {
@@ -43,49 +40,22 @@ export class ProjectSettingsProjectCommandRepository
if (!current) {
throw new Error("Project not found.");
}
const target = normalizeProjectSettingsValues(input.command, current);
const target = input.command.payload;
if (projectSettingsEqual(current, target)) {
throw new Error("Project settings did not change.");
}
const inverse =
input.command.schemaVersion ===
legacyProjectSettingsUpdateCommandSchemaVersion
? {
schemaVersion: legacyProjectSettingsUpdateCommandSchemaVersion,
type: input.command.type,
payload: {
singlePhaseVoltageV: current.singlePhaseVoltageV,
threePhaseVoltageV: current.threePhaseVoltageV,
},
}
: input.command.schemaVersion ===
previousProjectSettingsUpdateCommandSchemaVersion
? {
schemaVersion:
previousProjectSettingsUpdateCommandSchemaVersion,
type: input.command.type,
payload: {
name: current.name,
internalProjectNumber: current.internalProjectNumber,
externalProjectNumber: current.externalProjectNumber,
buildingOwner: current.buildingOwner,
description: current.description,
singlePhaseVoltageV: current.singlePhaseVoltageV,
threePhaseVoltageV: current.threePhaseVoltageV,
},
}
: createProjectSettingsUpdateProjectCommand({
name: current.name,
internalProjectNumber: current.internalProjectNumber,
externalProjectNumber: current.externalProjectNumber,
buildingOwner: current.buildingOwner,
description: current.description,
singlePhaseVoltageV: current.singlePhaseVoltageV,
threePhaseVoltageV: current.threePhaseVoltageV,
enabledDistributionBoardSupplyTypes:
current.enabledDistributionBoardSupplyTypes,
});
const inverse = createProjectSettingsUpdateProjectCommand({
name: current.name,
internalProjectNumber: current.internalProjectNumber,
externalProjectNumber: current.externalProjectNumber,
buildingOwner: current.buildingOwner,
description: current.description,
singlePhaseVoltageV: current.singlePhaseVoltageV,
threePhaseVoltageV: current.threePhaseVoltageV,
enabledDistributionBoardSupplyTypes:
current.enabledDistributionBoardSupplyTypes,
});
assertDisabledSupplyTypesAreUnused(tx, input.projectId, target);
const updated = tx
.update(projects)
@@ -4,14 +4,8 @@ import { ProjectRevisionConflictError } from "../../domain/errors/project-revisi
import { ProjectSnapshotNameConflictError } from "../../domain/errors/project-snapshot-name-conflict.error.js";
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
import {
distributionBoardProjectStateSnapshotSchemaVersion,
deserializeProjectStateSnapshot,
legacyProjectStateSnapshotSchemaVersion,
previousProjectStateSnapshotSchemaVersion,
projectStateSnapshotSchemaVersion,
simultaneityFactorProjectStateSnapshotSchemaVersion,
supplyTypesProjectStateSnapshotSchemaVersion,
voltageProjectStateSnapshotSchemaVersion,
} from "../../domain/models/project-state-snapshot.model.js";
import type {
CreateNamedProjectSnapshotInput,
@@ -144,19 +138,7 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
if (actualChecksum !== stored.payloadSha256) {
throw new Error("Project snapshot checksum verification failed.");
}
if (
stored.schemaVersion !== legacyProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !== previousProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !==
distributionBoardProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !==
supplyTypesProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !==
voltageProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !==
simultaneityFactorProjectStateSnapshotSchemaVersion &&
stored.schemaVersion !== projectStateSnapshotSchemaVersion
) {
if (stored.schemaVersion !== projectStateSnapshotSchemaVersion) {
throw new Error("Project snapshot schema version is not supported.");
}
const targetState = deserializeProjectStateSnapshot(stored.payloadJson);
@@ -18,13 +18,10 @@ import { circuitLists } from "../schema/circuit-lists.js";
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
import { circuitSections } from "../schema/circuit-sections.js";
import { circuits } from "../schema/circuits.js";
import { consumers } from "../schema/consumers.js";
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
import { distributionBoards } from "../schema/distribution-boards.js";
import { floors } from "../schema/floors.js";
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js";
import { projectDevices } from "../schema/project-devices.js";
import { projectChangeSets } from "../schema/project-change-sets.js";
import { projects } from "../schema/projects.js";
@@ -186,10 +183,6 @@ function replaceProjectState(
database: AppDatabase,
state: ProjectStateSnapshot
) {
const upgradeOnlyData = captureUpgradeOnlyData(
database,
state.project.id
);
database
.delete(distributionBoards)
.where(eq(distributionBoards.projectId, state.project.id))
@@ -257,136 +250,6 @@ function replaceProjectState(
circuitDeviceRows,
state.circuits.flatMap((circuit) => circuit.deviceRows)
);
restoreCompatibleUpgradeOnlyData(database, state, upgradeOnlyData);
}
function captureUpgradeOnlyData(
database: AppDatabase,
projectId: string
) {
const consumerLinks = database
.select({
id: consumers.id,
distributionBoardId: consumers.distributionBoardId,
circuitListId: consumers.circuitListId,
projectDeviceId: consumers.projectDeviceId,
roomId: consumers.roomId,
})
.from(consumers)
.where(eq(consumers.projectId, projectId))
.all();
const migrationMappings = database
.select({
consumerId: legacyConsumerCircuitMigrations.consumerId,
circuitId: legacyConsumerCircuitMigrations.circuitId,
circuitDeviceRowId:
legacyConsumerCircuitMigrations.circuitDeviceRowId,
circuitListId: legacyConsumerCircuitMigrations.circuitListId,
createdAtIso: legacyConsumerCircuitMigrations.createdAtIso,
})
.from(legacyConsumerCircuitMigrations)
.innerJoin(
circuitLists,
eq(
circuitLists.id,
legacyConsumerCircuitMigrations.circuitListId
)
)
.where(eq(circuitLists.projectId, projectId))
.all();
const migrationReports = database
.select({
id: legacyConsumerMigrationReports.id,
circuitListId: legacyConsumerMigrationReports.circuitListId,
legacyConsumerCount:
legacyConsumerMigrationReports.legacyConsumerCount,
createdCircuitCount:
legacyConsumerMigrationReports.createdCircuitCount,
createdDeviceRowCount:
legacyConsumerMigrationReports.createdDeviceRowCount,
duplicateGroupedCount:
legacyConsumerMigrationReports.duplicateGroupedCount,
generatedIdentifierCount:
legacyConsumerMigrationReports.generatedIdentifierCount,
unassignedRowCount:
legacyConsumerMigrationReports.unassignedRowCount,
warningsJson: legacyConsumerMigrationReports.warningsJson,
generatedIdentifiersJson:
legacyConsumerMigrationReports.generatedIdentifiersJson,
duplicateGroupsJson:
legacyConsumerMigrationReports.duplicateGroupsJson,
createdAtIso: legacyConsumerMigrationReports.createdAtIso,
})
.from(legacyConsumerMigrationReports)
.innerJoin(
circuitLists,
eq(circuitLists.id, legacyConsumerMigrationReports.circuitListId)
)
.where(eq(circuitLists.projectId, projectId))
.all();
return { consumerLinks, migrationMappings, migrationReports };
}
function restoreCompatibleUpgradeOnlyData(
database: AppDatabase,
state: ProjectStateSnapshot,
data: ReturnType<typeof captureUpgradeOnlyData>
) {
const boardIds = new Set(
state.distributionBoards.map((entry) => entry.id)
);
const listIds = new Set(state.circuitLists.map((entry) => entry.id));
const deviceIds = new Set(
state.projectDevices.map((entry) => entry.id)
);
const roomIds = new Set(state.rooms.map((entry) => entry.id));
const circuitIds = new Set(state.circuits.map((entry) => entry.id));
const rowIds = new Set(
state.circuits.flatMap((circuit) =>
circuit.deviceRows.map((entry) => entry.id)
)
);
for (const link of data.consumerLinks) {
database
.update(consumers)
.set({
distributionBoardId:
link.distributionBoardId &&
boardIds.has(link.distributionBoardId)
? link.distributionBoardId
: null,
circuitListId:
link.circuitListId && listIds.has(link.circuitListId)
? link.circuitListId
: null,
projectDeviceId:
link.projectDeviceId && deviceIds.has(link.projectDeviceId)
? link.projectDeviceId
: null,
roomId:
link.roomId && roomIds.has(link.roomId) ? link.roomId : null,
})
.where(eq(consumers.id, link.id))
.run();
}
insertMany(
database,
legacyConsumerCircuitMigrations,
data.migrationMappings.filter(
(entry) =>
listIds.has(entry.circuitListId) &&
circuitIds.has(entry.circuitId) &&
rowIds.has(entry.circuitDeviceRowId)
)
);
insertMany(
database,
legacyConsumerMigrationReports,
data.migrationReports.filter((entry) =>
listIds.has(entry.circuitListId)
)
);
}
function insertMany<TTable extends Parameters<AppDatabase["insert"]>[0]>(
@@ -145,9 +145,6 @@ export function readProjectStateSnapshot(
equipmentIdentifier: circuits.equipmentIdentifier,
displayName: circuits.displayName,
sortOrder: circuits.sortOrder,
protectionType: circuits.protectionType,
protectionRatedCurrent: circuits.protectionRatedCurrent,
protectionCharacteristic: circuits.protectionCharacteristic,
cableType: circuits.cableType,
cableCrossSection: circuits.cableCrossSection,
cableLength: circuits.cableLength,
@@ -173,7 +170,6 @@ export function readProjectStateSnapshot(
id: circuitDeviceRows.id,
circuitId: circuitDeviceRows.circuitId,
linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId,
legacyConsumerId: circuitDeviceRows.legacyConsumerId,
sortOrder: circuitDeviceRows.sortOrder,
name: circuitDeviceRows.name,
displayName: circuitDeviceRows.displayName,