Add cell copy/paste, multi-row edits and a context menu to the circuit grid

Ctrl+C/Ctrl+V copy and paste grid cells without opening an editor, using
the document copy/paste events so it also works over plain HTTP in the
LAN. Protection and cable summaries copy the complete circuit record;
tab-separated blocks from Excel spread over visible rows and columns.
With a Ctrl/Shift multi-selection, typing, pasting and the protection
dialog apply to every selected row that can take the value, including
device power and GZF; other cells are skipped and reported.

All of these writes use the new circuit-list.bulk-update command. It
carries complete circuit.update, circuit-device-row.update and
circuit-protection.update commands, applies them with the single-command
rules in one transaction and stores the reversed inverses, so each edit
is one undo step. The single-command apply logic is extracted for reuse.

A right-click menu (also context-menu key / Shift+F10) offers copy,
paste, edit, protection, cable sizing, insert circuit below, add device
and delete. The protection summary is now selected by click and opened
by double click, Enter or F2. Existing numbers open in the inline editor
with a decimal comma, so confirming an unchanged decimal no longer fails.

Refs #26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Julian Appel 2026-10-30 07:38:28 +01:00
parent 07bc52a134
commit 17a3945813
18 changed files with 2548 additions and 391 deletions

View file

@ -37,6 +37,11 @@ import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
export type CircuitDeviceRowUpdateApplyInput = Pick<
ExecuteCircuitDeviceRowUpdateCommandInput,
"projectId" | "source" | "command"
>;
export class CircuitDeviceRowProjectCommandRepository
implements CircuitDeviceRowProjectCommandStore
{
@ -51,208 +56,216 @@ export class CircuitDeviceRowProjectCommandRepository
return executeProjectCommandTransactionWithAppliedForward(
this.database,
input,
(tx) => this.applyCommand(tx, input)
(tx) => applyCircuitDeviceRowUpdateCommand(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.");
}
// Applies one circuit-device-row.update inside an open transaction and
// returns the applied forward command plus its exact inverse, including
// single-device circuit name transitions. Shared with
// circuit-list.bulk-update.
export function applyCircuitDeviceRowUpdateCommand(
tx: AppDatabase,
input: CircuitDeviceRowUpdateApplyInput
) {
assertCircuitDeviceRowUpdateProjectCommand(input.command);
assertCircuitDisplayNameTransitions(
input.command.payload.circuitDisplayNames
);
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)
)
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;
if (patch.quantity !== undefined || patch.manualQuantity !== undefined) {
this.applyQuantityPatch(tx, current, patch);
}
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)
),
};
}
// The row quantity is always the manual share plus every linked Revit
// object. A manual-share edit derives the total; a total edit is only
// valid while no Revit object contributes to the row.
private applyQuantityPatch(
database: AppDatabase,
current: CircuitDeviceRow,
patch: CircuitDeviceRowPatchInput
) {
const externalObjects = database
.select({ planningValues: externalModelObjects.planningValues })
.from(externalModelObjects)
.where(eq(externalModelObjects.circuitDeviceRowId, current.id))
.all()
.map((object) => ({
effectiveQuantity: object.planningValues.effectiveQuantity,
}));
const manualQuantity = patch.manualQuantity ?? current.manualQuantity;
if (patch.quantity === undefined) {
patch.quantity = calculateCircuitDeviceRowQuantity(
manualQuantity,
externalObjects
);
return;
}
if (
externalObjects.length > 0 &&
patch.quantity !==
calculateCircuitDeviceRowQuantity(manualQuantity, externalObjects)
) {
throw new Error(
"Die Anzahl dieser Gerätezeile ist mit Revit-Objekten verknüpft. Bitte die manuelle Anzahl bearbeiten."
);
}
assertCircuitDeviceRowQuantity({
quantity: patch.quantity,
manualQuantity,
externalObjects,
});
}
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.");
}
.get()
: null;
if (!owningList) {
throw new Error("Circuit device row does not belong to project.");
}
private assertRoom(
database: AppDatabase,
projectId: string,
patch: CircuitDeviceRowPatchInput
const patch = Object.fromEntries(
input.command.payload.changes.map((change) => [
change.field,
change.value,
])
) as CircuitDeviceRowPatchInput;
if (patch.quantity !== undefined || patch.manualQuantity !== undefined) {
applyQuantityPatch(tx, current, patch);
}
if (
input.source === "user" &&
patch.phaseType !== undefined &&
!isElectricalPhaseType(patch.phaseType)
) {
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.");
}
throw new Error("Device-row phase type is invalid.");
}
assertLinkedProjectDevice(tx, input.projectId, patch);
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)
),
};
}
// The row quantity is always the manual share plus every linked Revit
// object. A manual-share edit derives the total; a total edit is only
// valid while no Revit object contributes to the row.
function applyQuantityPatch(
database: AppDatabase,
current: CircuitDeviceRow,
patch: CircuitDeviceRowPatchInput
) {
const externalObjects = database
.select({ planningValues: externalModelObjects.planningValues })
.from(externalModelObjects)
.where(eq(externalModelObjects.circuitDeviceRowId, current.id))
.all()
.map((object) => ({
effectiveQuantity: object.planningValues.effectiveQuantity,
}));
const manualQuantity = patch.manualQuantity ?? current.manualQuantity;
if (patch.quantity === undefined) {
patch.quantity = calculateCircuitDeviceRowQuantity(
manualQuantity,
externalObjects
);
return;
}
if (
externalObjects.length > 0 &&
patch.quantity !==
calculateCircuitDeviceRowQuantity(manualQuantity, externalObjects)
) {
throw new Error(
"Die Anzahl dieser Gerätezeile ist mit Revit-Objekten verknüpft. Bitte die manuelle Anzahl bearbeiten."
);
}
assertCircuitDeviceRowQuantity({
quantity: patch.quantity,
manualQuantity,
externalObjects,
});
}
function 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.");
}
}
function 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.");
}
}

View file

@ -0,0 +1,69 @@
import { circuitDeviceRowUpdateCommandType } from "../../domain/models/circuit-device-row-project-command.model.js";
import {
assertCircuitListBulkUpdateProjectCommand,
createCircuitListBulkUpdateProjectCommand,
type CircuitListBulkUpdateEntry,
} from "../../domain/models/circuit-list-bulk-update-project-command.model.js";
import { circuitUpdateCommandType } from "../../domain/models/circuit-project-command.model.js";
import type {
CircuitListBulkUpdateProjectCommandStore,
ExecuteCircuitListBulkUpdateCommandInput,
} from "../../domain/ports/circuit-list-bulk-update-project-command.store.js";
import type { AppDatabase } from "../database-context.js";
import { applyCircuitDeviceRowUpdateCommand } from "./circuit-device-row-project-command.repository.js";
import { applyCircuitUpdateCommand } from "./circuit-project-command.repository.js";
import { applyCircuitProtectionUpdateCommand } from "./circuit-protection-project-command.repository.js";
import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js";
export class CircuitListBulkUpdateProjectCommandRepository
implements CircuitListBulkUpdateProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
// Applies every update with the same rules as its single command inside
// one transaction. Any rejected update rolls the whole edit back.
execute(input: ExecuteCircuitListBulkUpdateCommandInput) {
assertCircuitListBulkUpdateProjectCommand(input.command);
return executeProjectCommandTransactionWithAppliedForward(
this.database,
input,
(tx) => {
const forward: CircuitListBulkUpdateEntry[] = [];
const inverse: CircuitListBulkUpdateEntry[] = [];
for (const update of input.command.payload.updates) {
const applied = applyBulkUpdateEntry(tx, input, update);
forward.push(applied.forward);
inverse.unshift(applied.inverse);
}
return {
forward: createCircuitListBulkUpdateProjectCommand(forward),
inverse: createCircuitListBulkUpdateProjectCommand(inverse),
};
}
);
}
}
function applyBulkUpdateEntry(
tx: AppDatabase,
input: ExecuteCircuitListBulkUpdateCommandInput,
update: CircuitListBulkUpdateEntry
): { forward: CircuitListBulkUpdateEntry; inverse: CircuitListBulkUpdateEntry } {
const context = { projectId: input.projectId, source: input.source };
if (update.type === circuitUpdateCommandType) {
return applyCircuitUpdateCommand(tx, { ...context, command: update });
}
if (update.type === circuitDeviceRowUpdateCommandType) {
return applyCircuitDeviceRowUpdateCommand(tx, {
...context,
command: update,
});
}
return {
forward: update,
inverse: applyCircuitProtectionUpdateCommand(tx, {
...context,
command: update,
}),
};
}

View file

@ -25,6 +25,11 @@ import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqu
type CircuitRow = typeof circuits.$inferSelect;
export type CircuitUpdateApplyInput = Pick<
ExecuteCircuitUpdateCommandInput,
"projectId" | "source" | "command"
>;
export class CircuitProjectCommandRepository
implements CircuitProjectCommandStore
{
@ -36,144 +41,148 @@ export class CircuitProjectCommandRepository
return executeProjectCommandTransactionWithAppliedForward(
this.database,
input,
(tx) => this.applyCommand(tx, input)
(tx) => applyCircuitUpdateCommand(tx, input)
);
}
}
private applyCommand(
tx: AppDatabase,
input: ExecuteCircuitUpdateCommandInput
) {
const current = tx
.select()
.from(circuits)
.where(eq(circuits.id, input.command.payload.circuitId))
.get();
if (!current) {
throw new Error("Invalid circuit id.");
}
// Applies one circuit.update inside an open transaction and returns the
// applied forward command (with derived voltage) plus its exact inverse.
// Shared with circuit-list.bulk-update.
export function applyCircuitUpdateCommand(
tx: AppDatabase,
input: CircuitUpdateApplyInput
) {
assertCircuitUpdateProjectCommand(input.command);
const current = tx
.select()
.from(circuits)
.where(eq(circuits.id, input.command.payload.circuitId))
.get();
if (!current) {
throw new Error("Invalid circuit id.");
}
const owningList = tx
.select({ id: circuitLists.id })
.from(circuitLists)
.where(
and(
eq(circuitLists.id, current.circuitListId),
eq(circuitLists.projectId, input.projectId)
)
const owningList = tx
.select({ id: circuitLists.id })
.from(circuitLists)
.where(
and(
eq(circuitLists.id, current.circuitListId),
eq(circuitLists.projectId, input.projectId)
)
.get();
if (!owningList) {
throw new Error("Circuit does not belong to project.");
}
const patch = Object.fromEntries(
input.command.payload.changes.map((change) => [
change.field,
change.value,
])
) as CircuitPatchPersistenceInput;
this.assertSectionInCircuitList(tx, current, patch.sectionId);
if (input.source === "user") {
if (
input.command.payload.changes.every(
(change) => change.field === "voltage"
)
) {
throw new Error("Circuit voltage is derived and cannot be edited.");
}
const devicePhaseTypes = tx
.select({ phaseType: circuitDeviceRows.phaseType })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, current.id))
.all()
.map((row) => row.phaseType);
const derivedVoltage = resolveCircuitVoltage(
tx,
input.projectId,
patch.sectionId ?? current.sectionId,
devicePhaseTypes
);
if (derivedVoltage === current.voltage) {
delete patch.voltage;
} else {
patch.voltage = derivedVoltage;
}
}
this.assertUniqueEquipmentIdentifier(
tx,
current,
patch.equipmentIdentifier
);
const appliedForward = createCircuitUpdateProjectCommand(
current.id,
patch as CircuitUpdatePatch
);
const inversePatch = Object.fromEntries(
appliedForward.payload.changes.map((change) => [
change.field,
getCircuitFieldValue(current, change.field),
])
) as CircuitUpdatePatch;
const inverse = createCircuitUpdateProjectCommand(
current.id,
inversePatch
);
const update = tx
.update(circuits)
.set(toCircuitPatchValues(patch))
.where(eq(circuits.id, current.id))
.run();
if (update.changes !== 1) {
throw new Error("Circuit changed before command execution.");
}
return { forward: appliedForward, inverse };
)
.get();
if (!owningList) {
throw new Error("Circuit does not belong to project.");
}
private assertSectionInCircuitList(
database: AppDatabase,
circuit: CircuitRow,
sectionId: string | undefined
) {
if (sectionId === undefined || sectionId === circuit.sectionId) {
return;
}
const section = database
.select({ id: circuitSections.id })
.from(circuitSections)
.where(
and(
eq(circuitSections.id, sectionId),
eq(circuitSections.circuitListId, circuit.circuitListId)
)
)
.get();
if (!section) {
throw new Error("Section does not belong to circuit list.");
}
}
private assertUniqueEquipmentIdentifier(
database: AppDatabase,
circuit: CircuitRow,
equipmentIdentifier: string | undefined
) {
const patch = Object.fromEntries(
input.command.payload.changes.map((change) => [
change.field,
change.value,
])
) as CircuitPatchPersistenceInput;
assertSectionInCircuitList(tx, current, patch.sectionId);
if (input.source === "user") {
if (
equipmentIdentifier === undefined ||
equipmentIdentifier === circuit.equipmentIdentifier
input.command.payload.changes.every(
(change) => change.field === "voltage"
)
) {
return;
throw new Error("Circuit voltage is derived and cannot be edited.");
}
assertEquipmentIdentifierAvailable(
database,
circuit.circuitListId,
equipmentIdentifier,
circuit.id
const devicePhaseTypes = tx
.select({ phaseType: circuitDeviceRows.phaseType })
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, current.id))
.all()
.map((row) => row.phaseType);
const derivedVoltage = resolveCircuitVoltage(
tx,
input.projectId,
patch.sectionId ?? current.sectionId,
devicePhaseTypes
);
if (derivedVoltage === current.voltage) {
delete patch.voltage;
} else {
patch.voltage = derivedVoltage;
}
}
assertUniqueEquipmentIdentifier(
tx,
current,
patch.equipmentIdentifier
);
const appliedForward = createCircuitUpdateProjectCommand(
current.id,
patch as CircuitUpdatePatch
);
const inversePatch = Object.fromEntries(
appliedForward.payload.changes.map((change) => [
change.field,
getCircuitFieldValue(current, change.field),
])
) as CircuitUpdatePatch;
const inverse = createCircuitUpdateProjectCommand(
current.id,
inversePatch
);
const update = tx
.update(circuits)
.set(toCircuitPatchValues(patch))
.where(eq(circuits.id, current.id))
.run();
if (update.changes !== 1) {
throw new Error("Circuit changed before command execution.");
}
return { forward: appliedForward, inverse };
}
function assertSectionInCircuitList(
database: AppDatabase,
circuit: CircuitRow,
sectionId: string | undefined
) {
if (sectionId === undefined || sectionId === circuit.sectionId) {
return;
}
const section = database
.select({ id: circuitSections.id })
.from(circuitSections)
.where(
and(
eq(circuitSections.id, sectionId),
eq(circuitSections.circuitListId, circuit.circuitListId)
)
)
.get();
if (!section) {
throw new Error("Section does not belong to circuit list.");
}
}
function assertUniqueEquipmentIdentifier(
database: AppDatabase,
circuit: CircuitRow,
equipmentIdentifier: string | undefined
) {
if (
equipmentIdentifier === undefined ||
equipmentIdentifier === circuit.equipmentIdentifier
) {
return;
}
assertEquipmentIdentifierAvailable(
database,
circuit.circuitListId,
equipmentIdentifier,
circuit.id
);
}
function getCircuitFieldValue<TField extends CircuitUpdateField>(

View file

@ -14,69 +14,83 @@ import { circuitProtectionDevices } from "../schema/circuit-protection-devices.j
import { circuits } from "../schema/circuits.js";
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
export type CircuitProtectionUpdateApplyInput = Pick<
ExecuteCircuitProtectionCommandInput,
"projectId" | "source" | "command"
>;
export class CircuitProtectionProjectCommandRepository
implements CircuitProtectionProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
execute(input: ExecuteCircuitProtectionCommandInput) {
return executeProjectCommandTransaction(this.database, input, (tx) => {
assertCircuitProtectionUpdateProjectCommand(input.command);
const { circuitId, expected, target } = input.command.payload;
if (input.source === "user" && target === null) {
throw new Error("User commands cannot remove circuit protection.");
}
this.assertOwnership(tx, input.projectId, circuitId);
const actual = tx
.select()
.from(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, circuitId))
.get();
if (!sameNullableSnapshot(expected, actual)) {
throw new Error("Circuit protection changed before update.");
}
if (target === null) {
tx.delete(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, circuitId))
.run();
} else if (actual) {
tx.update(circuitProtectionDevices)
.set(target)
.where(eq(circuitProtectionDevices.circuitId, circuitId))
.run();
} else {
tx.insert(circuitProtectionDevices).values(target).run();
}
return createCircuitProtectionUpdateProjectCommand(
circuitId,
target,
expected
);
});
return executeProjectCommandTransaction(this.database, input, (tx) =>
applyCircuitProtectionUpdateCommand(tx, input)
);
}
}
private assertOwnership(
database: AppDatabase,
projectId: string,
circuitId: string
) {
const circuit = database
.select({ projectId: circuitLists.projectId })
.from(circuits)
.innerJoin(
circuitLists,
eq(circuitLists.id, circuits.circuitListId)
// Applies one circuit-protection.update inside an open transaction and
// returns its exact inverse. Shared with circuit-list.bulk-update.
export function applyCircuitProtectionUpdateCommand(
tx: AppDatabase,
input: CircuitProtectionUpdateApplyInput
) {
assertCircuitProtectionUpdateProjectCommand(input.command);
const { circuitId, expected, target } = input.command.payload;
if (input.source === "user" && target === null) {
throw new Error("User commands cannot remove circuit protection.");
}
assertOwnership(tx, input.projectId, circuitId);
const actual = tx
.select()
.from(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, circuitId))
.get();
if (!sameNullableSnapshot(expected, actual)) {
throw new Error("Circuit protection changed before update.");
}
if (target === null) {
tx.delete(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, circuitId))
.run();
} else if (actual) {
tx.update(circuitProtectionDevices)
.set(target)
.where(eq(circuitProtectionDevices.circuitId, circuitId))
.run();
} else {
tx.insert(circuitProtectionDevices).values(target).run();
}
return createCircuitProtectionUpdateProjectCommand(
circuitId,
target,
expected
);
}
function assertOwnership(
database: AppDatabase,
projectId: string,
circuitId: string
) {
const circuit = database
.select({ projectId: circuitLists.projectId })
.from(circuits)
.innerJoin(
circuitLists,
eq(circuitLists.id, circuits.circuitListId)
)
.where(
and(
eq(circuits.id, circuitId),
eq(circuitLists.projectId, projectId)
)
.where(
and(
eq(circuits.id, circuitId),
eq(circuitLists.projectId, projectId)
)
)
.get();
if (!circuit) {
throw new Error("Circuit protection does not belong to project.");
}
)
.get();
if (!circuit) {
throw new Error("Circuit protection does not belong to project.");
}
}