433 lines
14 KiB
TypeScript
433 lines
14 KiB
TypeScript
import { and, asc, eq, inArray } from "drizzle-orm";
|
|
import {
|
|
assertDistributionBoardDeleteSubtreeProjectCommand,
|
|
assertDistributionBoardInsertSubtreeProjectCommand,
|
|
createDistributionBoardDeleteSubtreeProjectCommand,
|
|
createDistributionBoardInsertSubtreeProjectCommand,
|
|
distributionBoardDeleteSubtreeCommandType,
|
|
distributionBoardInsertSubtreeCommandType,
|
|
type DistributionBoardSubtreeProjectCommand,
|
|
} from "../../domain/models/distribution-board-subtree-project-command.model.js";
|
|
import {
|
|
assertDistributionBoardSubtreeSnapshot,
|
|
type DistributionBoardSubtreeSnapshot,
|
|
} from "../../domain/models/distribution-board-subtree-snapshot.model.js";
|
|
import type {
|
|
DistributionBoardSubtreeProjectCommandStore,
|
|
ExecuteDistributionBoardSubtreeCommandInput,
|
|
} from "../../domain/ports/distribution-board-subtree-project-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 { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
|
import { circuitSections } from "../schema/circuit-sections.js";
|
|
import { circuits } from "../schema/circuits.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 { projectDevices } from "../schema/project-devices.js";
|
|
import { projects } from "../schema/projects.js";
|
|
import { rooms } from "../schema/rooms.js";
|
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
|
|
|
export class DistributionBoardSubtreeProjectCommandRepository
|
|
implements DistributionBoardSubtreeProjectCommandStore
|
|
{
|
|
constructor(private readonly database: AppDatabase) {}
|
|
|
|
capture(projectId: string, distributionBoardId: string) {
|
|
return this.captureFrom(
|
|
this.database,
|
|
projectId,
|
|
distributionBoardId
|
|
);
|
|
}
|
|
|
|
execute(input: ExecuteDistributionBoardSubtreeCommandInput) {
|
|
if (input.command.type === distributionBoardInsertSubtreeCommandType) {
|
|
assertDistributionBoardInsertSubtreeProjectCommand(input.command);
|
|
} else {
|
|
assertDistributionBoardDeleteSubtreeProjectCommand(input.command);
|
|
}
|
|
return executeProjectCommandTransaction(
|
|
this.database,
|
|
input,
|
|
(tx) => this.applyCommand(tx, input.projectId, input.command)
|
|
);
|
|
}
|
|
|
|
private applyCommand(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
command: DistributionBoardSubtreeProjectCommand
|
|
) {
|
|
if (command.type === distributionBoardInsertSubtreeCommandType) {
|
|
this.insert(database, projectId, command.payload.snapshot);
|
|
return createDistributionBoardDeleteSubtreeProjectCommand(
|
|
command.payload.snapshot
|
|
);
|
|
}
|
|
if (command.type === distributionBoardDeleteSubtreeCommandType) {
|
|
const current = this.captureFrom(
|
|
database,
|
|
projectId,
|
|
command.payload.snapshot.distributionBoard.id
|
|
);
|
|
if (!sameSnapshot(current, command.payload.snapshot)) {
|
|
throw new Error(
|
|
"Distribution-board subtree changed before deletion."
|
|
);
|
|
}
|
|
const deleted = database
|
|
.delete(distributionBoards)
|
|
.where(
|
|
and(
|
|
eq(
|
|
distributionBoards.id,
|
|
current.distributionBoard.id
|
|
),
|
|
eq(distributionBoards.projectId, projectId)
|
|
)
|
|
)
|
|
.run();
|
|
if (deleted.changes !== 1) {
|
|
throw new Error(
|
|
"Distribution-board subtree could not be deleted."
|
|
);
|
|
}
|
|
return createDistributionBoardInsertSubtreeProjectCommand(
|
|
command.payload.snapshot
|
|
);
|
|
}
|
|
throw new Error("Unsupported distribution-board subtree command.");
|
|
}
|
|
|
|
private captureFrom(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
distributionBoardId: string
|
|
): DistributionBoardSubtreeSnapshot {
|
|
const distributionBoard = database
|
|
.select()
|
|
.from(distributionBoards)
|
|
.where(
|
|
and(
|
|
eq(distributionBoards.id, distributionBoardId),
|
|
eq(distributionBoards.projectId, projectId)
|
|
)
|
|
)
|
|
.get();
|
|
if (!distributionBoard) {
|
|
throw new Error("Distribution board not found.");
|
|
}
|
|
const circuitList = database
|
|
.select()
|
|
.from(circuitLists)
|
|
.where(
|
|
and(
|
|
eq(circuitLists.distributionBoardId, distributionBoard.id),
|
|
eq(circuitLists.projectId, projectId)
|
|
)
|
|
)
|
|
.get();
|
|
if (!circuitList) {
|
|
throw new Error("Distribution-board circuit list not found.");
|
|
}
|
|
const sections = database
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.circuitListId, circuitList.id))
|
|
.orderBy(asc(circuitSections.sortOrder), asc(circuitSections.id))
|
|
.all();
|
|
const components = database
|
|
.select()
|
|
.from(distributionBoardComponents)
|
|
.where(eq(distributionBoardComponents.circuitListId, circuitList.id))
|
|
.orderBy(
|
|
asc(distributionBoardComponents.sortOrder),
|
|
asc(distributionBoardComponents.id)
|
|
)
|
|
.all()
|
|
.map((component) => ({
|
|
component,
|
|
protectionDevice:
|
|
database
|
|
.select()
|
|
.from(distributionBoardComponentProtectionDevices)
|
|
.where(
|
|
eq(
|
|
distributionBoardComponentProtectionDevices.componentId,
|
|
component.id
|
|
)
|
|
)
|
|
.get() ?? null,
|
|
}));
|
|
const circuitSnapshots = database
|
|
.select()
|
|
.from(circuits)
|
|
.where(eq(circuits.circuitListId, circuitList.id))
|
|
.orderBy(asc(circuits.sortOrder), asc(circuits.id))
|
|
.all()
|
|
.map((circuit) => {
|
|
const { isReserve, ...values } = circuit;
|
|
return {
|
|
...values,
|
|
isReserve: Boolean(isReserve),
|
|
deviceRows: database
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.circuitId, circuit.id))
|
|
.orderBy(
|
|
asc(circuitDeviceRows.sortOrder),
|
|
asc(circuitDeviceRows.id)
|
|
)
|
|
.all(),
|
|
protectionDevice:
|
|
database
|
|
.select()
|
|
.from(circuitProtectionDevices)
|
|
.where(eq(circuitProtectionDevices.circuitId, circuit.id))
|
|
.get() ?? null,
|
|
};
|
|
});
|
|
const snapshot: DistributionBoardSubtreeSnapshot = {
|
|
distributionBoard,
|
|
circuitList,
|
|
sections,
|
|
components,
|
|
circuits: circuitSnapshots,
|
|
};
|
|
assertDistributionBoardSubtreeSnapshot(snapshot);
|
|
return snapshot;
|
|
}
|
|
|
|
private insert(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
snapshot: DistributionBoardSubtreeSnapshot
|
|
) {
|
|
assertDistributionBoardSubtreeSnapshot(snapshot);
|
|
if (
|
|
snapshot.distributionBoard.projectId !== projectId ||
|
|
snapshot.circuitList.projectId !== projectId
|
|
) {
|
|
throw new Error(
|
|
"Distribution-board subtree belongs to another project."
|
|
);
|
|
}
|
|
this.assertProjectReferences(database, projectId, snapshot);
|
|
this.assertIdsAvailable(database, snapshot);
|
|
|
|
database
|
|
.insert(distributionBoards)
|
|
.values(snapshot.distributionBoard)
|
|
.run();
|
|
database.insert(circuitLists).values(snapshot.circuitList).run();
|
|
if (snapshot.sections.length > 0) {
|
|
database.insert(circuitSections).values(snapshot.sections).run();
|
|
}
|
|
for (const entry of snapshot.components) {
|
|
database
|
|
.insert(distributionBoardComponents)
|
|
.values(entry.component)
|
|
.run();
|
|
if (entry.protectionDevice !== null) {
|
|
database
|
|
.insert(distributionBoardComponentProtectionDevices)
|
|
.values(entry.protectionDevice)
|
|
.run();
|
|
}
|
|
}
|
|
for (const circuit of snapshot.circuits) {
|
|
const { deviceRows, protectionDevice, isReserve, ...values } =
|
|
circuit;
|
|
database
|
|
.insert(circuits)
|
|
.values({ ...values, isReserve: isReserve ? 1 : 0 })
|
|
.run();
|
|
if (protectionDevice !== null && protectionDevice !== undefined) {
|
|
database
|
|
.insert(circuitProtectionDevices)
|
|
.values(protectionDevice)
|
|
.run();
|
|
}
|
|
if (deviceRows.length > 0) {
|
|
database.insert(circuitDeviceRows).values(deviceRows).run();
|
|
}
|
|
}
|
|
}
|
|
|
|
private assertProjectReferences(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
snapshot: DistributionBoardSubtreeSnapshot
|
|
) {
|
|
const project = database
|
|
.select({
|
|
enabledSupplyTypes:
|
|
projects.enabledDistributionBoardSupplyTypes,
|
|
})
|
|
.from(projects)
|
|
.where(eq(projects.id, projectId))
|
|
.get();
|
|
if (!project) {
|
|
throw new Error("Project not found.");
|
|
}
|
|
if (
|
|
snapshot.distributionBoard.supplyType !== null &&
|
|
!project.enabledSupplyTypes.includes(
|
|
snapshot.distributionBoard.supplyType
|
|
)
|
|
) {
|
|
throw new Error(
|
|
"Distribution-board subtree uses a disabled supply type."
|
|
);
|
|
}
|
|
if (snapshot.distributionBoard.floorId !== null) {
|
|
const floor = database
|
|
.select({ projectId: floors.projectId })
|
|
.from(floors)
|
|
.where(eq(floors.id, snapshot.distributionBoard.floorId))
|
|
.get();
|
|
if (!floor || floor.projectId !== projectId) {
|
|
throw new Error(
|
|
"Distribution-board floor does not belong to project."
|
|
);
|
|
}
|
|
}
|
|
const linkedProjectDeviceIds = uniqueStrings(
|
|
snapshot.circuits.flatMap((circuit) =>
|
|
circuit.deviceRows.flatMap((row) =>
|
|
row.linkedProjectDeviceId === null
|
|
? []
|
|
: [row.linkedProjectDeviceId]
|
|
)
|
|
)
|
|
);
|
|
if (linkedProjectDeviceIds.length > 0) {
|
|
const references = database
|
|
.select({ id: projectDevices.id, projectId: projectDevices.projectId })
|
|
.from(projectDevices)
|
|
.where(inArray(projectDevices.id, linkedProjectDeviceIds))
|
|
.all();
|
|
if (
|
|
references.length !== linkedProjectDeviceIds.length ||
|
|
references.some((reference) => reference.projectId !== projectId)
|
|
) {
|
|
throw new Error(
|
|
"Distribution-board project-device reference is invalid."
|
|
);
|
|
}
|
|
}
|
|
const roomIds = uniqueStrings(
|
|
snapshot.circuits.flatMap((circuit) =>
|
|
circuit.deviceRows.flatMap((row) =>
|
|
row.roomId === null ? [] : [row.roomId]
|
|
)
|
|
)
|
|
);
|
|
if (roomIds.length > 0) {
|
|
const references = database
|
|
.select({ id: rooms.id, projectId: rooms.projectId })
|
|
.from(rooms)
|
|
.where(inArray(rooms.id, roomIds))
|
|
.all();
|
|
if (
|
|
references.length !== roomIds.length ||
|
|
references.some((reference) => reference.projectId !== projectId)
|
|
) {
|
|
throw new Error("Distribution-board room reference is invalid.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private assertIdsAvailable(
|
|
database: AppDatabase,
|
|
snapshot: DistributionBoardSubtreeSnapshot
|
|
) {
|
|
const sectionIds = snapshot.sections.map((section) => section.id);
|
|
const componentIds = snapshot.components.map(
|
|
({ component }) => component.id
|
|
);
|
|
const circuitIds = snapshot.circuits.map((circuit) => circuit.id);
|
|
const deviceRowIds = snapshot.circuits.flatMap((circuit) =>
|
|
circuit.deviceRows.map((row) => row.id)
|
|
);
|
|
const conflicts = [
|
|
database
|
|
.select({ id: distributionBoards.id })
|
|
.from(distributionBoards)
|
|
.where(eq(distributionBoards.id, snapshot.distributionBoard.id))
|
|
.get(),
|
|
database
|
|
.select({ id: circuitLists.id })
|
|
.from(circuitLists)
|
|
.where(eq(circuitLists.id, snapshot.circuitList.id))
|
|
.get(),
|
|
sectionIds.length === 0
|
|
? undefined
|
|
: database
|
|
.select({ id: circuitSections.id })
|
|
.from(circuitSections)
|
|
.where(inArray(circuitSections.id, sectionIds))
|
|
.limit(1)
|
|
.get(),
|
|
componentIds.length === 0
|
|
? undefined
|
|
: database
|
|
.select({ id: distributionBoardComponents.id })
|
|
.from(distributionBoardComponents)
|
|
.where(inArray(distributionBoardComponents.id, componentIds))
|
|
.limit(1)
|
|
.get(),
|
|
circuitIds.length === 0
|
|
? undefined
|
|
: database
|
|
.select({ id: circuits.id })
|
|
.from(circuits)
|
|
.where(inArray(circuits.id, circuitIds))
|
|
.limit(1)
|
|
.get(),
|
|
deviceRowIds.length === 0
|
|
? undefined
|
|
: database
|
|
.select({ id: circuitDeviceRows.id })
|
|
.from(circuitDeviceRows)
|
|
.where(inArray(circuitDeviceRows.id, deviceRowIds))
|
|
.limit(1)
|
|
.get(),
|
|
];
|
|
if (conflicts.some(Boolean)) {
|
|
throw new Error("Distribution-board subtree id already exists.");
|
|
}
|
|
}
|
|
}
|
|
|
|
function uniqueStrings(values: string[]) {
|
|
return [...new Set(values)];
|
|
}
|
|
|
|
function sameSnapshot(
|
|
left: DistributionBoardSubtreeSnapshot,
|
|
right: DistributionBoardSubtreeSnapshot
|
|
) {
|
|
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") {
|
|
return `{${Object.entries(value)
|
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
.map(
|
|
([key, child]) =>
|
|
`${JSON.stringify(key)}:${canonicalJson(child)}`
|
|
)
|
|
.join(",")}}`;
|
|
}
|
|
return JSON.stringify(value) ?? "null";
|
|
}
|