forked from jappel/leistungsbilanz-ts
A circuit with exactly one device row and no fed distribution board is shown as that row, so its own name was hidden and could drift (e.g. "Neuer Stromkreis" in the print view). Commands that create or leave such a circuit, rename its row or unlink its downstream board now set the circuit name to the row name and store the change as optional circuitDisplayNames, so Undo restores an overwritten free name exactly. Migration 0016 aligns existing data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
6.9 KiB
TypeScript
226 lines
6.9 KiB
TypeScript
import { and, eq } from "drizzle-orm";
|
|
import {
|
|
assertCircuitDeviceRowUpdateProjectCommand,
|
|
createCircuitDeviceRowUpdateProjectCommand,
|
|
type CircuitDeviceRowUpdateField,
|
|
type CircuitDeviceRowUpdatePatch,
|
|
type CircuitDeviceRowUpdateValues,
|
|
} from "../../domain/models/circuit-device-row-project-command.model.js";
|
|
import {
|
|
assertCircuitDisplayNameTransitions,
|
|
invertCircuitDisplayNameTransitions,
|
|
} from "../../domain/models/circuit-display-name-sync.model.js";
|
|
import type {
|
|
CircuitDeviceRowProjectCommandStore,
|
|
ExecuteCircuitDeviceRowUpdateCommandInput,
|
|
} from "../../domain/ports/circuit-device-row-project-command.store.js";
|
|
import { deriveOverriddenFieldsForLocalEdit } from "../../domain/services/project-device-overrides.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 { circuits } from "../schema/circuits.js";
|
|
import { projectDevices } from "../schema/project-devices.js";
|
|
import { rooms } from "../schema/rooms.js";
|
|
import {
|
|
toCircuitDeviceRowPatchValues,
|
|
type CircuitDeviceRowPatchInput,
|
|
} from "./circuit-device-row.persistence.js";
|
|
import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js";
|
|
import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js";
|
|
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
|
|
|
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
|
|
|
export class CircuitDeviceRowProjectCommandRepository
|
|
implements CircuitDeviceRowProjectCommandStore
|
|
{
|
|
constructor(private readonly database: AppDatabase) {}
|
|
|
|
executeUpdate(input: ExecuteCircuitDeviceRowUpdateCommandInput) {
|
|
assertCircuitDeviceRowUpdateProjectCommand(input.command);
|
|
assertCircuitDisplayNameTransitions(
|
|
input.command.payload.circuitDisplayNames
|
|
);
|
|
|
|
return executeProjectCommandTransactionWithAppliedForward(
|
|
this.database,
|
|
input,
|
|
(tx) => this.applyCommand(tx, input)
|
|
);
|
|
}
|
|
|
|
private applyCommand(
|
|
tx: AppDatabase,
|
|
input: ExecuteCircuitDeviceRowUpdateCommandInput
|
|
) {
|
|
const current = tx
|
|
.select()
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.id, input.command.payload.rowId))
|
|
.get();
|
|
if (!current) {
|
|
throw new Error("Invalid device row id.");
|
|
}
|
|
|
|
const circuit = tx
|
|
.select({ circuitListId: circuits.circuitListId })
|
|
.from(circuits)
|
|
.where(eq(circuits.id, current.circuitId))
|
|
.get();
|
|
const owningList = circuit
|
|
? tx
|
|
.select({ id: circuitLists.id })
|
|
.from(circuitLists)
|
|
.where(
|
|
and(
|
|
eq(circuitLists.id, circuit.circuitListId),
|
|
eq(circuitLists.projectId, input.projectId)
|
|
)
|
|
)
|
|
.get()
|
|
: null;
|
|
if (!owningList) {
|
|
throw new Error("Circuit device row does not belong to project.");
|
|
}
|
|
|
|
const patch = Object.fromEntries(
|
|
input.command.payload.changes.map((change) => [
|
|
change.field,
|
|
change.value,
|
|
])
|
|
) as CircuitDeviceRowPatchInput;
|
|
const targetQuantity = patch.quantity ?? current.quantity;
|
|
const targetManualQuantity =
|
|
patch.manualQuantity ?? current.manualQuantity;
|
|
if (targetManualQuantity > targetQuantity) {
|
|
throw new Error(
|
|
"Device-row manual quantity must not exceed total quantity."
|
|
);
|
|
}
|
|
if (
|
|
input.source === "user" &&
|
|
patch.phaseType !== undefined &&
|
|
!isElectricalPhaseType(patch.phaseType)
|
|
) {
|
|
throw new Error("Device-row phase type is invalid.");
|
|
}
|
|
this.assertLinkedProjectDevice(tx, input.projectId, patch);
|
|
this.assertRoom(tx, input.projectId, patch);
|
|
|
|
const overriddenFields = deriveOverriddenFieldsForLocalEdit(
|
|
current,
|
|
patch
|
|
);
|
|
if (
|
|
overriddenFields !== undefined &&
|
|
patch.overriddenFields === undefined
|
|
) {
|
|
patch.overriddenFields = overriddenFields;
|
|
}
|
|
|
|
const appliedPatch = createCircuitDeviceRowUpdateProjectCommand(
|
|
current.id,
|
|
patch as CircuitDeviceRowUpdatePatch
|
|
);
|
|
const inversePatch = Object.fromEntries(
|
|
appliedPatch.payload.changes.map((change) => [
|
|
change.field,
|
|
getCircuitDeviceRowFieldValue(current, change.field),
|
|
])
|
|
) as CircuitDeviceRowUpdatePatch;
|
|
|
|
const update = tx
|
|
.update(circuitDeviceRows)
|
|
.set(toCircuitDeviceRowPatchValues(patch))
|
|
.where(eq(circuitDeviceRows.id, current.id))
|
|
.run();
|
|
if (update.changes !== 1) {
|
|
throw new Error("Circuit device row changed before command execution.");
|
|
}
|
|
if (patch.phaseType !== undefined) {
|
|
updateDerivedCircuitVoltage(
|
|
tx,
|
|
input.projectId,
|
|
current.circuitId
|
|
);
|
|
}
|
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
|
tx,
|
|
input.source,
|
|
patch.displayName === undefined ? [] : [current.circuitId],
|
|
input.command.payload.circuitDisplayNames
|
|
);
|
|
|
|
return {
|
|
forward: createCircuitDeviceRowUpdateProjectCommand(
|
|
current.id,
|
|
patch as CircuitDeviceRowUpdatePatch,
|
|
circuitDisplayNames
|
|
),
|
|
inverse: createCircuitDeviceRowUpdateProjectCommand(
|
|
current.id,
|
|
inversePatch,
|
|
invertCircuitDisplayNameTransitions(circuitDisplayNames)
|
|
),
|
|
};
|
|
}
|
|
|
|
private assertLinkedProjectDevice(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
patch: CircuitDeviceRowPatchInput
|
|
) {
|
|
if (
|
|
!Object.prototype.hasOwnProperty.call(patch, "linkedProjectDeviceId") ||
|
|
patch.linkedProjectDeviceId === null ||
|
|
patch.linkedProjectDeviceId === undefined
|
|
) {
|
|
return;
|
|
}
|
|
const device = database
|
|
.select({ id: projectDevices.id })
|
|
.from(projectDevices)
|
|
.where(
|
|
and(
|
|
eq(projectDevices.id, patch.linkedProjectDeviceId),
|
|
eq(projectDevices.projectId, projectId)
|
|
)
|
|
)
|
|
.get();
|
|
if (!device) {
|
|
throw new Error("Invalid linked project device id.");
|
|
}
|
|
}
|
|
|
|
private assertRoom(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
patch: CircuitDeviceRowPatchInput
|
|
) {
|
|
if (
|
|
!Object.prototype.hasOwnProperty.call(patch, "roomId") ||
|
|
patch.roomId === null ||
|
|
patch.roomId === undefined
|
|
) {
|
|
return;
|
|
}
|
|
const room = database
|
|
.select({ id: rooms.id })
|
|
.from(rooms)
|
|
.where(and(eq(rooms.id, patch.roomId), eq(rooms.projectId, projectId)))
|
|
.get();
|
|
if (!room) {
|
|
throw new Error("Invalid room id.");
|
|
}
|
|
}
|
|
}
|
|
|
|
function getCircuitDeviceRowFieldValue<
|
|
TField extends CircuitDeviceRowUpdateField,
|
|
>(
|
|
row: CircuitDeviceRow,
|
|
field: TField
|
|
): CircuitDeviceRowUpdateValues[TField] {
|
|
return row[field] as unknown as CircuitDeviceRowUpdateValues[TField];
|
|
}
|