240 lines
9.6 KiB
TypeScript
240 lines
9.6 KiB
TypeScript
import { and, asc, eq } from "drizzle-orm";
|
|
import { assertCircuitDeviceRowQuantity } from "../../domain/calculations/circuit-device-row-quantity.js";
|
|
import {
|
|
assertExternalObjectNewCircuitProjectCommand,
|
|
externalObjectAssignToNewCircuitCommandType,
|
|
externalObjectDeleteCreatedCircuitCommandType,
|
|
invertExternalObjectNewCircuitProjectCommand,
|
|
type ExternalObjectNewCircuitProjectCommand,
|
|
} from "../../domain/models/external-object-new-circuit-project-command.model.js";
|
|
import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js";
|
|
import type { CircuitSnapshot } from "../../domain/models/circuit-structure-project-command.model.js";
|
|
import type { ExternalObjectNewCircuitProjectCommandStore } from "../../domain/ports/external-object-new-circuit-project-command.store.js";
|
|
import { parseGroupedEquipmentIdentifier } from "../../domain/services/circuit-group-numbering.js";
|
|
import { isElectricalPhaseType } from "../../domain/services/project-voltage.service.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 { externalModelObjects } from "../schema/external-model-objects.js";
|
|
import {
|
|
assertCircuitDeviceRowReferencesInProject,
|
|
toCircuitDeviceRowInsertValues,
|
|
toCircuitDeviceRowSnapshot,
|
|
} from "./circuit-device-row-structure.persistence.js";
|
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
|
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
|
|
import {
|
|
applyExternalObjectLinks,
|
|
assertExternalObjectsCompatibleWithRow,
|
|
loadExpectedExternalObjectTransitions,
|
|
snapshotsEqual,
|
|
} from "./external-object-assignment.persistence.js";
|
|
|
|
export class ExternalObjectNewCircuitProjectCommandRepository
|
|
implements ExternalObjectNewCircuitProjectCommandStore
|
|
{
|
|
constructor(private readonly database: AppDatabase) {}
|
|
|
|
execute(input: Parameters<ExternalObjectNewCircuitProjectCommandStore["execute"]>[0]) {
|
|
assertExternalObjectNewCircuitProjectCommand(input.command);
|
|
if (
|
|
input.source === "user" &&
|
|
input.command.type === externalObjectDeleteCreatedCircuitCommandType
|
|
) {
|
|
throw new Error("Created external circuits may only be removed through project history.");
|
|
}
|
|
return executeProjectCommandTransaction(this.database, input, (tx) => {
|
|
if (input.command.type === externalObjectAssignToNewCircuitCommandType) {
|
|
this.insert(tx, input.projectId, input.command);
|
|
} else {
|
|
this.remove(tx, input.projectId, input.command);
|
|
}
|
|
return invertExternalObjectNewCircuitProjectCommand(input.command);
|
|
});
|
|
}
|
|
|
|
private insert(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
command: ExternalObjectNewCircuitProjectCommand
|
|
) {
|
|
const circuit = command.payload.circuit;
|
|
const row = circuit.deviceRows[0]!;
|
|
const context = this.loadSectionContext(
|
|
database,
|
|
projectId,
|
|
circuit.circuitListId,
|
|
circuit.sectionId
|
|
);
|
|
const parsedIdentifier = parseGroupedEquipmentIdentifier(circuit.equipmentIdentifier);
|
|
if (
|
|
!parsedIdentifier ||
|
|
parsedIdentifier.kind !== "circuit" ||
|
|
parsedIdentifier.category !== context.category ||
|
|
parsedIdentifier.groupNumber !== context.groupNumber
|
|
) {
|
|
throw new Error("External circuit equipment identifier does not match target group.");
|
|
}
|
|
if (row.category !== context.category || !isElectricalPhaseType(row.phaseType)) {
|
|
throw new Error("External device row does not match target circuit group.");
|
|
}
|
|
assertCircuitDeviceRowReferencesInProject(database, projectId, row);
|
|
const voltage = resolveCircuitVoltage(database, projectId, circuit.sectionId, [row.phaseType]);
|
|
if (circuit.voltage !== voltage) {
|
|
throw new Error("External circuit voltage must match the project phase voltage.");
|
|
}
|
|
if (database.select({ id: circuits.id }).from(circuits)
|
|
.where(eq(circuits.id, circuit.id)).get()) {
|
|
throw new Error("External circuit id already exists.");
|
|
}
|
|
if (database.select({ id: circuits.id }).from(circuits).where(and(
|
|
eq(circuits.circuitListId, circuit.circuitListId),
|
|
eq(circuits.equipmentIdentifier, circuit.equipmentIdentifier)
|
|
)).get()) {
|
|
throw new Error("Duplicate equipmentIdentifier in circuit list.");
|
|
}
|
|
if (database.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, row.id)).get()) {
|
|
throw new Error("External device-row id already exists.");
|
|
}
|
|
|
|
const objects = this.loadExpectedObjects(database, projectId, command);
|
|
assertExternalObjectsCompatibleWithRow({
|
|
database,
|
|
projectId,
|
|
row,
|
|
distributionBoardId: context.distributionBoardId,
|
|
category: context.category,
|
|
assignedObjects: objects.map(({ target }) => target),
|
|
allLinkedObjects: objects.map(({ target }) => target),
|
|
confirmedConflictObjectIds: new Set(command.payload.confirmedConflictObjectIds),
|
|
});
|
|
assertCircuitDeviceRowQuantity({
|
|
quantity: row.quantity,
|
|
manualQuantity: 0,
|
|
externalObjects: objects.map(({ target }) => ({
|
|
effectiveQuantity: target.planningValues.effectiveQuantity,
|
|
})),
|
|
});
|
|
|
|
database.insert(circuits).values(toCircuitValues(circuit)).run();
|
|
database.insert(circuitDeviceRows).values(toCircuitDeviceRowInsertValues(row)).run();
|
|
database.insert(circuitProtectionDevices).values(circuit.protectionDevice!).run();
|
|
applyExternalObjectLinks(
|
|
database,
|
|
objects,
|
|
"External object changed during new-circuit assignment."
|
|
);
|
|
}
|
|
|
|
private remove(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
command: ExternalObjectNewCircuitProjectCommand
|
|
) {
|
|
const expectedCircuit = command.payload.circuit;
|
|
this.loadSectionContext(
|
|
database,
|
|
projectId,
|
|
expectedCircuit.circuitListId,
|
|
expectedCircuit.sectionId
|
|
);
|
|
const current = this.loadCircuitSnapshot(database, expectedCircuit.id);
|
|
if (!current || !snapshotsEqual(current, expectedCircuit)) {
|
|
throw new Error("Created external circuit changed before history removal.");
|
|
}
|
|
const objects = this.loadExpectedObjects(database, projectId, command);
|
|
const rowId = expectedCircuit.deviceRows[0]!.id;
|
|
const linkedObjects = database.select({ id: externalModelObjects.id })
|
|
.from(externalModelObjects)
|
|
.where(eq(externalModelObjects.circuitDeviceRowId, rowId)).all();
|
|
if (
|
|
linkedObjects.length !== objects.length ||
|
|
linkedObjects.some(({ id }) => !objects.some(({ expected }) => expected.id === id))
|
|
) {
|
|
throw new Error("Created external circuit has different object links.");
|
|
}
|
|
applyExternalObjectLinks(
|
|
database,
|
|
objects,
|
|
"External object changed during new-circuit assignment."
|
|
);
|
|
const deleted = database.delete(circuits).where(and(
|
|
eq(circuits.id, expectedCircuit.id),
|
|
eq(circuits.circuitListId, expectedCircuit.circuitListId)
|
|
)).run();
|
|
if (deleted.changes !== 1) {
|
|
throw new Error("Created external circuit changed during history removal.");
|
|
}
|
|
}
|
|
|
|
private loadSectionContext(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
circuitListId: string,
|
|
sectionId: string
|
|
) {
|
|
const context = database.select({
|
|
projectId: circuitLists.projectId,
|
|
distributionBoardId: circuitLists.distributionBoardId,
|
|
category: circuitSections.category,
|
|
groupNumber: circuitSections.groupNumber,
|
|
}).from(circuitSections)
|
|
.innerJoin(circuitLists, eq(circuitLists.id, circuitSections.circuitListId))
|
|
.where(and(
|
|
eq(circuitSections.id, sectionId),
|
|
eq(circuitSections.circuitListId, circuitListId)
|
|
)).get();
|
|
if (
|
|
!context ||
|
|
context.projectId !== projectId ||
|
|
context.category === null ||
|
|
context.groupNumber === null
|
|
) {
|
|
throw new Error("Target circuit group does not belong to the project.");
|
|
}
|
|
return {
|
|
distributionBoardId: context.distributionBoardId,
|
|
category: context.category,
|
|
groupNumber: context.groupNumber,
|
|
};
|
|
}
|
|
|
|
private loadExpectedObjects(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
command: ExternalObjectNewCircuitProjectCommand
|
|
) {
|
|
return loadExpectedExternalObjectTransitions(
|
|
database,
|
|
projectId,
|
|
command.payload.objects,
|
|
"External object changed before new-circuit assignment."
|
|
);
|
|
}
|
|
|
|
private loadCircuitSnapshot(database: AppDatabase, circuitId: string): CircuitSnapshot | null {
|
|
const circuit = database.select().from(circuits).where(eq(circuits.id, circuitId)).get();
|
|
if (!circuit) return null;
|
|
const rows = database.select().from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.circuitId, circuitId))
|
|
.orderBy(asc(circuitDeviceRows.sortOrder), asc(circuitDeviceRows.id)).all();
|
|
const protection = database.select().from(circuitProtectionDevices)
|
|
.where(eq(circuitProtectionDevices.circuitId, circuitId)).get();
|
|
return {
|
|
...circuit,
|
|
isReserve: Boolean(circuit.isReserve),
|
|
deviceRows: rows.map(toCircuitDeviceRowSnapshot),
|
|
protectionDevice: protection ?? null,
|
|
};
|
|
}
|
|
}
|
|
|
|
function toCircuitValues(circuit: CircuitSnapshot) {
|
|
const { deviceRows: _deviceRows, protectionDevice: _protectionDevice, ...values } = circuit;
|
|
return { ...values, isReserve: circuit.isReserve ? 1 : 0 };
|
|
}
|