Restore named project snapshots
This commit is contained in:
@@ -14,7 +14,7 @@ export function applyProjectHistoryTransition(
|
||||
database: AppDatabase,
|
||||
input: ApplyProjectHistoryTransitionInput
|
||||
) {
|
||||
if (input.source === "user") {
|
||||
if (input.source === "user" || input.source === "restore") {
|
||||
database
|
||||
.delete(projectHistoryStackEntries)
|
||||
.where(
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
import crypto from "node:crypto";
|
||||
import { and, asc, desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { ProjectRevisionConflictError } from "../../domain/errors/project-revision-conflict.error.js";
|
||||
import { ProjectSnapshotNameConflictError } from "../../domain/errors/project-snapshot-name-conflict.error.js";
|
||||
import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js";
|
||||
import {
|
||||
parseProjectStateSnapshot,
|
||||
deserializeProjectStateSnapshot,
|
||||
projectStateSnapshotSchemaVersion,
|
||||
serializeProjectStateSnapshot,
|
||||
} from "../../domain/models/project-state-snapshot.model.js";
|
||||
import type {
|
||||
CreateNamedProjectSnapshotInput,
|
||||
PreparedProjectSnapshotRestore,
|
||||
ProjectSnapshotMetadata,
|
||||
ProjectSnapshotStore,
|
||||
} from "../../domain/ports/project-snapshot.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
import { floors } from "../schema/floors.js";
|
||||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projectSnapshots } from "../schema/project-snapshots.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import {
|
||||
hashProjectStatePayload,
|
||||
readProjectStateSnapshot,
|
||||
} from "./project-state-snapshot.persistence.js";
|
||||
|
||||
export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
@@ -35,25 +32,15 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
||||
const snapshotDescription =
|
||||
input.description?.trim() || null;
|
||||
return this.database.transaction((tx) => {
|
||||
const project = tx
|
||||
.select({
|
||||
id: projects.id,
|
||||
name: projects.name,
|
||||
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: projects.threePhaseVoltageV,
|
||||
currentRevision: projects.currentRevision,
|
||||
})
|
||||
.from(projects)
|
||||
.where(eq(projects.id, input.projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
const current = readProjectStateSnapshot(tx, input.projectId);
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
if (project.currentRevision !== input.expectedRevision) {
|
||||
if (current.currentRevision !== input.expectedRevision) {
|
||||
throw new ProjectRevisionConflictError(
|
||||
input.projectId,
|
||||
input.expectedRevision,
|
||||
project.currentRevision
|
||||
current.currentRevision
|
||||
);
|
||||
}
|
||||
const existing = tx
|
||||
@@ -73,182 +60,21 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
||||
);
|
||||
}
|
||||
|
||||
const boardRows = tx
|
||||
.select()
|
||||
.from(distributionBoards)
|
||||
.where(eq(distributionBoards.projectId, input.projectId))
|
||||
.orderBy(asc(distributionBoards.name), asc(distributionBoards.id))
|
||||
.all();
|
||||
const listRows = tx
|
||||
.select()
|
||||
.from(circuitLists)
|
||||
.where(eq(circuitLists.projectId, input.projectId))
|
||||
.orderBy(asc(circuitLists.name), asc(circuitLists.id))
|
||||
.all();
|
||||
const sectionRows = tx
|
||||
.select({
|
||||
id: circuitSections.id,
|
||||
circuitListId: circuitSections.circuitListId,
|
||||
key: circuitSections.key,
|
||||
displayName: circuitSections.displayName,
|
||||
prefix: circuitSections.prefix,
|
||||
sortOrder: circuitSections.sortOrder,
|
||||
})
|
||||
.from(circuitSections)
|
||||
.innerJoin(
|
||||
circuitLists,
|
||||
eq(circuitLists.id, circuitSections.circuitListId)
|
||||
)
|
||||
.where(eq(circuitLists.projectId, input.projectId))
|
||||
.orderBy(
|
||||
asc(circuitSections.circuitListId),
|
||||
asc(circuitSections.sortOrder),
|
||||
asc(circuitSections.id)
|
||||
)
|
||||
.all();
|
||||
const circuitRows = tx
|
||||
.select({
|
||||
id: circuits.id,
|
||||
circuitListId: circuits.circuitListId,
|
||||
sectionId: circuits.sectionId,
|
||||
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,
|
||||
rcdAssignment: circuits.rcdAssignment,
|
||||
terminalDesignation: circuits.terminalDesignation,
|
||||
voltage: circuits.voltage,
|
||||
controlRequirement: circuits.controlRequirement,
|
||||
status: circuits.status,
|
||||
isReserve: circuits.isReserve,
|
||||
remark: circuits.remark,
|
||||
})
|
||||
.from(circuits)
|
||||
.innerJoin(
|
||||
circuitLists,
|
||||
eq(circuitLists.id, circuits.circuitListId)
|
||||
)
|
||||
.where(eq(circuitLists.projectId, input.projectId))
|
||||
.orderBy(
|
||||
asc(circuits.circuitListId),
|
||||
asc(circuits.sortOrder),
|
||||
asc(circuits.id)
|
||||
)
|
||||
.all();
|
||||
const deviceRowRows = tx
|
||||
.select({
|
||||
id: circuitDeviceRows.id,
|
||||
circuitId: circuitDeviceRows.circuitId,
|
||||
linkedProjectDeviceId:
|
||||
circuitDeviceRows.linkedProjectDeviceId,
|
||||
legacyConsumerId: circuitDeviceRows.legacyConsumerId,
|
||||
sortOrder: circuitDeviceRows.sortOrder,
|
||||
name: circuitDeviceRows.name,
|
||||
displayName: circuitDeviceRows.displayName,
|
||||
phaseType: circuitDeviceRows.phaseType,
|
||||
connectionKind: circuitDeviceRows.connectionKind,
|
||||
costGroup: circuitDeviceRows.costGroup,
|
||||
category: circuitDeviceRows.category,
|
||||
level: circuitDeviceRows.level,
|
||||
roomId: circuitDeviceRows.roomId,
|
||||
roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot,
|
||||
roomNameSnapshot: circuitDeviceRows.roomNameSnapshot,
|
||||
quantity: circuitDeviceRows.quantity,
|
||||
powerPerUnit: circuitDeviceRows.powerPerUnit,
|
||||
simultaneityFactor:
|
||||
circuitDeviceRows.simultaneityFactor,
|
||||
cosPhi: circuitDeviceRows.cosPhi,
|
||||
remark: circuitDeviceRows.remark,
|
||||
overriddenFields: circuitDeviceRows.overriddenFields,
|
||||
})
|
||||
.from(circuitDeviceRows)
|
||||
.innerJoin(
|
||||
circuits,
|
||||
eq(circuits.id, circuitDeviceRows.circuitId)
|
||||
)
|
||||
.innerJoin(
|
||||
circuitLists,
|
||||
eq(circuitLists.id, circuits.circuitListId)
|
||||
)
|
||||
.where(eq(circuitLists.projectId, input.projectId))
|
||||
.orderBy(
|
||||
asc(circuitDeviceRows.circuitId),
|
||||
asc(circuitDeviceRows.sortOrder),
|
||||
asc(circuitDeviceRows.id)
|
||||
)
|
||||
.all();
|
||||
const projectDeviceRows = tx
|
||||
.select()
|
||||
.from(projectDevices)
|
||||
.where(eq(projectDevices.projectId, input.projectId))
|
||||
.orderBy(
|
||||
asc(projectDevices.displayName),
|
||||
asc(projectDevices.id)
|
||||
)
|
||||
.all();
|
||||
const floorRows = tx
|
||||
.select()
|
||||
.from(floors)
|
||||
.where(eq(floors.projectId, input.projectId))
|
||||
.orderBy(asc(floors.sortOrder), asc(floors.id))
|
||||
.all();
|
||||
const roomRows = tx
|
||||
.select()
|
||||
.from(rooms)
|
||||
.where(eq(rooms.projectId, input.projectId))
|
||||
.orderBy(asc(rooms.roomNumber), asc(rooms.id))
|
||||
.all();
|
||||
const deviceRowsByCircuit = new Map<string, typeof deviceRowRows>();
|
||||
for (const row of deviceRowRows) {
|
||||
const entries = deviceRowsByCircuit.get(row.circuitId) ?? [];
|
||||
entries.push(row);
|
||||
deviceRowsByCircuit.set(row.circuitId, entries);
|
||||
}
|
||||
|
||||
const snapshot = parseProjectStateSnapshot({
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
project: {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||
},
|
||||
distributionBoards: boardRows,
|
||||
circuitLists: listRows,
|
||||
circuitSections: sectionRows,
|
||||
circuits: circuitRows.map((circuit) => ({
|
||||
...circuit,
|
||||
isReserve: Boolean(circuit.isReserve),
|
||||
deviceRows: deviceRowsByCircuit.get(circuit.id) ?? [],
|
||||
})),
|
||||
projectDevices: projectDeviceRows,
|
||||
floors: floorRows,
|
||||
rooms: roomRows,
|
||||
});
|
||||
const payloadJson = serializeProjectStateSnapshot(snapshot);
|
||||
const metadata: ProjectSnapshotMetadata = {
|
||||
id: crypto.randomUUID(),
|
||||
projectId: input.projectId,
|
||||
sourceRevision: project.currentRevision,
|
||||
sourceRevision: current.currentRevision,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
name: snapshotName,
|
||||
description: snapshotDescription,
|
||||
payloadSha256: crypto
|
||||
.createHash("sha256")
|
||||
.update(payloadJson)
|
||||
.digest("hex"),
|
||||
payloadSha256: current.payloadSha256,
|
||||
createdAtIso: new Date().toISOString(),
|
||||
createdByActorId: input.actorId ?? null,
|
||||
};
|
||||
tx.insert(projectSnapshots)
|
||||
.values({
|
||||
...metadata,
|
||||
payloadJson,
|
||||
payloadJson: current.payloadJson,
|
||||
})
|
||||
.run();
|
||||
return metadata;
|
||||
@@ -284,6 +110,54 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore {
|
||||
)
|
||||
.all();
|
||||
}
|
||||
|
||||
prepareRestore(
|
||||
projectId: string,
|
||||
snapshotId: string
|
||||
): PreparedProjectSnapshotRestore | null {
|
||||
const current = readProjectStateSnapshot(this.database, projectId);
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
const stored = this.database
|
||||
.select()
|
||||
.from(projectSnapshots)
|
||||
.where(
|
||||
and(
|
||||
eq(projectSnapshots.id, snapshotId),
|
||||
eq(projectSnapshots.projectId, projectId)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
if (!stored) {
|
||||
return null;
|
||||
}
|
||||
const actualChecksum = hashProjectStatePayload(stored.payloadJson);
|
||||
if (actualChecksum !== stored.payloadSha256) {
|
||||
throw new Error("Project snapshot checksum verification failed.");
|
||||
}
|
||||
if (stored.schemaVersion !== projectStateSnapshotSchemaVersion) {
|
||||
throw new Error("Project snapshot schema version is not supported.");
|
||||
}
|
||||
const targetState = deserializeProjectStateSnapshot(stored.payloadJson);
|
||||
return {
|
||||
snapshot: {
|
||||
id: stored.id,
|
||||
projectId: stored.projectId,
|
||||
sourceRevision: stored.sourceRevision,
|
||||
schemaVersion: stored.schemaVersion,
|
||||
name: stored.name,
|
||||
description: stored.description,
|
||||
payloadSha256: stored.payloadSha256,
|
||||
createdAtIso: stored.createdAtIso,
|
||||
createdByActorId: stored.createdByActorId,
|
||||
},
|
||||
command: createProjectStateRestoreCommand(
|
||||
current.payloadSha256,
|
||||
targetState
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function validateCreateInput(input: CreateNamedProjectSnapshotInput) {
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { ProjectStateConflictError } from "../../domain/errors/project-state-conflict.error.js";
|
||||
import {
|
||||
assertProjectStateRestoreCommand,
|
||||
createProjectStateRestoreCommand,
|
||||
} from "../../domain/models/project-state-restore-command.model.js";
|
||||
import {
|
||||
serializeProjectStateSnapshot,
|
||||
type ProjectStateSnapshot,
|
||||
} from "../../domain/models/project-state-snapshot.model.js";
|
||||
import type {
|
||||
ExecuteProjectStateRestoreCommandInput,
|
||||
ProjectStateRestoreCommandStore,
|
||||
} from "../../domain/ports/project-state-restore-command.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { consumers } from "../schema/consumers.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 { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
||||
import {
|
||||
hashProjectStatePayload,
|
||||
readProjectStateSnapshot,
|
||||
} from "./project-state-snapshot.persistence.js";
|
||||
|
||||
export class ProjectStateRestoreCommandRepository
|
||||
implements ProjectStateRestoreCommandStore
|
||||
{
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
execute(input: ExecuteProjectStateRestoreCommandInput) {
|
||||
assertProjectStateRestoreCommand(input.command);
|
||||
if (input.command.payload.targetState.project.id !== input.projectId) {
|
||||
throw new Error("Restore state belongs to a different project.");
|
||||
}
|
||||
|
||||
return this.database.transaction((tx) => {
|
||||
const current = readProjectStateSnapshot(tx, input.projectId);
|
||||
if (!current) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
if (
|
||||
current.payloadSha256 !==
|
||||
input.command.payload.expectedStateSha256
|
||||
) {
|
||||
throw new ProjectStateConflictError(
|
||||
input.projectId,
|
||||
input.command.payload.expectedStateSha256,
|
||||
current.payloadSha256
|
||||
);
|
||||
}
|
||||
|
||||
const targetPayloadJson = serializeProjectStateSnapshot(
|
||||
input.command.payload.targetState
|
||||
);
|
||||
const inverse = createProjectStateRestoreCommand(
|
||||
hashProjectStatePayload(targetPayloadJson),
|
||||
current.state
|
||||
);
|
||||
|
||||
replaceProjectState(tx, input.command.payload.targetState);
|
||||
|
||||
const revision = appendProjectRevision(tx, {
|
||||
projectId: input.projectId,
|
||||
expectedRevision: input.expectedRevision,
|
||||
source: input.source,
|
||||
description: input.description,
|
||||
actorId: input.actorId,
|
||||
forward: input.command,
|
||||
inverse,
|
||||
});
|
||||
applyProjectHistoryTransition(tx, {
|
||||
projectId: input.projectId,
|
||||
source: input.source,
|
||||
recordedChangeSetId: revision.changeSetId,
|
||||
targetChangeSetId: input.historyTargetChangeSetId,
|
||||
});
|
||||
return { revision, inverse };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function replaceProjectState(
|
||||
database: AppDatabase,
|
||||
state: ProjectStateSnapshot
|
||||
) {
|
||||
const upgradeOnlyData = captureUpgradeOnlyData(
|
||||
database,
|
||||
state.project.id
|
||||
);
|
||||
database
|
||||
.delete(distributionBoards)
|
||||
.where(eq(distributionBoards.projectId, state.project.id))
|
||||
.run();
|
||||
database
|
||||
.delete(projectDevices)
|
||||
.where(eq(projectDevices.projectId, state.project.id))
|
||||
.run();
|
||||
database
|
||||
.delete(rooms)
|
||||
.where(eq(rooms.projectId, state.project.id))
|
||||
.run();
|
||||
database
|
||||
.delete(floors)
|
||||
.where(eq(floors.projectId, state.project.id))
|
||||
.run();
|
||||
|
||||
database
|
||||
.update(projects)
|
||||
.set({
|
||||
name: state.project.name,
|
||||
singlePhaseVoltageV: state.project.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: state.project.threePhaseVoltageV,
|
||||
})
|
||||
.where(eq(projects.id, state.project.id))
|
||||
.run();
|
||||
|
||||
insertMany(database, floors, state.floors);
|
||||
insertMany(database, rooms, state.rooms);
|
||||
insertMany(database, projectDevices, state.projectDevices);
|
||||
insertMany(database, distributionBoards, state.distributionBoards);
|
||||
insertMany(database, circuitLists, state.circuitLists);
|
||||
insertMany(database, circuitSections, state.circuitSections);
|
||||
insertMany(
|
||||
database,
|
||||
circuits,
|
||||
state.circuits.map(({ deviceRows: _deviceRows, ...circuit }) => ({
|
||||
...circuit,
|
||||
isReserve: circuit.isReserve ? 1 : 0,
|
||||
}))
|
||||
);
|
||||
insertMany(
|
||||
database,
|
||||
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]>(
|
||||
database: AppDatabase,
|
||||
table: TTable,
|
||||
values: unknown[]
|
||||
) {
|
||||
if (values.length === 0) {
|
||||
return;
|
||||
}
|
||||
database
|
||||
.insert(table)
|
||||
.values(values as never)
|
||||
.run();
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import crypto from "node:crypto";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import {
|
||||
parseProjectStateSnapshot,
|
||||
projectStateSnapshotSchemaVersion,
|
||||
serializeProjectStateSnapshot,
|
||||
type ProjectStateSnapshot,
|
||||
} from "../../domain/models/project-state-snapshot.model.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
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";
|
||||
|
||||
export interface PersistedProjectStateSnapshot {
|
||||
currentRevision: number;
|
||||
state: ProjectStateSnapshot;
|
||||
payloadJson: string;
|
||||
payloadSha256: string;
|
||||
}
|
||||
|
||||
export function readProjectStateSnapshot(
|
||||
database: AppDatabase,
|
||||
projectId: string
|
||||
): PersistedProjectStateSnapshot | null {
|
||||
const project = database
|
||||
.select({
|
||||
id: projects.id,
|
||||
name: projects.name,
|
||||
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: projects.threePhaseVoltageV,
|
||||
currentRevision: projects.currentRevision,
|
||||
})
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const boardRows = database
|
||||
.select()
|
||||
.from(distributionBoards)
|
||||
.where(eq(distributionBoards.projectId, projectId))
|
||||
.orderBy(asc(distributionBoards.name), asc(distributionBoards.id))
|
||||
.all();
|
||||
const listRows = database
|
||||
.select()
|
||||
.from(circuitLists)
|
||||
.where(eq(circuitLists.projectId, projectId))
|
||||
.orderBy(asc(circuitLists.name), asc(circuitLists.id))
|
||||
.all();
|
||||
const sectionRows = database
|
||||
.select({
|
||||
id: circuitSections.id,
|
||||
circuitListId: circuitSections.circuitListId,
|
||||
key: circuitSections.key,
|
||||
displayName: circuitSections.displayName,
|
||||
prefix: circuitSections.prefix,
|
||||
sortOrder: circuitSections.sortOrder,
|
||||
})
|
||||
.from(circuitSections)
|
||||
.innerJoin(
|
||||
circuitLists,
|
||||
eq(circuitLists.id, circuitSections.circuitListId)
|
||||
)
|
||||
.where(eq(circuitLists.projectId, projectId))
|
||||
.orderBy(
|
||||
asc(circuitSections.circuitListId),
|
||||
asc(circuitSections.sortOrder),
|
||||
asc(circuitSections.id)
|
||||
)
|
||||
.all();
|
||||
const circuitRows = database
|
||||
.select({
|
||||
id: circuits.id,
|
||||
circuitListId: circuits.circuitListId,
|
||||
sectionId: circuits.sectionId,
|
||||
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,
|
||||
rcdAssignment: circuits.rcdAssignment,
|
||||
terminalDesignation: circuits.terminalDesignation,
|
||||
voltage: circuits.voltage,
|
||||
controlRequirement: circuits.controlRequirement,
|
||||
status: circuits.status,
|
||||
isReserve: circuits.isReserve,
|
||||
remark: circuits.remark,
|
||||
})
|
||||
.from(circuits)
|
||||
.innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId))
|
||||
.where(eq(circuitLists.projectId, projectId))
|
||||
.orderBy(
|
||||
asc(circuits.circuitListId),
|
||||
asc(circuits.sortOrder),
|
||||
asc(circuits.id)
|
||||
)
|
||||
.all();
|
||||
const deviceRowRows = database
|
||||
.select({
|
||||
id: circuitDeviceRows.id,
|
||||
circuitId: circuitDeviceRows.circuitId,
|
||||
linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId,
|
||||
legacyConsumerId: circuitDeviceRows.legacyConsumerId,
|
||||
sortOrder: circuitDeviceRows.sortOrder,
|
||||
name: circuitDeviceRows.name,
|
||||
displayName: circuitDeviceRows.displayName,
|
||||
phaseType: circuitDeviceRows.phaseType,
|
||||
connectionKind: circuitDeviceRows.connectionKind,
|
||||
costGroup: circuitDeviceRows.costGroup,
|
||||
category: circuitDeviceRows.category,
|
||||
level: circuitDeviceRows.level,
|
||||
roomId: circuitDeviceRows.roomId,
|
||||
roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot,
|
||||
roomNameSnapshot: circuitDeviceRows.roomNameSnapshot,
|
||||
quantity: circuitDeviceRows.quantity,
|
||||
powerPerUnit: circuitDeviceRows.powerPerUnit,
|
||||
simultaneityFactor: circuitDeviceRows.simultaneityFactor,
|
||||
cosPhi: circuitDeviceRows.cosPhi,
|
||||
remark: circuitDeviceRows.remark,
|
||||
overriddenFields: circuitDeviceRows.overriddenFields,
|
||||
})
|
||||
.from(circuitDeviceRows)
|
||||
.innerJoin(circuits, eq(circuits.id, circuitDeviceRows.circuitId))
|
||||
.innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId))
|
||||
.where(eq(circuitLists.projectId, projectId))
|
||||
.orderBy(
|
||||
asc(circuitDeviceRows.circuitId),
|
||||
asc(circuitDeviceRows.sortOrder),
|
||||
asc(circuitDeviceRows.id)
|
||||
)
|
||||
.all();
|
||||
const projectDeviceRows = database
|
||||
.select()
|
||||
.from(projectDevices)
|
||||
.where(eq(projectDevices.projectId, projectId))
|
||||
.orderBy(asc(projectDevices.displayName), asc(projectDevices.id))
|
||||
.all();
|
||||
const floorRows = database
|
||||
.select()
|
||||
.from(floors)
|
||||
.where(eq(floors.projectId, projectId))
|
||||
.orderBy(asc(floors.sortOrder), asc(floors.id))
|
||||
.all();
|
||||
const roomRows = database
|
||||
.select()
|
||||
.from(rooms)
|
||||
.where(eq(rooms.projectId, projectId))
|
||||
.orderBy(asc(rooms.roomNumber), asc(rooms.id))
|
||||
.all();
|
||||
|
||||
const deviceRowsByCircuit = new Map<string, typeof deviceRowRows>();
|
||||
for (const row of deviceRowRows) {
|
||||
const entries = deviceRowsByCircuit.get(row.circuitId) ?? [];
|
||||
entries.push(row);
|
||||
deviceRowsByCircuit.set(row.circuitId, entries);
|
||||
}
|
||||
|
||||
const state = parseProjectStateSnapshot({
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
project: {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||
},
|
||||
distributionBoards: boardRows,
|
||||
circuitLists: listRows,
|
||||
circuitSections: sectionRows,
|
||||
circuits: circuitRows.map((circuit) => ({
|
||||
...circuit,
|
||||
isReserve: Boolean(circuit.isReserve),
|
||||
deviceRows: deviceRowsByCircuit.get(circuit.id) ?? [],
|
||||
})),
|
||||
projectDevices: projectDeviceRows,
|
||||
floors: floorRows,
|
||||
rooms: roomRows,
|
||||
});
|
||||
const payloadJson = serializeProjectStateSnapshot(state);
|
||||
return {
|
||||
currentRevision: project.currentRevision,
|
||||
state,
|
||||
payloadJson,
|
||||
payloadSha256: hashProjectStatePayload(payloadJson),
|
||||
};
|
||||
}
|
||||
|
||||
export function hashProjectStatePayload(payloadJson: string) {
|
||||
return crypto.createHash("sha256").update(payloadJson).digest("hex");
|
||||
}
|
||||
Reference in New Issue
Block a user