Keep single-device circuit names equal to their device row
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>
This commit is contained in:
parent
58cec53713
commit
01312b89fe
21 changed files with 4330 additions and 111 deletions
|
|
@ -58,6 +58,11 @@ See `docs/current-architecture.md` for the complete module and request flow.
|
||||||
- A circuit contains zero, one or multiple device rows.
|
- A circuit contains zero, one or multiple device rows.
|
||||||
- A single-device circuit may be displayed as one compact row.
|
- A single-device circuit may be displayed as one compact row.
|
||||||
- A multi-device circuit must be displayed with a circuit summary row and indented device rows.
|
- A multi-device circuit must be displayed with a circuit summary row and indented device rows.
|
||||||
|
- A circuit with exactly one device row and no fed distribution board carries
|
||||||
|
that row's name (`syncSingleDeviceCircuitDisplayNames`). Commands that create
|
||||||
|
or leave such a circuit, rename its row or unlink its downstream board store
|
||||||
|
the circuit-name changes as optional `circuitDisplayNames`, so Undo restores
|
||||||
|
an overwritten free name. Migration `0016` aligned existing data.
|
||||||
- Equipment identifiers belong to circuits, not to every device row.
|
- Equipment identifiers belong to circuits, not to every device row.
|
||||||
- Device rows inside a circuit do not have their own equipment identifiers.
|
- Device rows inside a circuit do not have their own equipment identifiers.
|
||||||
- Protection and cable data belong to the circuit, not to individual devices.
|
- Protection and cable data belong to the circuit, not to individual devices.
|
||||||
|
|
|
||||||
23
src/db/migrations/0016_single_device_circuit_names.sql
Normal file
23
src/db/migrations/0016_single_device_circuit_names.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
-- A circuit with exactly one device row and no fed distribution board is
|
||||||
|
-- shown as that row, so its hidden name follows the device row name.
|
||||||
|
UPDATE `circuits`
|
||||||
|
SET `display_name` = (
|
||||||
|
SELECT `circuit_device_rows`.`display_name`
|
||||||
|
FROM `circuit_device_rows`
|
||||||
|
WHERE `circuit_device_rows`.`circuit_id` = `circuits`.`id`
|
||||||
|
)
|
||||||
|
WHERE (
|
||||||
|
SELECT count(*)
|
||||||
|
FROM `circuit_device_rows`
|
||||||
|
WHERE `circuit_device_rows`.`circuit_id` = `circuits`.`id`
|
||||||
|
) = 1
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM `distribution_boards`
|
||||||
|
WHERE `distribution_boards`.`feeding_circuit_id` = `circuits`.`id`
|
||||||
|
)
|
||||||
|
AND `display_name` IS NOT (
|
||||||
|
SELECT `circuit_device_rows`.`display_name`
|
||||||
|
FROM `circuit_device_rows`
|
||||||
|
WHERE `circuit_device_rows`.`circuit_id` = `circuits`.`id`
|
||||||
|
);
|
||||||
3210
src/db/migrations/meta/0016_snapshot.json
Normal file
3210
src/db/migrations/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -113,6 +113,13 @@
|
||||||
"when": 1790773066696,
|
"when": 1790773066696,
|
||||||
"tag": "0015_wide_night_thrasher",
|
"tag": "0015_wide_night_thrasher",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1793277078601,
|
||||||
|
"tag": "0016_single_device_circuit_names",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
type CircuitDeviceRowMoveHistoryProjectCommand,
|
type CircuitDeviceRowMoveHistoryProjectCommand,
|
||||||
} from "../../domain/models/circuit-device-row-move-project-command.model.js";
|
} from "../../domain/models/circuit-device-row-move-project-command.model.js";
|
||||||
import type { CircuitSnapshot } from "../../domain/models/circuit-structure-project-command.model.js";
|
import type { CircuitSnapshot } from "../../domain/models/circuit-structure-project-command.model.js";
|
||||||
|
import { invertCircuitDisplayNameTransitions } from "../../domain/models/circuit-display-name-sync.model.js";
|
||||||
import type {
|
import type {
|
||||||
CircuitDeviceRowMoveProjectCommandStore,
|
CircuitDeviceRowMoveProjectCommandStore,
|
||||||
ExecuteCircuitDeviceRowMoveCommandInput,
|
ExecuteCircuitDeviceRowMoveCommandInput,
|
||||||
|
|
@ -20,7 +21,11 @@ import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js";
|
||||||
|
import {
|
||||||
|
executeProjectCommandTransactionWithAppliedForward,
|
||||||
|
type AppliedProjectCommand,
|
||||||
|
} from "./project-command-transaction.persistence.js";
|
||||||
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||||
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
|
import { resolveCircuitVoltage } from "./project-voltage.persistence.js";
|
||||||
import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
||||||
|
|
@ -30,6 +35,7 @@ interface PersistedMoveRow {
|
||||||
circuitId: string;
|
circuitId: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
phaseType: string | null;
|
phaseType: string | null;
|
||||||
|
displayName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CircuitDeviceRowMoveProjectCommandRepository
|
export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
|
|
@ -40,11 +46,16 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
execute(input: ExecuteCircuitDeviceRowMoveCommandInput) {
|
execute(input: ExecuteCircuitDeviceRowMoveCommandInput) {
|
||||||
this.assertSupportedCommand(input.command);
|
this.assertSupportedCommand(input.command);
|
||||||
|
|
||||||
return executeProjectCommandTransaction(
|
return executeProjectCommandTransactionWithAppliedForward(
|
||||||
this.database,
|
this.database,
|
||||||
input,
|
input,
|
||||||
(tx) =>
|
(tx) =>
|
||||||
this.applyCommand(tx, input.projectId, input.command)
|
this.applyCommand(
|
||||||
|
tx,
|
||||||
|
input.projectId,
|
||||||
|
input.source,
|
||||||
|
input.command
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,8 +81,12 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
private applyCommand(
|
private applyCommand(
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
source: ExecuteCircuitDeviceRowMoveCommandInput["source"],
|
||||||
command: CircuitDeviceRowMoveHistoryProjectCommand
|
command: CircuitDeviceRowMoveHistoryProjectCommand
|
||||||
): CircuitDeviceRowMoveHistoryProjectCommand {
|
): AppliedProjectCommand<
|
||||||
|
CircuitDeviceRowMoveHistoryProjectCommand,
|
||||||
|
CircuitDeviceRowMoveHistoryProjectCommand
|
||||||
|
> {
|
||||||
if (command.type === circuitDeviceRowMoveCommandType) {
|
if (command.type === circuitDeviceRowMoveCommandType) {
|
||||||
const rowsById = this.loadExpectedRows(
|
const rowsById = this.loadExpectedRows(
|
||||||
database,
|
database,
|
||||||
|
|
@ -85,21 +100,43 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
projectId,
|
projectId,
|
||||||
circuitIds
|
circuitIds
|
||||||
);
|
);
|
||||||
const inverse = createCircuitDeviceRowMoveProjectCommand(
|
|
||||||
this.reverseMoves(command.payload.moves, rowsById)
|
|
||||||
);
|
|
||||||
this.applyMoves(database, command.payload.moves);
|
this.applyMoves(database, command.payload.moves);
|
||||||
this.updateReserveStates(database, circuitIds);
|
this.updateReserveStates(database, circuitIds);
|
||||||
this.updateVoltages(database, projectId, circuitIds);
|
this.updateVoltages(database, projectId, circuitIds);
|
||||||
return inverse;
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
||||||
|
database,
|
||||||
|
source,
|
||||||
|
circuitIds,
|
||||||
|
command.payload.circuitDisplayNames
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
forward: createCircuitDeviceRowMoveProjectCommand(
|
||||||
|
command.payload.moves,
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
|
inverse: createCircuitDeviceRowMoveProjectCommand(
|
||||||
|
this.reverseMoves(command.payload.moves, rowsById),
|
||||||
|
invertCircuitDisplayNameTransitions(circuitDisplayNames)
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
command
|
command
|
||||||
);
|
);
|
||||||
const { targetCircuit, targetCircuitAction, moves } =
|
const { targetCircuitAction, moves } = command.payload;
|
||||||
command.payload;
|
|
||||||
const rowsById = this.loadExpectedRows(database, moves);
|
const rowsById = this.loadExpectedRows(database, moves);
|
||||||
|
// A new circuit receiving exactly one row is shown as that row, so it
|
||||||
|
// takes the row's name instead of the client's placeholder name.
|
||||||
|
const targetCircuit =
|
||||||
|
source === "user" &&
|
||||||
|
targetCircuitAction === "create" &&
|
||||||
|
moves.length === 1
|
||||||
|
? {
|
||||||
|
...command.payload.targetCircuit,
|
||||||
|
displayName: rowsById.get(moves[0]!.rowId)!.displayName,
|
||||||
|
}
|
||||||
|
: command.payload.targetCircuit;
|
||||||
const appliedTargetCircuit = {
|
const appliedTargetCircuit = {
|
||||||
...targetCircuit,
|
...targetCircuit,
|
||||||
voltage: resolveCircuitVoltage(
|
voltage: resolveCircuitVoltage(
|
||||||
|
|
@ -124,12 +161,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
this.assertTargetCircuitAvailable(database, appliedTargetCircuit);
|
this.assertTargetCircuitAvailable(database, appliedTargetCircuit);
|
||||||
this.insertTargetCircuit(database, appliedTargetCircuit);
|
this.insertTargetCircuit(database, appliedTargetCircuit);
|
||||||
|
|
||||||
const inverse =
|
|
||||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
|
||||||
"delete",
|
|
||||||
appliedTargetCircuit,
|
|
||||||
this.reverseMoves(moves, rowsById)
|
|
||||||
);
|
|
||||||
this.applyMoves(database, moves);
|
this.applyMoves(database, moves);
|
||||||
this.updateReserveStates(database, [
|
this.updateReserveStates(database, [
|
||||||
...sourceCircuitIds,
|
...sourceCircuitIds,
|
||||||
|
|
@ -139,7 +170,26 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
...sourceCircuitIds,
|
...sourceCircuitIds,
|
||||||
targetCircuit.id,
|
targetCircuit.id,
|
||||||
]);
|
]);
|
||||||
return inverse;
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
||||||
|
database,
|
||||||
|
source,
|
||||||
|
[...sourceCircuitIds, targetCircuit.id],
|
||||||
|
command.payload.circuitDisplayNames
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
forward: createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
|
"create",
|
||||||
|
targetCircuit,
|
||||||
|
moves,
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
|
inverse: createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
|
"delete",
|
||||||
|
appliedTargetCircuit,
|
||||||
|
this.reverseMoves(moves, rowsById),
|
||||||
|
invertCircuitDisplayNameTransitions(circuitDisplayNames)
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.assertTargetCircuitUnchanged(
|
this.assertTargetCircuitUnchanged(
|
||||||
|
|
@ -156,12 +206,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
destinationCircuitIds,
|
destinationCircuitIds,
|
||||||
targetCircuit.circuitListId
|
targetCircuit.circuitListId
|
||||||
);
|
);
|
||||||
const inverse =
|
|
||||||
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
|
||||||
"create",
|
|
||||||
appliedTargetCircuit,
|
|
||||||
this.reverseMoves(moves, rowsById)
|
|
||||||
);
|
|
||||||
this.applyMoves(database, moves);
|
this.applyMoves(database, moves);
|
||||||
this.updateReserveStates(database, [
|
this.updateReserveStates(database, [
|
||||||
targetCircuit.id,
|
targetCircuit.id,
|
||||||
|
|
@ -172,6 +216,12 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
projectId,
|
projectId,
|
||||||
destinationCircuitIds
|
destinationCircuitIds
|
||||||
);
|
);
|
||||||
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
||||||
|
database,
|
||||||
|
source,
|
||||||
|
destinationCircuitIds,
|
||||||
|
command.payload.circuitDisplayNames
|
||||||
|
);
|
||||||
assertCircuitsAreNotReferencedAsFeeders(database, projectId, [
|
assertCircuitsAreNotReferencedAsFeeders(database, projectId, [
|
||||||
targetCircuit.id,
|
targetCircuit.id,
|
||||||
]);
|
]);
|
||||||
|
|
@ -193,7 +243,20 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
"Created target circuit changed before deletion."
|
"Created target circuit changed before deletion."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return inverse;
|
return {
|
||||||
|
forward: createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
|
"delete",
|
||||||
|
targetCircuit,
|
||||||
|
moves,
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
|
inverse: createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
|
"create",
|
||||||
|
appliedTargetCircuit,
|
||||||
|
this.reverseMoves(moves, rowsById),
|
||||||
|
invertCircuitDisplayNameTransitions(circuitDisplayNames)
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateVoltages(
|
private updateVoltages(
|
||||||
|
|
@ -217,6 +280,7 @@ export class CircuitDeviceRowMoveProjectCommandRepository
|
||||||
circuitId: circuitDeviceRows.circuitId,
|
circuitId: circuitDeviceRows.circuitId,
|
||||||
sortOrder: circuitDeviceRows.sortOrder,
|
sortOrder: circuitDeviceRows.sortOrder,
|
||||||
phaseType: circuitDeviceRows.phaseType,
|
phaseType: circuitDeviceRows.phaseType,
|
||||||
|
displayName: circuitDeviceRows.displayName,
|
||||||
})
|
})
|
||||||
.from(circuitDeviceRows)
|
.from(circuitDeviceRows)
|
||||||
.where(inArray(circuitDeviceRows.id, rowIds))
|
.where(inArray(circuitDeviceRows.id, rowIds))
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ import {
|
||||||
type CircuitDeviceRowUpdatePatch,
|
type CircuitDeviceRowUpdatePatch,
|
||||||
type CircuitDeviceRowUpdateValues,
|
type CircuitDeviceRowUpdateValues,
|
||||||
} from "../../domain/models/circuit-device-row-project-command.model.js";
|
} from "../../domain/models/circuit-device-row-project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertCircuitDisplayNameTransitions,
|
||||||
|
invertCircuitDisplayNameTransitions,
|
||||||
|
} from "../../domain/models/circuit-display-name-sync.model.js";
|
||||||
import type {
|
import type {
|
||||||
CircuitDeviceRowProjectCommandStore,
|
CircuitDeviceRowProjectCommandStore,
|
||||||
ExecuteCircuitDeviceRowUpdateCommandInput,
|
ExecuteCircuitDeviceRowUpdateCommandInput,
|
||||||
|
|
@ -22,6 +26,7 @@ import {
|
||||||
toCircuitDeviceRowPatchValues,
|
toCircuitDeviceRowPatchValues,
|
||||||
type CircuitDeviceRowPatchInput,
|
type CircuitDeviceRowPatchInput,
|
||||||
} from "./circuit-device-row.persistence.js";
|
} from "./circuit-device-row.persistence.js";
|
||||||
|
import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js";
|
||||||
import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js";
|
import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js";
|
||||||
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||||
|
|
||||||
|
|
@ -34,6 +39,9 @@ export class CircuitDeviceRowProjectCommandRepository
|
||||||
|
|
||||||
executeUpdate(input: ExecuteCircuitDeviceRowUpdateCommandInput) {
|
executeUpdate(input: ExecuteCircuitDeviceRowUpdateCommandInput) {
|
||||||
assertCircuitDeviceRowUpdateProjectCommand(input.command);
|
assertCircuitDeviceRowUpdateProjectCommand(input.command);
|
||||||
|
assertCircuitDisplayNameTransitions(
|
||||||
|
input.command.payload.circuitDisplayNames
|
||||||
|
);
|
||||||
|
|
||||||
return executeProjectCommandTransactionWithAppliedForward(
|
return executeProjectCommandTransactionWithAppliedForward(
|
||||||
this.database,
|
this.database,
|
||||||
|
|
@ -111,20 +119,16 @@ export class CircuitDeviceRowProjectCommandRepository
|
||||||
patch.overriddenFields = overriddenFields;
|
patch.overriddenFields = overriddenFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appliedForward = createCircuitDeviceRowUpdateProjectCommand(
|
const appliedPatch = createCircuitDeviceRowUpdateProjectCommand(
|
||||||
current.id,
|
current.id,
|
||||||
patch as CircuitDeviceRowUpdatePatch
|
patch as CircuitDeviceRowUpdatePatch
|
||||||
);
|
);
|
||||||
const inversePatch = Object.fromEntries(
|
const inversePatch = Object.fromEntries(
|
||||||
appliedForward.payload.changes.map((change) => [
|
appliedPatch.payload.changes.map((change) => [
|
||||||
change.field,
|
change.field,
|
||||||
getCircuitDeviceRowFieldValue(current, change.field),
|
getCircuitDeviceRowFieldValue(current, change.field),
|
||||||
])
|
])
|
||||||
) as CircuitDeviceRowUpdatePatch;
|
) as CircuitDeviceRowUpdatePatch;
|
||||||
const inverse = createCircuitDeviceRowUpdateProjectCommand(
|
|
||||||
current.id,
|
|
||||||
inversePatch
|
|
||||||
);
|
|
||||||
|
|
||||||
const update = tx
|
const update = tx
|
||||||
.update(circuitDeviceRows)
|
.update(circuitDeviceRows)
|
||||||
|
|
@ -141,10 +145,24 @@ export class CircuitDeviceRowProjectCommandRepository
|
||||||
current.circuitId
|
current.circuitId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
||||||
|
tx,
|
||||||
|
input.source,
|
||||||
|
patch.displayName === undefined ? [] : [current.circuitId],
|
||||||
|
input.command.payload.circuitDisplayNames
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
forward: appliedForward,
|
forward: createCircuitDeviceRowUpdateProjectCommand(
|
||||||
inverse,
|
current.id,
|
||||||
|
patch as CircuitDeviceRowUpdatePatch,
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
|
inverse: createCircuitDeviceRowUpdateProjectCommand(
|
||||||
|
current.id,
|
||||||
|
inversePatch,
|
||||||
|
invertCircuitDisplayNameTransitions(circuitDisplayNames)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ import {
|
||||||
type CircuitDeviceRowSnapshot,
|
type CircuitDeviceRowSnapshot,
|
||||||
type CircuitDeviceRowStructureProjectCommand,
|
type CircuitDeviceRowStructureProjectCommand,
|
||||||
} from "../../domain/models/circuit-device-row-structure-project-command.model.js";
|
} from "../../domain/models/circuit-device-row-structure-project-command.model.js";
|
||||||
|
import {
|
||||||
|
invertCircuitDisplayNameTransitions,
|
||||||
|
type CircuitDisplayNameTransition,
|
||||||
|
} from "../../domain/models/circuit-display-name-sync.model.js";
|
||||||
import type {
|
import type {
|
||||||
CircuitDeviceRowStructureProjectCommandStore,
|
CircuitDeviceRowStructureProjectCommandStore,
|
||||||
ExecuteCircuitDeviceRowStructureCommandInput,
|
ExecuteCircuitDeviceRowStructureCommandInput,
|
||||||
|
|
@ -23,6 +27,7 @@ import {
|
||||||
toCircuitDeviceRowInsertValues,
|
toCircuitDeviceRowInsertValues,
|
||||||
toCircuitDeviceRowSnapshot,
|
toCircuitDeviceRowSnapshot,
|
||||||
} from "./circuit-device-row-structure.persistence.js";
|
} from "./circuit-device-row-structure.persistence.js";
|
||||||
|
import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js";
|
||||||
import { executeProjectCommandTransactionWithAppliedForward, type AppliedProjectCommand } from "./project-command-transaction.persistence.js";
|
import { executeProjectCommandTransactionWithAppliedForward, type AppliedProjectCommand } from "./project-command-transaction.persistence.js";
|
||||||
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -69,7 +74,8 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
||||||
projectId,
|
projectId,
|
||||||
source,
|
source,
|
||||||
command.payload.row,
|
command.payload.row,
|
||||||
command.payload.externalObjects ?? []
|
command.payload.externalObjects ?? [],
|
||||||
|
command.payload.circuitDisplayNames
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (command.type === circuitDeviceRowDeleteCommandType) {
|
if (command.type === circuitDeviceRowDeleteCommandType) {
|
||||||
|
|
@ -77,9 +83,11 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
||||||
return this.delete(
|
return this.delete(
|
||||||
database,
|
database,
|
||||||
projectId,
|
projectId,
|
||||||
|
source,
|
||||||
command.payload.rowId,
|
command.payload.rowId,
|
||||||
command.payload.expectedCircuitId,
|
command.payload.expectedCircuitId,
|
||||||
command.payload.externalObjects
|
command.payload.externalObjects,
|
||||||
|
command.payload.circuitDisplayNames
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error("Unsupported circuit device-row structure command.");
|
throw new Error("Unsupported circuit device-row structure command.");
|
||||||
|
|
@ -90,7 +98,8 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
||||||
projectId: string,
|
projectId: string,
|
||||||
source: ExecuteCircuitDeviceRowStructureCommandInput["source"],
|
source: ExecuteCircuitDeviceRowStructureCommandInput["source"],
|
||||||
row: CircuitDeviceRowSnapshot,
|
row: CircuitDeviceRowSnapshot,
|
||||||
externalObjects: ExternalObjectLinkTransition[]
|
externalObjects: ExternalObjectLinkTransition[],
|
||||||
|
storedCircuitDisplayNames: CircuitDisplayNameTransition[] | undefined
|
||||||
) {
|
) {
|
||||||
if (source === "user" && !isElectricalPhaseType(row.phaseType)) {
|
if (source === "user" && !isElectricalPhaseType(row.phaseType)) {
|
||||||
throw new Error("Device-row phase type is invalid.");
|
throw new Error("Device-row phase type is invalid.");
|
||||||
|
|
@ -125,19 +134,32 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
||||||
loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Wiederherstellen geändert.");
|
loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Wiederherstellen geändert.");
|
||||||
applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Wiederherstellen geändert.");
|
applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Wiederherstellen geändert.");
|
||||||
}
|
}
|
||||||
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
||||||
|
database,
|
||||||
|
source,
|
||||||
|
[row.circuitId],
|
||||||
|
storedCircuitDisplayNames
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
forward: createCircuitDeviceRowInsertProjectCommand(row, externalObjects),
|
forward: createCircuitDeviceRowInsertProjectCommand(row, externalObjects, circuitDisplayNames),
|
||||||
inverse: createCircuitDeviceRowDeleteProjectCommand(row.id, row.circuitId, invertExternalObjectLinkTransitions(externalObjects)),
|
inverse: createCircuitDeviceRowDeleteProjectCommand(
|
||||||
|
row.id,
|
||||||
|
row.circuitId,
|
||||||
|
invertExternalObjectLinkTransitions(externalObjects),
|
||||||
|
invertCircuitDisplayNameTransitions(circuitDisplayNames)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private delete(
|
private delete(
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
source: ExecuteCircuitDeviceRowStructureCommandInput["source"],
|
||||||
rowId: string,
|
rowId: string,
|
||||||
expectedCircuitId: string,
|
expectedCircuitId: string,
|
||||||
suppliedExternalObjects?: ExternalObjectLinkTransition[]
|
suppliedExternalObjects: ExternalObjectLinkTransition[] | undefined,
|
||||||
|
storedCircuitDisplayNames: CircuitDisplayNameTransition[] | undefined
|
||||||
) {
|
) {
|
||||||
const row = database
|
const row = database
|
||||||
.select()
|
.select()
|
||||||
|
|
@ -187,10 +209,20 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
||||||
projectId,
|
projectId,
|
||||||
expectedCircuitId
|
expectedCircuitId
|
||||||
);
|
);
|
||||||
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
||||||
|
database,
|
||||||
|
source,
|
||||||
|
[expectedCircuitId],
|
||||||
|
storedCircuitDisplayNames
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
forward: createCircuitDeviceRowDeleteProjectCommand(rowId, expectedCircuitId, externalObjects),
|
forward: createCircuitDeviceRowDeleteProjectCommand(rowId, expectedCircuitId, externalObjects, circuitDisplayNames),
|
||||||
inverse: createCircuitDeviceRowInsertProjectCommand(toCircuitDeviceRowSnapshot(row), invertExternalObjectLinkTransitions(externalObjects)),
|
inverse: createCircuitDeviceRowInsertProjectCommand(
|
||||||
|
toCircuitDeviceRowSnapshot(row),
|
||||||
|
invertExternalObjectLinkTransitions(externalObjects),
|
||||||
|
invertCircuitDisplayNameTransitions(circuitDisplayNames)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
89
src/db/repositories/circuit-display-name-sync.persistence.ts
Normal file
89
src/db/repositories/circuit-display-name-sync.persistence.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
resolveSingleDeviceCircuitDisplayName,
|
||||||
|
type CircuitDisplayNameTransition,
|
||||||
|
} from "../../domain/models/circuit-display-name-sync.model.js";
|
||||||
|
import type { ProjectRevisionSource } from "../../domain/ports/project-revision.store.js";
|
||||||
|
import type { AppDatabase } from "../database-context.js";
|
||||||
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
|
import { circuits } from "../schema/circuits.js";
|
||||||
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the name of single-device circuits equal to their device row name.
|
||||||
|
* A user command derives the changes for the affected circuits after its own
|
||||||
|
* write; history replays apply exactly the stored changes. Commands stored
|
||||||
|
* before this rule carry no changes and keep their original behavior.
|
||||||
|
*/
|
||||||
|
export function syncSingleDeviceCircuitDisplayNames(
|
||||||
|
database: AppDatabase,
|
||||||
|
source: ProjectRevisionSource,
|
||||||
|
circuitIds: Iterable<string>,
|
||||||
|
storedTransitions: readonly CircuitDisplayNameTransition[] | undefined
|
||||||
|
): CircuitDisplayNameTransition[] {
|
||||||
|
if (source !== "user") {
|
||||||
|
const transitions = [...(storedTransitions ?? [])];
|
||||||
|
for (const transition of transitions) {
|
||||||
|
applyCircuitDisplayNameTransition(database, transition);
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transitions: CircuitDisplayNameTransition[] = [];
|
||||||
|
for (const circuitId of new Set(circuitIds)) {
|
||||||
|
const circuit = database
|
||||||
|
.select({ displayName: circuits.displayName })
|
||||||
|
.from(circuits)
|
||||||
|
.where(eq(circuits.id, circuitId))
|
||||||
|
.get();
|
||||||
|
if (!circuit) continue;
|
||||||
|
const deviceRowDisplayNames = database
|
||||||
|
.select({ displayName: circuitDeviceRows.displayName })
|
||||||
|
.from(circuitDeviceRows)
|
||||||
|
.where(eq(circuitDeviceRows.circuitId, circuitId))
|
||||||
|
.limit(2)
|
||||||
|
.all()
|
||||||
|
.map((row) => row.displayName);
|
||||||
|
const fedBoard = database
|
||||||
|
.select({ id: distributionBoards.id })
|
||||||
|
.from(distributionBoards)
|
||||||
|
.where(eq(distributionBoards.feedingCircuitId, circuitId))
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
const target = resolveSingleDeviceCircuitDisplayName({
|
||||||
|
displayName: circuit.displayName,
|
||||||
|
deviceRowDisplayNames,
|
||||||
|
feedsDistributionBoard: fedBoard !== undefined,
|
||||||
|
});
|
||||||
|
if (target === null) continue;
|
||||||
|
const transition = {
|
||||||
|
circuitId,
|
||||||
|
expected: circuit.displayName,
|
||||||
|
target,
|
||||||
|
};
|
||||||
|
applyCircuitDisplayNameTransition(database, transition);
|
||||||
|
transitions.push(transition);
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCircuitDisplayNameTransition(
|
||||||
|
database: AppDatabase,
|
||||||
|
transition: CircuitDisplayNameTransition
|
||||||
|
) {
|
||||||
|
const updated = database
|
||||||
|
.update(circuits)
|
||||||
|
.set({ displayName: transition.target })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(circuits.id, transition.circuitId),
|
||||||
|
transition.expected === null
|
||||||
|
? isNull(circuits.displayName)
|
||||||
|
: eq(circuits.displayName, transition.expected)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
if (updated.changes !== 1) {
|
||||||
|
throw new Error("Circuit name changed before command execution.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -106,9 +106,17 @@ export class CircuitStructureProjectCommandRepository
|
||||||
database: AppDatabase,
|
database: AppDatabase,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
source: ExecuteCircuitStructureCommandInput["source"],
|
source: ExecuteCircuitStructureCommandInput["source"],
|
||||||
snapshot: CircuitSnapshot,
|
requestedSnapshot: CircuitSnapshot,
|
||||||
externalObjects: ExternalObjectLinkTransition[]
|
externalObjects: ExternalObjectLinkTransition[]
|
||||||
) {
|
) {
|
||||||
|
// A single-device circuit is shown as its device row and carries its name.
|
||||||
|
const snapshot =
|
||||||
|
source === "user" && requestedSnapshot.deviceRows.length === 1
|
||||||
|
? {
|
||||||
|
...requestedSnapshot,
|
||||||
|
displayName: requestedSnapshot.deviceRows[0]!.displayName,
|
||||||
|
}
|
||||||
|
: requestedSnapshot;
|
||||||
this.assertCircuitLocation(database, projectId, snapshot);
|
this.assertCircuitLocation(database, projectId, snapshot);
|
||||||
if (source === "user") {
|
if (source === "user") {
|
||||||
const derivedVoltage = resolveCircuitVoltage(
|
const derivedVoltage = resolveCircuitVoltage(
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
createCircuitDeleteProjectCommand,
|
createCircuitDeleteProjectCommand,
|
||||||
createCircuitInsertProjectCommand,
|
createCircuitInsertProjectCommand,
|
||||||
} from "../../domain/models/circuit-structure-project-command.model.js";
|
} from "../../domain/models/circuit-structure-project-command.model.js";
|
||||||
|
import { invertCircuitDisplayNameTransitions } from "../../domain/models/circuit-display-name-sync.model.js";
|
||||||
import type {
|
import type {
|
||||||
DistributionBoardStructureProjectCommandStore,
|
DistributionBoardStructureProjectCommandStore,
|
||||||
ExecuteDistributionBoardStructureCommandInput,
|
ExecuteDistributionBoardStructureCommandInput,
|
||||||
|
|
@ -42,7 +43,12 @@ import { distributionBoardComponents } from "../schema/distribution-board-compon
|
||||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||||
import { floors } from "../schema/floors.js";
|
import { floors } from "../schema/floors.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js";
|
||||||
|
import {
|
||||||
|
executeProjectCommandTransaction,
|
||||||
|
executeProjectCommandTransactionWithAppliedForward,
|
||||||
|
type AppliedProjectCommand,
|
||||||
|
} from "./project-command-transaction.persistence.js";
|
||||||
import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js";
|
||||||
import { assertDistributionBoardHasNoExternalObjects } from "./external-object-deletion-guard.persistence.js";
|
import { assertDistributionBoardHasNoExternalObjects } from "./external-object-deletion-guard.persistence.js";
|
||||||
import { CircuitStructureProjectCommandRepository } from "./circuit-structure-project-command.repository.js";
|
import { CircuitStructureProjectCommandRepository } from "./circuit-structure-project-command.repository.js";
|
||||||
|
|
@ -70,7 +76,7 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
assertDistributionBoardUpdateProjectCommand(input.command);
|
assertDistributionBoardUpdateProjectCommand(input.command);
|
||||||
return executeProjectCommandTransaction(
|
return executeProjectCommandTransactionWithAppliedForward(
|
||||||
this.database,
|
this.database,
|
||||||
input,
|
input,
|
||||||
(tx) => this.update(tx, input)
|
(tx) => this.update(tx, input)
|
||||||
|
|
@ -94,7 +100,7 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
"Generated feeder circuits may only be removed through project history."
|
"Generated feeder circuits may only be removed through project history."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return executeProjectCommandTransaction(
|
return executeProjectCommandTransactionWithAppliedForward(
|
||||||
this.database,
|
this.database,
|
||||||
input,
|
input,
|
||||||
(tx) => this.applyFeederCircuitCommand(tx, input)
|
(tx) => this.applyFeederCircuitCommand(tx, input)
|
||||||
|
|
@ -384,11 +390,6 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
getDistributionBoardFieldValue(current, change.field),
|
getDistributionBoardFieldValue(current, change.field),
|
||||||
])
|
])
|
||||||
) as DistributionBoardUpdatePatch;
|
) as DistributionBoardUpdatePatch;
|
||||||
const inverse = createDistributionBoardUpdateProjectCommand(
|
|
||||||
current.id,
|
|
||||||
inversePatch,
|
|
||||||
input.command.schemaVersion
|
|
||||||
);
|
|
||||||
const updated = database
|
const updated = database
|
||||||
.update(distributionBoards)
|
.update(distributionBoards)
|
||||||
.set(patch)
|
.set(patch)
|
||||||
|
|
@ -404,7 +405,26 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
"Distribution board changed before command execution."
|
"Distribution board changed before command execution."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return inverse;
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
||||||
|
database,
|
||||||
|
input.source,
|
||||||
|
current.feedingCircuitId === null ? [] : [current.feedingCircuitId],
|
||||||
|
input.command.payload.circuitDisplayNames
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
forward: createDistributionBoardUpdateProjectCommand(
|
||||||
|
current.id,
|
||||||
|
patch,
|
||||||
|
input.command.schemaVersion,
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
|
inverse: createDistributionBoardUpdateProjectCommand(
|
||||||
|
current.id,
|
||||||
|
inversePatch,
|
||||||
|
input.command.schemaVersion,
|
||||||
|
invertCircuitDisplayNameTransitions(circuitDisplayNames)
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyFeederCircuitCommand(
|
private applyFeederCircuitCommand(
|
||||||
|
|
@ -415,7 +435,10 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
> & {
|
> & {
|
||||||
command: DistributionBoardFeederCircuitProjectCommand;
|
command: DistributionBoardFeederCircuitProjectCommand;
|
||||||
}
|
}
|
||||||
): DistributionBoardFeederCircuitProjectCommand {
|
): AppliedProjectCommand<
|
||||||
|
DistributionBoardFeederCircuitProjectCommand,
|
||||||
|
DistributionBoardFeederCircuitProjectCommand
|
||||||
|
> {
|
||||||
const circuitRepository =
|
const circuitRepository =
|
||||||
new CircuitStructureProjectCommandRepository(database);
|
new CircuitStructureProjectCommandRepository(database);
|
||||||
const { circuit, boardUpdate } = input.command.payload;
|
const { circuit, boardUpdate } = input.command.payload;
|
||||||
|
|
@ -431,17 +454,23 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
if (circuitInverse.type !== circuitDeleteCommandType) {
|
if (circuitInverse.type !== circuitDeleteCommandType) {
|
||||||
throw new Error("Created feeder circuit returned an invalid inverse.");
|
throw new Error("Created feeder circuit returned an invalid inverse.");
|
||||||
}
|
}
|
||||||
const boardInverse = this.update(database, {
|
const board = this.update(database, {
|
||||||
...input,
|
...input,
|
||||||
command: boardUpdate,
|
command: boardUpdate,
|
||||||
});
|
});
|
||||||
return createDistributionBoardRemoveFeederCircuitProjectCommand(
|
return {
|
||||||
circuit,
|
forward: createDistributionBoardCreateFeederCircuitProjectCommand(
|
||||||
boardInverse
|
circuit,
|
||||||
);
|
board.forward
|
||||||
|
),
|
||||||
|
inverse: createDistributionBoardRemoveFeederCircuitProjectCommand(
|
||||||
|
circuit,
|
||||||
|
board.inverse
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const boardInverse = this.update(database, {
|
const board = this.update(database, {
|
||||||
...input,
|
...input,
|
||||||
command: boardUpdate,
|
command: boardUpdate,
|
||||||
});
|
});
|
||||||
|
|
@ -459,10 +488,16 @@ export class DistributionBoardStructureProjectCommandRepository
|
||||||
"Generated feeder circuit changed before history removal."
|
"Generated feeder circuit changed before history removal."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return createDistributionBoardCreateFeederCircuitProjectCommand(
|
return {
|
||||||
circuit,
|
forward: createDistributionBoardRemoveFeederCircuitProjectCommand(
|
||||||
boardInverse
|
circuit,
|
||||||
);
|
board.forward
|
||||||
|
),
|
||||||
|
inverse: createDistributionBoardCreateFeederCircuitProjectCommand(
|
||||||
|
circuit,
|
||||||
|
board.inverse
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertFeedingReference(
|
private assertFeedingReference(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { eq } from "drizzle-orm";
|
||||||
import { assertCircuitDeviceRowQuantity } from "../../domain/calculations/circuit-device-row-quantity.js";
|
import { assertCircuitDeviceRowQuantity } from "../../domain/calculations/circuit-device-row-quantity.js";
|
||||||
import {
|
import {
|
||||||
assertExternalObjectNewRowProjectCommand,
|
assertExternalObjectNewRowProjectCommand,
|
||||||
|
createExternalObjectNewRowProjectCommand,
|
||||||
externalObjectAssignToNewRowCommandType,
|
externalObjectAssignToNewRowCommandType,
|
||||||
externalObjectDeleteCreatedRowCommandType,
|
externalObjectDeleteCreatedRowCommandType,
|
||||||
invertExternalObjectNewRowProjectCommand,
|
invertExternalObjectNewRowProjectCommand,
|
||||||
|
|
@ -20,7 +21,8 @@ import {
|
||||||
toCircuitDeviceRowInsertValues,
|
toCircuitDeviceRowInsertValues,
|
||||||
toCircuitDeviceRowSnapshot,
|
toCircuitDeviceRowSnapshot,
|
||||||
} from "./circuit-device-row-structure.persistence.js";
|
} from "./circuit-device-row-structure.persistence.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.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";
|
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||||
import {
|
import {
|
||||||
applyExternalObjectLinks,
|
applyExternalObjectLinks,
|
||||||
|
|
@ -42,13 +44,26 @@ export class ExternalObjectNewRowProjectCommandRepository
|
||||||
) {
|
) {
|
||||||
throw new Error("Created external rows may only be removed through project history.");
|
throw new Error("Created external rows may only be removed through project history.");
|
||||||
}
|
}
|
||||||
return executeProjectCommandTransaction(this.database, input, (tx) => {
|
return executeProjectCommandTransactionWithAppliedForward(this.database, input, (tx) => {
|
||||||
if (input.command.type === externalObjectAssignToNewRowCommandType) {
|
if (input.command.type === externalObjectAssignToNewRowCommandType) {
|
||||||
this.insert(tx, input.projectId, input.command);
|
this.insert(tx, input.projectId, input.command);
|
||||||
} else {
|
} else {
|
||||||
this.remove(tx, input.projectId, input.command);
|
this.remove(tx, input.projectId, input.command);
|
||||||
}
|
}
|
||||||
return invertExternalObjectNewRowProjectCommand(input.command);
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
||||||
|
tx,
|
||||||
|
input.source,
|
||||||
|
[input.command.payload.row.circuitId],
|
||||||
|
input.command.payload.circuitDisplayNames
|
||||||
|
);
|
||||||
|
const forward = createExternalObjectNewRowProjectCommand(
|
||||||
|
input.command.type,
|
||||||
|
{ ...input.command.payload, circuitDisplayNames }
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
forward,
|
||||||
|
inverse: invertExternalObjectNewRowProjectCommand(forward),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
projectDeviceSyncRowSnapshotFields,
|
projectDeviceSyncRowSnapshotFields,
|
||||||
type ProjectDeviceSyncRowSnapshot,
|
type ProjectDeviceSyncRowSnapshot,
|
||||||
} from "../../domain/models/project-device-row-sync-project-command.model.js";
|
} from "../../domain/models/project-device-row-sync-project-command.model.js";
|
||||||
|
import { invertCircuitDisplayNameTransitions } from "../../domain/models/circuit-display-name-sync.model.js";
|
||||||
import type {
|
import type {
|
||||||
ExecuteProjectDeviceRowSyncCommandInput,
|
ExecuteProjectDeviceRowSyncCommandInput,
|
||||||
ProjectDeviceRowSyncProjectCommandStore,
|
ProjectDeviceRowSyncProjectCommandStore,
|
||||||
|
|
@ -16,7 +17,8 @@ import { circuitLists } from "../schema/circuit-lists.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { externalModelObjects } from "../schema/external-model-objects.js";
|
import { externalModelObjects } from "../schema/external-model-objects.js";
|
||||||
import { projectDevices } from "../schema/project-devices.js";
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.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";
|
import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js";
|
||||||
|
|
||||||
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
||||||
|
|
@ -29,7 +31,7 @@ export class ProjectDeviceRowSyncProjectCommandRepository
|
||||||
execute(input: ExecuteProjectDeviceRowSyncCommandInput) {
|
execute(input: ExecuteProjectDeviceRowSyncCommandInput) {
|
||||||
assertProjectDeviceRowSyncProjectCommand(input.command);
|
assertProjectDeviceRowSyncProjectCommand(input.command);
|
||||||
|
|
||||||
return executeProjectCommandTransaction(
|
return executeProjectCommandTransactionWithAppliedForward(
|
||||||
this.database,
|
this.database,
|
||||||
input,
|
input,
|
||||||
(tx) => this.applyCommand(tx, input)
|
(tx) => this.applyCommand(tx, input)
|
||||||
|
|
@ -107,18 +109,6 @@ export class ProjectDeviceRowSyncProjectCommandRepository
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inverse = createProjectDeviceRowSyncProjectCommand(
|
|
||||||
projectDevice.id,
|
|
||||||
invertProjectDeviceRowSyncOperation(
|
|
||||||
input.command.payload.operation
|
|
||||||
),
|
|
||||||
input.command.payload.rows.map((row) => ({
|
|
||||||
rowId: row.rowId,
|
|
||||||
expected: row.target,
|
|
||||||
target: row.expected,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const assignment of input.command.payload.rows) {
|
for (const assignment of input.command.payload.rows) {
|
||||||
const values: typeof assignment.target & {
|
const values: typeof assignment.target & {
|
||||||
manualQuantity?: number;
|
manualQuantity?: number;
|
||||||
|
|
@ -173,8 +163,40 @@ export class ProjectDeviceRowSyncProjectCommandRepository
|
||||||
circuitId
|
circuitId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames(
|
||||||
|
tx,
|
||||||
|
input.source,
|
||||||
|
input.command.payload.rows
|
||||||
|
.filter(
|
||||||
|
(assignment) =>
|
||||||
|
assignment.expected.displayName !==
|
||||||
|
assignment.target.displayName
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(assignment) => persistedById.get(assignment.rowId)!.circuitId
|
||||||
|
),
|
||||||
|
input.command.payload.circuitDisplayNames
|
||||||
|
);
|
||||||
|
|
||||||
return inverse;
|
const { projectDeviceId, operation, rows } = input.command.payload;
|
||||||
|
return {
|
||||||
|
forward: createProjectDeviceRowSyncProjectCommand(
|
||||||
|
projectDeviceId,
|
||||||
|
operation,
|
||||||
|
rows,
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
|
inverse: createProjectDeviceRowSyncProjectCommand(
|
||||||
|
projectDevice.id,
|
||||||
|
invertProjectDeviceRowSyncOperation(operation),
|
||||||
|
rows.map((row) => ({
|
||||||
|
rowId: row.rowId,
|
||||||
|
expected: row.target,
|
||||||
|
target: row.expected,
|
||||||
|
})),
|
||||||
|
invertCircuitDisplayNameTransitions(circuitDisplayNames)
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertCircuitDisplayNameTransitions,
|
||||||
|
withCircuitDisplayNames,
|
||||||
|
type CircuitDisplayNameTransition,
|
||||||
|
} from "./circuit-display-name-sync.model.js";
|
||||||
import {
|
import {
|
||||||
assertCircuitInsertProjectCommand,
|
assertCircuitInsertProjectCommand,
|
||||||
circuitInsertCommandType,
|
circuitInsertCommandType,
|
||||||
|
|
@ -22,6 +27,7 @@ export interface CircuitDeviceRowMoveAssignment {
|
||||||
|
|
||||||
export interface CircuitDeviceRowMoveCommandPayload {
|
export interface CircuitDeviceRowMoveCommandPayload {
|
||||||
moves: CircuitDeviceRowMoveAssignment[];
|
moves: CircuitDeviceRowMoveAssignment[];
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitDeviceRowMoveProjectCommand
|
export interface CircuitDeviceRowMoveProjectCommand
|
||||||
|
|
@ -38,6 +44,7 @@ export interface CircuitDeviceRowMoveWithNewCircuitCommandPayload {
|
||||||
targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction;
|
targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction;
|
||||||
targetCircuit: CircuitSnapshot;
|
targetCircuit: CircuitSnapshot;
|
||||||
moves: CircuitDeviceRowMoveAssignment[];
|
moves: CircuitDeviceRowMoveAssignment[];
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitDeviceRowMoveWithNewCircuitProjectCommand
|
export interface CircuitDeviceRowMoveWithNewCircuitProjectCommand
|
||||||
|
|
@ -51,12 +58,13 @@ export type CircuitDeviceRowMoveHistoryProjectCommand =
|
||||||
| CircuitDeviceRowMoveWithNewCircuitProjectCommand;
|
| CircuitDeviceRowMoveWithNewCircuitProjectCommand;
|
||||||
|
|
||||||
export function createCircuitDeviceRowMoveProjectCommand(
|
export function createCircuitDeviceRowMoveProjectCommand(
|
||||||
moves: CircuitDeviceRowMoveAssignment[]
|
moves: CircuitDeviceRowMoveAssignment[],
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[]
|
||||||
): CircuitDeviceRowMoveProjectCommand {
|
): CircuitDeviceRowMoveProjectCommand {
|
||||||
const command: CircuitDeviceRowMoveProjectCommand = {
|
const command: CircuitDeviceRowMoveProjectCommand = {
|
||||||
schemaVersion: circuitDeviceRowMoveCommandSchemaVersion,
|
schemaVersion: circuitDeviceRowMoveCommandSchemaVersion,
|
||||||
type: circuitDeviceRowMoveCommandType,
|
type: circuitDeviceRowMoveCommandType,
|
||||||
payload: { moves },
|
payload: withCircuitDisplayNames({ moves }, circuitDisplayNames),
|
||||||
};
|
};
|
||||||
assertCircuitDeviceRowMoveProjectCommand(command);
|
assertCircuitDeviceRowMoveProjectCommand(command);
|
||||||
return command;
|
return command;
|
||||||
|
|
@ -65,16 +73,20 @@ export function createCircuitDeviceRowMoveProjectCommand(
|
||||||
export function createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
export function createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction,
|
targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction,
|
||||||
targetCircuit: CircuitSnapshot,
|
targetCircuit: CircuitSnapshot,
|
||||||
moves: CircuitDeviceRowMoveAssignment[]
|
moves: CircuitDeviceRowMoveAssignment[],
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[]
|
||||||
): CircuitDeviceRowMoveWithNewCircuitProjectCommand {
|
): CircuitDeviceRowMoveWithNewCircuitProjectCommand {
|
||||||
const command: CircuitDeviceRowMoveWithNewCircuitProjectCommand = {
|
const command: CircuitDeviceRowMoveWithNewCircuitProjectCommand = {
|
||||||
schemaVersion: circuitDeviceRowMoveCommandSchemaVersion,
|
schemaVersion: circuitDeviceRowMoveCommandSchemaVersion,
|
||||||
type: circuitDeviceRowMoveWithNewCircuitCommandType,
|
type: circuitDeviceRowMoveWithNewCircuitCommandType,
|
||||||
payload: {
|
payload: withCircuitDisplayNames(
|
||||||
targetCircuitAction,
|
{
|
||||||
targetCircuit,
|
targetCircuitAction,
|
||||||
moves,
|
targetCircuit,
|
||||||
},
|
moves,
|
||||||
|
},
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
};
|
};
|
||||||
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(command);
|
assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(command);
|
||||||
return command;
|
return command;
|
||||||
|
|
@ -135,6 +147,7 @@ export function assertCircuitDeviceRowMoveProjectCommand(
|
||||||
|
|
||||||
rowIds.add(move.rowId);
|
rowIds.add(move.rowId);
|
||||||
}
|
}
|
||||||
|
assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
export function assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
|
|
@ -205,6 +218,7 @@ export function assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertNonEmptyString(
|
function assertNonEmptyString(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// The frontend imports this model as a value, so it keeps type-only imports.
|
||||||
|
// The repository validates the stored circuitDisplayNames.
|
||||||
|
import type { CircuitDisplayNameTransition } from "./circuit-display-name-sync.model.js";
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
|
||||||
export const circuitDeviceRowUpdateCommandType =
|
export const circuitDeviceRowUpdateCommandType =
|
||||||
|
|
@ -40,6 +43,7 @@ export interface CircuitDeviceRowUpdateFieldChange<
|
||||||
export interface CircuitDeviceRowUpdateCommandPayload {
|
export interface CircuitDeviceRowUpdateCommandPayload {
|
||||||
rowId: string;
|
rowId: string;
|
||||||
changes: CircuitDeviceRowUpdateFieldChange[];
|
changes: CircuitDeviceRowUpdateFieldChange[];
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitDeviceRowUpdateProjectCommand
|
export interface CircuitDeviceRowUpdateProjectCommand
|
||||||
|
|
@ -50,7 +54,8 @@ export interface CircuitDeviceRowUpdateProjectCommand
|
||||||
|
|
||||||
export function createCircuitDeviceRowUpdateProjectCommand(
|
export function createCircuitDeviceRowUpdateProjectCommand(
|
||||||
rowId: string,
|
rowId: string,
|
||||||
patch: CircuitDeviceRowUpdatePatch
|
patch: CircuitDeviceRowUpdatePatch,
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[]
|
||||||
): CircuitDeviceRowUpdateProjectCommand {
|
): CircuitDeviceRowUpdateProjectCommand {
|
||||||
const normalizedPatch =
|
const normalizedPatch =
|
||||||
patch.quantity !== undefined && patch.manualQuantity === undefined
|
patch.quantity !== undefined && patch.manualQuantity === undefined
|
||||||
|
|
@ -70,7 +75,9 @@ export function createCircuitDeviceRowUpdateProjectCommand(
|
||||||
const command: CircuitDeviceRowUpdateProjectCommand = {
|
const command: CircuitDeviceRowUpdateProjectCommand = {
|
||||||
schemaVersion: circuitDeviceRowUpdateCommandSchemaVersion,
|
schemaVersion: circuitDeviceRowUpdateCommandSchemaVersion,
|
||||||
type: circuitDeviceRowUpdateCommandType,
|
type: circuitDeviceRowUpdateCommandType,
|
||||||
payload: { rowId, changes },
|
payload: circuitDisplayNames?.length
|
||||||
|
? { rowId, changes, circuitDisplayNames }
|
||||||
|
: { rowId, changes },
|
||||||
};
|
};
|
||||||
assertCircuitDeviceRowUpdateProjectCommand(command);
|
assertCircuitDeviceRowUpdateProjectCommand(command);
|
||||||
return command;
|
return command;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertCircuitDisplayNameTransitions,
|
||||||
|
withCircuitDisplayNames,
|
||||||
|
type CircuitDisplayNameTransition,
|
||||||
|
} from "./circuit-display-name-sync.model.js";
|
||||||
import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js";
|
import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js";
|
||||||
import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js";
|
import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js";
|
||||||
|
|
||||||
|
|
@ -40,12 +45,14 @@ export interface CircuitDeviceRowSnapshot {
|
||||||
export interface CircuitDeviceRowInsertCommandPayload {
|
export interface CircuitDeviceRowInsertCommandPayload {
|
||||||
row: CircuitDeviceRowSnapshot;
|
row: CircuitDeviceRowSnapshot;
|
||||||
externalObjects?: ExternalObjectLinkSnapshotTransition[];
|
externalObjects?: ExternalObjectLinkSnapshotTransition[];
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitDeviceRowDeleteCommandPayload {
|
export interface CircuitDeviceRowDeleteCommandPayload {
|
||||||
rowId: string;
|
rowId: string;
|
||||||
expectedCircuitId: string;
|
expectedCircuitId: string;
|
||||||
externalObjects?: ExternalObjectLinkSnapshotTransition[];
|
externalObjects?: ExternalObjectLinkSnapshotTransition[];
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircuitDeviceRowInsertProjectCommand
|
export interface CircuitDeviceRowInsertProjectCommand
|
||||||
|
|
@ -66,7 +73,8 @@ export type CircuitDeviceRowStructureProjectCommand =
|
||||||
|
|
||||||
export function createCircuitDeviceRowInsertProjectCommand(
|
export function createCircuitDeviceRowInsertProjectCommand(
|
||||||
row: CircuitDeviceRowSnapshot,
|
row: CircuitDeviceRowSnapshot,
|
||||||
externalObjects?: ExternalObjectLinkSnapshotTransition[]
|
externalObjects?: ExternalObjectLinkSnapshotTransition[],
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[]
|
||||||
): CircuitDeviceRowInsertProjectCommand {
|
): CircuitDeviceRowInsertProjectCommand {
|
||||||
const normalizedRow = {
|
const normalizedRow = {
|
||||||
...row,
|
...row,
|
||||||
|
|
@ -75,7 +83,10 @@ export function createCircuitDeviceRowInsertProjectCommand(
|
||||||
const command: CircuitDeviceRowInsertProjectCommand = {
|
const command: CircuitDeviceRowInsertProjectCommand = {
|
||||||
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
|
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
|
||||||
type: circuitDeviceRowInsertCommandType,
|
type: circuitDeviceRowInsertCommandType,
|
||||||
payload: { row: normalizedRow, ...(externalObjects?.length ? { externalObjects } : {}) },
|
payload: withCircuitDisplayNames(
|
||||||
|
{ row: normalizedRow, ...(externalObjects?.length ? { externalObjects } : {}) },
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
};
|
};
|
||||||
assertCircuitDeviceRowInsertProjectCommand(command);
|
assertCircuitDeviceRowInsertProjectCommand(command);
|
||||||
return command;
|
return command;
|
||||||
|
|
@ -84,12 +95,16 @@ export function createCircuitDeviceRowInsertProjectCommand(
|
||||||
export function createCircuitDeviceRowDeleteProjectCommand(
|
export function createCircuitDeviceRowDeleteProjectCommand(
|
||||||
rowId: string,
|
rowId: string,
|
||||||
expectedCircuitId: string,
|
expectedCircuitId: string,
|
||||||
externalObjects?: ExternalObjectLinkSnapshotTransition[]
|
externalObjects?: ExternalObjectLinkSnapshotTransition[],
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[]
|
||||||
): CircuitDeviceRowDeleteProjectCommand {
|
): CircuitDeviceRowDeleteProjectCommand {
|
||||||
const command: CircuitDeviceRowDeleteProjectCommand = {
|
const command: CircuitDeviceRowDeleteProjectCommand = {
|
||||||
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
|
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
|
||||||
type: circuitDeviceRowDeleteCommandType,
|
type: circuitDeviceRowDeleteCommandType,
|
||||||
payload: { rowId, expectedCircuitId, ...(externalObjects?.length ? { externalObjects } : {}) },
|
payload: withCircuitDisplayNames(
|
||||||
|
{ rowId, expectedCircuitId, ...(externalObjects?.length ? { externalObjects } : {}) },
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
};
|
};
|
||||||
assertCircuitDeviceRowDeleteProjectCommand(command);
|
assertCircuitDeviceRowDeleteProjectCommand(command);
|
||||||
return command;
|
return command;
|
||||||
|
|
@ -155,6 +170,7 @@ export function assertCircuitDeviceRowInsertProjectCommand(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertExternalObjectLinkTransitions(command.payload.externalObjects);
|
assertExternalObjectLinkTransitions(command.payload.externalObjects);
|
||||||
|
assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertCircuitDeviceRowDeleteProjectCommand(
|
export function assertCircuitDeviceRowDeleteProjectCommand(
|
||||||
|
|
@ -176,6 +192,7 @@ export function assertCircuitDeviceRowDeleteProjectCommand(
|
||||||
"expectedCircuitId"
|
"expectedCircuitId"
|
||||||
);
|
);
|
||||||
assertExternalObjectLinkTransitions(command.payload.externalObjects);
|
assertExternalObjectLinkTransitions(command.payload.externalObjects);
|
||||||
|
assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertExternalObjectLinkTransitions(value: unknown) {
|
export function assertExternalObjectLinkTransitions(value: unknown) {
|
||||||
|
|
|
||||||
99
src/domain/models/circuit-display-name-sync.model.ts
Normal file
99
src/domain/models/circuit-display-name-sync.model.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* A circuit with exactly one device row and no fed distribution board is
|
||||||
|
* shown as one compact row, so its own name is not visible or editable there.
|
||||||
|
* Its name therefore always follows that device row's name. Commands that
|
||||||
|
* change this state store the resulting circuit-name changes, so Undo/Redo
|
||||||
|
* restore them exactly, including a free name that was overwritten when only
|
||||||
|
* one device row remained.
|
||||||
|
*/
|
||||||
|
export interface CircuitDisplayNameTransition {
|
||||||
|
circuitId: string;
|
||||||
|
expected: string | null;
|
||||||
|
target: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name a circuit must carry, or `null` when its current name is
|
||||||
|
* already correct or freely editable.
|
||||||
|
*/
|
||||||
|
export function resolveSingleDeviceCircuitDisplayName(input: {
|
||||||
|
displayName: string | null;
|
||||||
|
deviceRowDisplayNames: readonly string[];
|
||||||
|
feedsDistributionBoard: boolean;
|
||||||
|
}): string | null {
|
||||||
|
if (
|
||||||
|
input.deviceRowDisplayNames.length !== 1 ||
|
||||||
|
input.feedsDistributionBoard
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const deviceRowDisplayName = input.deviceRowDisplayNames[0]!;
|
||||||
|
return input.displayName === deviceRowDisplayName
|
||||||
|
? null
|
||||||
|
: deviceRowDisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invertCircuitDisplayNameTransitions(
|
||||||
|
transitions: readonly CircuitDisplayNameTransition[]
|
||||||
|
): CircuitDisplayNameTransition[] {
|
||||||
|
return [...transitions].reverse().map((transition) => ({
|
||||||
|
circuitId: transition.circuitId,
|
||||||
|
expected: transition.target,
|
||||||
|
target: transition.expected,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the optional payload field only when it carries changes. */
|
||||||
|
export function withCircuitDisplayNames<Payload extends object>(
|
||||||
|
payload: Payload,
|
||||||
|
circuitDisplayNames: readonly CircuitDisplayNameTransition[] | undefined
|
||||||
|
): Payload & { circuitDisplayNames?: CircuitDisplayNameTransition[] } {
|
||||||
|
const { circuitDisplayNames: _previous, ...rest } = payload as Payload & {
|
||||||
|
circuitDisplayNames?: unknown;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
circuitDisplayNames?.length
|
||||||
|
? { ...rest, circuitDisplayNames: [...circuitDisplayNames] }
|
||||||
|
: rest
|
||||||
|
) as Payload & { circuitDisplayNames?: CircuitDisplayNameTransition[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertCircuitDisplayNameTransitions(
|
||||||
|
value: unknown
|
||||||
|
): asserts value is CircuitDisplayNameTransition[] | undefined {
|
||||||
|
if (value === undefined) return;
|
||||||
|
if (!Array.isArray(value) || value.length === 0) {
|
||||||
|
throw new Error("circuitDisplayNames must be a non-empty array.");
|
||||||
|
}
|
||||||
|
const circuitIds = new Set<string>();
|
||||||
|
for (const transition of value) {
|
||||||
|
if (
|
||||||
|
transition === null ||
|
||||||
|
typeof transition !== "object" ||
|
||||||
|
Array.isArray(transition)
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid circuit display-name transition.");
|
||||||
|
}
|
||||||
|
const { circuitId, expected, target } = transition as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
if (
|
||||||
|
typeof circuitId !== "string" ||
|
||||||
|
!circuitId.trim() ||
|
||||||
|
circuitIds.has(circuitId)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Circuit display-name transition has an invalid or duplicate circuit id."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(expected !== null && typeof expected !== "string") ||
|
||||||
|
(target !== null && typeof target !== "string") ||
|
||||||
|
expected === target
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid circuit display-name transition values.");
|
||||||
|
}
|
||||||
|
circuitIds.add(circuitId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,11 @@ import {
|
||||||
type DistributionBoardInfoField,
|
type DistributionBoardInfoField,
|
||||||
type DistributionBoardSupplyType,
|
type DistributionBoardSupplyType,
|
||||||
} from "../../shared/constants/distribution-board.js";
|
} from "../../shared/constants/distribution-board.js";
|
||||||
|
import {
|
||||||
|
assertCircuitDisplayNameTransitions,
|
||||||
|
withCircuitDisplayNames,
|
||||||
|
type CircuitDisplayNameTransition,
|
||||||
|
} from "./circuit-display-name-sync.model.js";
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
|
||||||
export const distributionBoardUpdateCommandType =
|
export const distributionBoardUpdateCommandType =
|
||||||
|
|
@ -39,6 +44,8 @@ export interface DistributionBoardUpdateFieldChange<
|
||||||
export interface DistributionBoardUpdateCommandPayload {
|
export interface DistributionBoardUpdateCommandPayload {
|
||||||
distributionBoardId: string;
|
distributionBoardId: string;
|
||||||
changes: DistributionBoardUpdateFieldChange[];
|
changes: DistributionBoardUpdateFieldChange[];
|
||||||
|
// Single-device feeding circuits that become compact rows take their row name.
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DistributionBoardUpdateProjectCommand
|
export interface DistributionBoardUpdateProjectCommand
|
||||||
|
|
@ -53,18 +60,22 @@ export function createDistributionBoardUpdateProjectCommand(
|
||||||
distributionBoardId: string,
|
distributionBoardId: string,
|
||||||
patch: DistributionBoardUpdatePatch,
|
patch: DistributionBoardUpdatePatch,
|
||||||
schemaVersion: DistributionBoardUpdateProjectCommand["schemaVersion"] =
|
schemaVersion: DistributionBoardUpdateProjectCommand["schemaVersion"] =
|
||||||
distributionBoardUpdateCommandSchemaVersion
|
distributionBoardUpdateCommandSchemaVersion,
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[]
|
||||||
): DistributionBoardUpdateProjectCommand {
|
): DistributionBoardUpdateProjectCommand {
|
||||||
const command: DistributionBoardUpdateProjectCommand = {
|
const command: DistributionBoardUpdateProjectCommand = {
|
||||||
schemaVersion,
|
schemaVersion,
|
||||||
type: distributionBoardUpdateCommandType,
|
type: distributionBoardUpdateCommandType,
|
||||||
payload: {
|
payload: withCircuitDisplayNames(
|
||||||
distributionBoardId,
|
{
|
||||||
changes: Object.entries(patch).map(([field, value]) => ({
|
distributionBoardId,
|
||||||
field: field as DistributionBoardUpdateField,
|
changes: Object.entries(patch).map(([field, value]) => ({
|
||||||
value,
|
field: field as DistributionBoardUpdateField,
|
||||||
})) as DistributionBoardUpdateFieldChange[],
|
value,
|
||||||
},
|
})) as DistributionBoardUpdateFieldChange[],
|
||||||
|
},
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
};
|
};
|
||||||
assertDistributionBoardUpdateProjectCommand(command);
|
assertDistributionBoardUpdateProjectCommand(command);
|
||||||
return command;
|
return command;
|
||||||
|
|
@ -154,6 +165,7 @@ export function assertDistributionBoardUpdateProjectCommand(
|
||||||
}
|
}
|
||||||
seen.add(change.field);
|
seen.add(change.field);
|
||||||
}
|
}
|
||||||
|
assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInfoField(field: string): field is DistributionBoardInfoField {
|
function isInfoField(field: string): field is DistributionBoardInfoField {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@ import {
|
||||||
circuitDeviceRowStructureCommandSchemaVersion,
|
circuitDeviceRowStructureCommandSchemaVersion,
|
||||||
type CircuitDeviceRowSnapshot,
|
type CircuitDeviceRowSnapshot,
|
||||||
} from "./circuit-device-row-structure-project-command.model.js";
|
} from "./circuit-device-row-structure-project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertCircuitDisplayNameTransitions,
|
||||||
|
invertCircuitDisplayNameTransitions,
|
||||||
|
withCircuitDisplayNames,
|
||||||
|
type CircuitDisplayNameTransition,
|
||||||
|
} from "./circuit-display-name-sync.model.js";
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js";
|
import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js";
|
||||||
|
|
||||||
|
|
@ -21,6 +27,7 @@ export interface ExternalObjectNewRowPayload {
|
||||||
target: ExternalModelObjectSnapshot;
|
target: ExternalModelObjectSnapshot;
|
||||||
}>;
|
}>;
|
||||||
confirmedConflictObjectIds: string[];
|
confirmedConflictObjectIds: string[];
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExternalObjectNewRowProjectCommand
|
export interface ExternalObjectNewRowProjectCommand
|
||||||
|
|
@ -38,14 +45,17 @@ export function createExternalObjectNewRowProjectCommand(
|
||||||
const command: ExternalObjectNewRowProjectCommand = {
|
const command: ExternalObjectNewRowProjectCommand = {
|
||||||
schemaVersion: externalObjectNewRowCommandSchemaVersion,
|
schemaVersion: externalObjectNewRowCommandSchemaVersion,
|
||||||
type,
|
type,
|
||||||
payload: {
|
payload: withCircuitDisplayNames(
|
||||||
row: {
|
{
|
||||||
...payload.row,
|
row: {
|
||||||
manualQuantity: payload.row.manualQuantity ?? payload.row.quantity,
|
...payload.row,
|
||||||
|
manualQuantity: payload.row.manualQuantity ?? payload.row.quantity,
|
||||||
|
},
|
||||||
|
objects: payload.objects,
|
||||||
|
confirmedConflictObjectIds: [...payload.confirmedConflictObjectIds],
|
||||||
},
|
},
|
||||||
objects: payload.objects,
|
payload.circuitDisplayNames
|
||||||
confirmedConflictObjectIds: [...payload.confirmedConflictObjectIds],
|
),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
assertExternalObjectNewRowProjectCommand(command);
|
assertExternalObjectNewRowProjectCommand(command);
|
||||||
return command;
|
return command;
|
||||||
|
|
@ -65,6 +75,9 @@ export function invertExternalObjectNewRowProjectCommand(
|
||||||
target: expected,
|
target: expected,
|
||||||
})),
|
})),
|
||||||
confirmedConflictObjectIds: command.payload.confirmedConflictObjectIds,
|
confirmedConflictObjectIds: command.payload.confirmedConflictObjectIds,
|
||||||
|
circuitDisplayNames: invertCircuitDisplayNameTransitions(
|
||||||
|
command.payload.circuitDisplayNames ?? []
|
||||||
|
),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +125,7 @@ export function assertExternalObjectNewRowProjectCommand(
|
||||||
}
|
}
|
||||||
confirmedIds.add(objectId);
|
confirmedIds.add(objectId);
|
||||||
}
|
}
|
||||||
|
assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRow(value: unknown): CircuitDeviceRowSnapshot {
|
function parseRow(value: unknown): CircuitDeviceRowSnapshot {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ import {
|
||||||
type CircuitDeviceRowUpdateField,
|
type CircuitDeviceRowUpdateField,
|
||||||
type CircuitDeviceRowUpdateValues,
|
type CircuitDeviceRowUpdateValues,
|
||||||
} from "./circuit-device-row-project-command.model.js";
|
} from "./circuit-device-row-project-command.model.js";
|
||||||
|
import {
|
||||||
|
assertCircuitDisplayNameTransitions,
|
||||||
|
withCircuitDisplayNames,
|
||||||
|
type CircuitDisplayNameTransition,
|
||||||
|
} from "./circuit-display-name-sync.model.js";
|
||||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||||
|
|
||||||
export const projectDeviceRowSyncCommandType =
|
export const projectDeviceRowSyncCommandType =
|
||||||
|
|
@ -47,6 +52,7 @@ export interface ProjectDeviceRowSyncCommandPayload {
|
||||||
projectDeviceId: string;
|
projectDeviceId: string;
|
||||||
operation: ProjectDeviceRowSyncOperation;
|
operation: ProjectDeviceRowSyncOperation;
|
||||||
rows: ProjectDeviceRowSyncAssignment[];
|
rows: ProjectDeviceRowSyncAssignment[];
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectDeviceRowSyncProjectCommand
|
export interface ProjectDeviceRowSyncProjectCommand
|
||||||
|
|
@ -58,12 +64,16 @@ export interface ProjectDeviceRowSyncProjectCommand
|
||||||
export function createProjectDeviceRowSyncProjectCommand(
|
export function createProjectDeviceRowSyncProjectCommand(
|
||||||
projectDeviceId: string,
|
projectDeviceId: string,
|
||||||
operation: ProjectDeviceRowSyncOperation,
|
operation: ProjectDeviceRowSyncOperation,
|
||||||
rows: ProjectDeviceRowSyncAssignment[]
|
rows: ProjectDeviceRowSyncAssignment[],
|
||||||
|
circuitDisplayNames?: CircuitDisplayNameTransition[]
|
||||||
): ProjectDeviceRowSyncProjectCommand {
|
): ProjectDeviceRowSyncProjectCommand {
|
||||||
const command: ProjectDeviceRowSyncProjectCommand = {
|
const command: ProjectDeviceRowSyncProjectCommand = {
|
||||||
schemaVersion: projectDeviceRowSyncCommandSchemaVersion,
|
schemaVersion: projectDeviceRowSyncCommandSchemaVersion,
|
||||||
type: projectDeviceRowSyncCommandType,
|
type: projectDeviceRowSyncCommandType,
|
||||||
payload: { projectDeviceId, operation, rows },
|
payload: withCircuitDisplayNames(
|
||||||
|
{ projectDeviceId, operation, rows },
|
||||||
|
circuitDisplayNames
|
||||||
|
),
|
||||||
};
|
};
|
||||||
assertProjectDeviceRowSyncProjectCommand(command);
|
assertProjectDeviceRowSyncProjectCommand(command);
|
||||||
return command;
|
return command;
|
||||||
|
|
@ -131,6 +141,7 @@ export function assertProjectDeviceRowSyncProjectCommand(
|
||||||
);
|
);
|
||||||
rowIds.add(row.rowId);
|
rowIds.add(row.rowId);
|
||||||
}
|
}
|
||||||
|
assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertProjectDeviceRowSyncOperation(
|
export function invertProjectDeviceRowSyncOperation(
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,8 @@ export async function assignExternalObjectsToNewCircuit(req: Request, res: Respo
|
||||||
circuitListId: sectionLocation.tree.circuitListId,
|
circuitListId: sectionLocation.tree.circuitListId,
|
||||||
sectionId,
|
sectionId,
|
||||||
equipmentIdentifier: await circuitNumberingService.getNextIdentifier(sectionId),
|
equipmentIdentifier: await circuitNumberingService.getNextIdentifier(sectionId),
|
||||||
displayName: null,
|
// The single external row is shown compactly, so the circuit carries its name.
|
||||||
|
displayName: row.displayName,
|
||||||
sortOrder: Math.max(0, ...sectionLocation.section.circuits.map((entry) => entry.sortOrder)) + 10,
|
sortOrder: Math.max(0, ...sectionLocation.section.circuits.map((entry) => entry.sortOrder)) + 10,
|
||||||
cableType: null, cableCrossSection: null, cableLength: null, rcdAssignment: null,
|
cableType: null, cableCrossSection: null, cableLength: null, rcdAssignment: null,
|
||||||
terminalDesignation: null,
|
terminalDesignation: null,
|
||||||
|
|
|
||||||
516
tests/single-device-circuit-name.test.ts
Normal file
516
tests/single-device-circuit-name.test.ts
Normal file
|
|
@ -0,0 +1,516 @@
|
||||||
|
import path from "node:path";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
|
import {
|
||||||
|
createDatabaseContext,
|
||||||
|
type DatabaseContext,
|
||||||
|
} from "../src/db/database-context.js";
|
||||||
|
import { CircuitDeviceRowMoveProjectCommandRepository } from "../src/db/repositories/circuit-device-row-move-project-command.repository.js";
|
||||||
|
import { CircuitDeviceRowProjectCommandRepository } from "../src/db/repositories/circuit-device-row-project-command.repository.js";
|
||||||
|
import { CircuitDeviceRowStructureProjectCommandRepository } from "../src/db/repositories/circuit-device-row-structure-project-command.repository.js";
|
||||||
|
import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-project-command.repository.js";
|
||||||
|
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
|
||||||
|
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
||||||
|
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
||||||
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
|
import { circuits } from "../src/db/schema/circuits.js";
|
||||||
|
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||||
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
|
import { resolveSingleDeviceCircuitDisplayName } from "../src/domain/models/circuit-display-name-sync.model.js";
|
||||||
|
import { createCircuitDeviceRowMoveWithNewCircuitProjectCommand } from "../src/domain/models/circuit-device-row-move-project-command.model.js";
|
||||||
|
import { createCircuitDeviceRowUpdateProjectCommand } from "../src/domain/models/circuit-device-row-project-command.model.js";
|
||||||
|
import {
|
||||||
|
createCircuitDeviceRowDeleteProjectCommand,
|
||||||
|
createCircuitDeviceRowInsertProjectCommand,
|
||||||
|
type CircuitDeviceRowSnapshot,
|
||||||
|
} from "../src/domain/models/circuit-device-row-structure-project-command.model.js";
|
||||||
|
import { createCircuitUpdateProjectCommand } from "../src/domain/models/circuit-project-command.model.js";
|
||||||
|
import {
|
||||||
|
createCircuitInsertProjectCommand,
|
||||||
|
type CircuitSnapshot,
|
||||||
|
} from "../src/domain/models/circuit-structure-project-command.model.js";
|
||||||
|
import { createDistributionBoardUpdateProjectCommand } from "../src/domain/models/distribution-board-project-command.model.js";
|
||||||
|
import type { SerializedProjectCommand } from "../src/domain/models/project-command.model.js";
|
||||||
|
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||||
|
|
||||||
|
interface Fixture {
|
||||||
|
context: DatabaseContext;
|
||||||
|
boardId: string;
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFixture(): Fixture {
|
||||||
|
const context = createDatabaseContext(":memory:");
|
||||||
|
migrate(context.db, {
|
||||||
|
migrationsFolder: path.resolve("src", "db", "migrations"),
|
||||||
|
});
|
||||||
|
context.db.insert(projects).values({ id: "project-1", name: "Projekt" }).run();
|
||||||
|
const boards = new DistributionBoardFixtureRepository(context.db);
|
||||||
|
const board = boards.createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||||
|
const section = context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.circuitListId, board.id))
|
||||||
|
.get();
|
||||||
|
assert.ok(section);
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
boardId: board.id,
|
||||||
|
circuitListId: section.circuitListId,
|
||||||
|
sectionId: section.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertCircuit(
|
||||||
|
fixture: Fixture,
|
||||||
|
id: string,
|
||||||
|
displayName: string | null,
|
||||||
|
rowNames: string[],
|
||||||
|
sortOrder = 10
|
||||||
|
) {
|
||||||
|
fixture.context.db
|
||||||
|
.insert(circuits)
|
||||||
|
.values({
|
||||||
|
id,
|
||||||
|
circuitListId: fixture.circuitListId,
|
||||||
|
sectionId: fixture.sectionId,
|
||||||
|
equipmentIdentifier: `-1F${sortOrder}`,
|
||||||
|
displayName,
|
||||||
|
sortOrder,
|
||||||
|
voltage: 230,
|
||||||
|
isReserve: rowNames.length === 0 ? 1 : 0,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
rowNames.forEach((rowName, index) => {
|
||||||
|
fixture.context.db
|
||||||
|
.insert(circuitDeviceRows)
|
||||||
|
.values(rowValues(`${id}-row-${index + 1}`, id, rowName, (index + 1) * 10))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowValues(
|
||||||
|
id: string,
|
||||||
|
circuitId: string,
|
||||||
|
displayName: string,
|
||||||
|
sortOrder: number
|
||||||
|
): CircuitDeviceRowSnapshot {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
circuitId,
|
||||||
|
linkedProjectDeviceId: null,
|
||||||
|
sortOrder,
|
||||||
|
name: displayName,
|
||||||
|
displayName,
|
||||||
|
phaseType: "single_phase",
|
||||||
|
connectionKind: null,
|
||||||
|
costGroup: null,
|
||||||
|
category: null,
|
||||||
|
level: null,
|
||||||
|
roomId: null,
|
||||||
|
roomNumberSnapshot: null,
|
||||||
|
roomNameSnapshot: null,
|
||||||
|
quantity: 1,
|
||||||
|
manualQuantity: 1,
|
||||||
|
powerPerUnit: 0.1,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
cosPhi: null,
|
||||||
|
remark: null,
|
||||||
|
overriddenFields: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function circuitName(fixture: Fixture, circuitId: string) {
|
||||||
|
return fixture.context.db
|
||||||
|
.select({ displayName: circuits.displayName })
|
||||||
|
.from(circuits)
|
||||||
|
.where(eq(circuits.id, circuitId))
|
||||||
|
.get()?.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryStore {
|
||||||
|
execute(input: {
|
||||||
|
projectId: string;
|
||||||
|
expectedRevision: number;
|
||||||
|
source: "user" | "undo" | "redo";
|
||||||
|
historyTargetChangeSetId?: string;
|
||||||
|
command: never;
|
||||||
|
}): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replays the stored history step like the project command service does. */
|
||||||
|
function replay(
|
||||||
|
fixture: Fixture,
|
||||||
|
store: HistoryStore,
|
||||||
|
direction: "undo" | "redo"
|
||||||
|
) {
|
||||||
|
const history = new ProjectHistoryRepository(fixture.context.db);
|
||||||
|
const step = history.getNextCommand("project-1", direction);
|
||||||
|
assert.ok(step);
|
||||||
|
store.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: history.getState("project-1")!.currentRevision,
|
||||||
|
source: direction,
|
||||||
|
historyTargetChangeSetId: step.changeSetId,
|
||||||
|
command: step.command as SerializedProjectCommand<unknown> as never,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentRevision(fixture: Fixture) {
|
||||||
|
return new ProjectHistoryRepository(fixture.context.db).getState("project-1")!
|
||||||
|
.currentRevision;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("single-device circuit name", () => {
|
||||||
|
it("follows the only device row unless a distribution board is fed", () => {
|
||||||
|
const resolve = (
|
||||||
|
displayName: string | null,
|
||||||
|
deviceRowDisplayNames: string[],
|
||||||
|
feedsDistributionBoard = false
|
||||||
|
) =>
|
||||||
|
resolveSingleDeviceCircuitDisplayName({
|
||||||
|
displayName,
|
||||||
|
deviceRowDisplayNames,
|
||||||
|
feedsDistributionBoard,
|
||||||
|
});
|
||||||
|
assert.equal(resolve("Neuer Stromkreis", ["Ersatzlast"]), "Ersatzlast");
|
||||||
|
assert.equal(resolve(null, ["Ersatzlast"]), "Ersatzlast");
|
||||||
|
assert.equal(resolve("Ersatzlast", ["Ersatzlast"]), null);
|
||||||
|
assert.equal(resolve("Frei", ["A", "B"]), null);
|
||||||
|
assert.equal(resolve("Reserve", []), null);
|
||||||
|
assert.equal(resolve("Zuleitung UV", ["A"], true), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("names a new single-device circuit after its row and keeps it on undo/redo", () => {
|
||||||
|
const fixture = createFixture();
|
||||||
|
try {
|
||||||
|
const store = new CircuitStructureProjectCommandRepository(fixture.context.db);
|
||||||
|
const snapshot: CircuitSnapshot = {
|
||||||
|
id: "circuit-new",
|
||||||
|
circuitListId: fixture.circuitListId,
|
||||||
|
sectionId: fixture.sectionId,
|
||||||
|
equipmentIdentifier: "-1F1",
|
||||||
|
displayName: "Neuer Stromkreis",
|
||||||
|
sortOrder: 10,
|
||||||
|
cableType: null,
|
||||||
|
cableCrossSection: null,
|
||||||
|
cableLength: null,
|
||||||
|
rcdAssignment: null,
|
||||||
|
terminalDesignation: null,
|
||||||
|
voltage: 230,
|
||||||
|
controlRequirement: null,
|
||||||
|
status: null,
|
||||||
|
isReserve: false,
|
||||||
|
remark: null,
|
||||||
|
deviceRows: [rowValues("row-new", "circuit-new", "Ersatzlast", 10)],
|
||||||
|
};
|
||||||
|
store.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitInsertProjectCommand(snapshot),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-new"), "Ersatzlast");
|
||||||
|
|
||||||
|
replay(fixture, store, "undo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-new"), undefined);
|
||||||
|
replay(fixture, store, "redo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-new"), "Ersatzlast");
|
||||||
|
} finally {
|
||||||
|
fixture.context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renames the circuit with its only row and restores both on undo", () => {
|
||||||
|
const fixture = createFixture();
|
||||||
|
try {
|
||||||
|
insertCircuit(fixture, "circuit-1", "Neuer Stromkreis", ["Gerät"]);
|
||||||
|
const store = new CircuitDeviceRowProjectCommandRepository(fixture.context.db);
|
||||||
|
store.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitDeviceRowUpdateProjectCommand("circuit-1-row-1", {
|
||||||
|
displayName: "Ersatzlast",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Ersatzlast");
|
||||||
|
|
||||||
|
replay(fixture, { execute: (input) => store.executeUpdate(input) }, "undo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Neuer Stromkreis");
|
||||||
|
replay(fixture, { execute: (input) => store.executeUpdate(input) }, "redo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Ersatzlast");
|
||||||
|
} finally {
|
||||||
|
fixture.context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves a multi-device circuit name free and does not touch it for other row edits", () => {
|
||||||
|
const fixture = createFixture();
|
||||||
|
try {
|
||||||
|
insertCircuit(fixture, "circuit-1", "Frei", ["A", "B"]);
|
||||||
|
const store = new CircuitDeviceRowProjectCommandRepository(fixture.context.db);
|
||||||
|
store.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitDeviceRowUpdateProjectCommand("circuit-1-row-1", {
|
||||||
|
displayName: "A neu",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Frei");
|
||||||
|
|
||||||
|
insertCircuit(fixture, "circuit-2", "Bewusst abweichend", ["C"], 20);
|
||||||
|
store.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitDeviceRowUpdateProjectCommand("circuit-2-row-1", {
|
||||||
|
quantity: 2,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-2"), "Bewusst abweichend");
|
||||||
|
} finally {
|
||||||
|
fixture.context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("takes the remaining row name after a delete and restores the free name on undo", () => {
|
||||||
|
const fixture = createFixture();
|
||||||
|
try {
|
||||||
|
insertCircuit(fixture, "circuit-1", "Gerät A", ["Gerät A"]);
|
||||||
|
const store = new CircuitDeviceRowStructureProjectCommandRepository(fixture.context.db);
|
||||||
|
store.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitDeviceRowInsertProjectCommand(
|
||||||
|
rowValues("row-b", "circuit-1", "Gerät B", 20)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Gerät A");
|
||||||
|
|
||||||
|
new CircuitProjectCommandRepository(fixture.context.db).executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitUpdateProjectCommand("circuit-1", {
|
||||||
|
displayName: "Küche",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
store.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 2,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitDeviceRowDeleteProjectCommand("circuit-1-row-1", "circuit-1"),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Gerät B");
|
||||||
|
|
||||||
|
replay(fixture, store, "undo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Küche");
|
||||||
|
replay(fixture, store, "redo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Gerät B");
|
||||||
|
assert.equal(currentRevision(fixture), 5);
|
||||||
|
} finally {
|
||||||
|
fixture.context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps stored legacy commands without name changes executable", () => {
|
||||||
|
const fixture = createFixture();
|
||||||
|
try {
|
||||||
|
insertCircuit(fixture, "circuit-1", "Küche", ["Gerät A", "Gerät B"]);
|
||||||
|
const store = new CircuitDeviceRowStructureProjectCommandRepository(fixture.context.db);
|
||||||
|
store.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitDeviceRowDeleteProjectCommand("circuit-1-row-1", "circuit-1"),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Gerät B");
|
||||||
|
const history = new ProjectHistoryRepository(fixture.context.db);
|
||||||
|
const step = history.getNextCommand("project-1", "undo");
|
||||||
|
assert.ok(step);
|
||||||
|
const legacyInverse = structuredClone(step.command) as SerializedProjectCommand<{
|
||||||
|
circuitDisplayNames?: unknown;
|
||||||
|
}>;
|
||||||
|
delete legacyInverse.payload.circuitDisplayNames;
|
||||||
|
store.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: step.changeSetId,
|
||||||
|
command: legacyInverse as never,
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Gerät B");
|
||||||
|
} finally {
|
||||||
|
fixture.context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("names a new circuit after its single moved row and syncs the source circuit", () => {
|
||||||
|
const fixture = createFixture();
|
||||||
|
try {
|
||||||
|
insertCircuit(fixture, "circuit-1", "Küche", ["Gerät A", "Gerät B"]);
|
||||||
|
const store = new CircuitDeviceRowMoveProjectCommandRepository(fixture.context.db);
|
||||||
|
store.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
|
||||||
|
"create",
|
||||||
|
{
|
||||||
|
id: "circuit-new",
|
||||||
|
circuitListId: fixture.circuitListId,
|
||||||
|
sectionId: fixture.sectionId,
|
||||||
|
equipmentIdentifier: "-1F99",
|
||||||
|
displayName: "Neuer Stromkreis",
|
||||||
|
sortOrder: 99,
|
||||||
|
cableType: null,
|
||||||
|
cableCrossSection: null,
|
||||||
|
cableLength: null,
|
||||||
|
rcdAssignment: null,
|
||||||
|
terminalDesignation: null,
|
||||||
|
voltage: null,
|
||||||
|
controlRequirement: null,
|
||||||
|
status: null,
|
||||||
|
isReserve: true,
|
||||||
|
remark: null,
|
||||||
|
deviceRows: [],
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
rowId: "circuit-1-row-2",
|
||||||
|
expectedCircuitId: "circuit-1",
|
||||||
|
expectedSortOrder: 20,
|
||||||
|
targetCircuitId: "circuit-new",
|
||||||
|
targetSortOrder: 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-new"), "Gerät B");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Gerät A");
|
||||||
|
|
||||||
|
replay(fixture, store, "undo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-new"), undefined);
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Küche");
|
||||||
|
replay(fixture, store, "redo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-new"), "Gerät B");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Gerät A");
|
||||||
|
} finally {
|
||||||
|
fixture.context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not rename a feeder circuit until its downstream board is unlinked", () => {
|
||||||
|
const fixture = createFixture();
|
||||||
|
try {
|
||||||
|
insertCircuit(fixture, "circuit-1", "Zuleitung UV-02", ["Gerät A"]);
|
||||||
|
const boards = new DistributionBoardFixtureRepository(fixture.context.db);
|
||||||
|
const downstream = boards.createWithCircuitListAndDefaultSections("project-1", "UV-02");
|
||||||
|
fixture.context.db
|
||||||
|
.update(distributionBoards)
|
||||||
|
.set({ feedingDistributionBoardId: fixture.boardId, feedingCircuitId: "circuit-1" })
|
||||||
|
.where(eq(distributionBoards.id, downstream.id))
|
||||||
|
.run();
|
||||||
|
|
||||||
|
const rowStore = new CircuitDeviceRowProjectCommandRepository(fixture.context.db);
|
||||||
|
rowStore.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitDeviceRowUpdateProjectCommand("circuit-1-row-1", {
|
||||||
|
displayName: "Gerät neu",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Zuleitung UV-02");
|
||||||
|
|
||||||
|
const boardStore = new DistributionBoardStructureProjectCommandRepository(fixture.context.db);
|
||||||
|
const execute = (input: Parameters<HistoryStore["execute"]>[0]) =>
|
||||||
|
boardStore.executeUpdate(input);
|
||||||
|
boardStore.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "user",
|
||||||
|
command: createDistributionBoardUpdateProjectCommand(downstream.id, {
|
||||||
|
feedingDistributionBoardId: null,
|
||||||
|
feedingCircuitId: null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Gerät neu");
|
||||||
|
|
||||||
|
replay(fixture, { execute }, "undo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Zuleitung UV-02");
|
||||||
|
replay(fixture, { execute }, "redo");
|
||||||
|
assert.equal(circuitName(fixture, "circuit-1"), "Gerät neu");
|
||||||
|
} finally {
|
||||||
|
fixture.context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("aligns existing single-device circuit names in migration 0016", () => {
|
||||||
|
const source = path.resolve("src", "db", "migrations");
|
||||||
|
const staging = mkdtempSync(path.join(tmpdir(), "leistungsbilanz-circuit-name-migration-"));
|
||||||
|
const journal = JSON.parse(readFileSync(path.join(source, "meta", "_journal.json"), "utf8"));
|
||||||
|
journal.entries = journal.entries.filter((entry: { idx: number }) => entry.idx <= 15);
|
||||||
|
mkdirSync(path.join(staging, "meta"));
|
||||||
|
writeFileSync(path.join(staging, "meta", "_journal.json"), JSON.stringify(journal));
|
||||||
|
for (const entry of journal.entries) {
|
||||||
|
copyFileSync(path.join(source, `${entry.tag}.sql`), path.join(staging, `${entry.tag}.sql`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = createDatabaseContext(":memory:");
|
||||||
|
try {
|
||||||
|
migrate(context.db, { migrationsFolder: staging });
|
||||||
|
context.sqlite.exec(`
|
||||||
|
INSERT INTO projects (id, name) VALUES ('p', 'Projekt');
|
||||||
|
INSERT INTO distribution_boards (id, project_id, name) VALUES ('b', 'p', 'UV');
|
||||||
|
INSERT INTO circuit_lists (id, project_id, distribution_board_id, name) VALUES ('l', 'p', 'b', 'Liste');
|
||||||
|
INSERT INTO circuit_sections (id, circuit_list_id, key, display_name, prefix) VALUES ('s', 'l', 'single_phase', 'Einphasig', '-2F1.');
|
||||||
|
INSERT INTO circuits (id, circuit_list_id, section_id, equipment_identifier, display_name) VALUES
|
||||||
|
('single', 'l', 's', '-2F1.1', 'Neuer Stromkreis'),
|
||||||
|
('unnamed', 'l', 's', '-2F1.2', NULL),
|
||||||
|
('multi', 'l', 's', '-2F1.3', 'Küche'),
|
||||||
|
('reserve', 'l', 's', '-2F1.4', 'Reserve'),
|
||||||
|
('feeder', 'l', 's', '-2F1.5', 'Zuleitung UV-02');
|
||||||
|
INSERT INTO circuit_device_rows
|
||||||
|
(id, circuit_id, name, display_name, quantity, manual_quantity, power_per_unit, simultaneity_factor)
|
||||||
|
VALUES
|
||||||
|
('r1', 'single', 'Gerät', 'Ersatzlast', 1, 1, 1, 1),
|
||||||
|
('r2', 'unnamed', 'Gerät', 'Leuchte', 1, 1, 1, 1),
|
||||||
|
('r3', 'multi', 'Gerät', 'A', 1, 1, 1, 1),
|
||||||
|
('r4', 'multi', 'Gerät', 'B', 1, 1, 1, 1),
|
||||||
|
('r5', 'feeder', 'Gerät', 'Pumpe', 1, 1, 1, 1);
|
||||||
|
INSERT INTO distribution_boards (id, project_id, name, feeding_distribution_board_id, feeding_circuit_id)
|
||||||
|
VALUES ('b2', 'p', 'UV-02', 'b', 'feeder');
|
||||||
|
`);
|
||||||
|
|
||||||
|
migrate(context.db, { migrationsFolder: source });
|
||||||
|
|
||||||
|
const names = Object.fromEntries(
|
||||||
|
(context.sqlite.prepare("SELECT id, display_name FROM circuits").all() as Array<{
|
||||||
|
id: string;
|
||||||
|
display_name: string | null;
|
||||||
|
}>).map((row) => [row.id, row.display_name])
|
||||||
|
);
|
||||||
|
assert.deepEqual(names, {
|
||||||
|
single: "Ersatzlast",
|
||||||
|
unnamed: "Leuchte",
|
||||||
|
multi: "Küche",
|
||||||
|
reserve: "Reserve",
|
||||||
|
feeder: "Zuleitung UV-02",
|
||||||
|
});
|
||||||
|
assert.deepEqual(context.sqlite.pragma("foreign_key_check"), []);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
rmSync(staging, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue