Persist populated group deletion
This commit is contained in:
@@ -326,6 +326,12 @@ groups of the same category. The stored target BMK is highest target suffix
|
||||
plus one at planning time and is never recalculated for Redo. Only circuit
|
||||
group, BMK and sort order change; device rows and circuit protection retain the
|
||||
stable circuit id.
|
||||
Confirmed populated-group deletion uses
|
||||
`circuit-group.delete-subtree`/`circuit-group.restore-subtree`. The exact
|
||||
snapshot contains the group, optional component protections, complete circuits,
|
||||
circuit protections and all device-row link/override metadata. Delete
|
||||
re-captures and compares the subtree before cascading; restore preserves every
|
||||
UUID in foreign-key-safe order.
|
||||
Distribution-board floor assignment and a project-enabled supply type use
|
||||
`distribution-board.update`; both values are snapshot/export fields and one
|
||||
persistent undo step.
|
||||
|
||||
@@ -336,7 +336,13 @@ Gruppenoperationen. Er umfasst Gruppe, optionale Gruppenkomponenten samt
|
||||
1:1-Schutzdaten, vollständige Stromkreise samt 1:1-Schutzdaten und sämtliche
|
||||
Gerätezeilen einschließlich Link- und Override-Metadaten. Seine
|
||||
Warnzusammenfassung liefert Vorsicherung/FI sowie Stromkreis- und
|
||||
Gerätezeilenanzahl. Persistentes Löschen/Wiederherstellen folgt separat.
|
||||
Gerätezeilenanzahl.
|
||||
`circuit-group.delete-subtree` nimmt den aktuellen Unterbaum unmittelbar vor
|
||||
dem Löschen erneut auf und verlangt exakte Übereinstimmung mit dem bestätigten
|
||||
Snapshot. `circuit-group.restore-subtree` schreibt ihn in FK-sicherer Reihenfolge
|
||||
mit denselben UUIDs zurück. Beide Richtungen laufen als vollständige inverse
|
||||
Projekt-Commands; späte Fehler hinterlassen weder Teillöschungen noch
|
||||
Teilwiederherstellungen.
|
||||
`distribution-board.update` versioniert Etage, Netzart und den
|
||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||
|
||||
@@ -30,7 +30,7 @@ requirements and intended sequencing, not proof of implementation.
|
||||
- [x] Phase D2a: deterministic same-category circuit-move plan.
|
||||
- [x] Phase D2b: persistent same-category circuit-move command.
|
||||
- [x] Phase D3a: complete validated group-subtree snapshot and warning summary.
|
||||
- [ ] Phase D3b: persistent populated-group delete/restore command.
|
||||
- [x] Phase D3b: persistent populated-group delete/restore command.
|
||||
- [ ] Phase E: editor projection and editing.
|
||||
- [ ] Phase F: documentation and full GUI verification.
|
||||
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
|
||||
|
||||
@@ -662,9 +662,7 @@ Acceptance:
|
||||
|
||||
### D. Group Numbering and Circuit Moves
|
||||
|
||||
Status: In progress. Group renumbering D1 and deterministic circuit-move
|
||||
planning and persistent moves D2 plus the deletion snapshot D3a are complete;
|
||||
persistent populated deletion D3b remains pending.
|
||||
Status: Complete.
|
||||
|
||||
- implement nested identifier generation
|
||||
- support same-category cross-group circuit moves
|
||||
@@ -721,6 +719,16 @@ Implemented in D3a:
|
||||
- the snapshot derives the confirmation summary for upstream protection, RCD,
|
||||
circuit count and device-row count
|
||||
|
||||
Implemented in D3b:
|
||||
|
||||
- `circuit-group.delete-subtree` requires the exact previously confirmed
|
||||
snapshot and re-captures the full current subtree before deletion
|
||||
- `circuit-group.restore-subtree` restores stable UUIDs in foreign-key-safe
|
||||
order, including both protection layers and row link/override metadata
|
||||
- deletion and restoration are exact persistent inverses for Undo/Redo
|
||||
- stale confirmation state and late history failures leave no partial delete
|
||||
or restore
|
||||
|
||||
Acceptance:
|
||||
|
||||
- target identifiers use highest suffix plus one
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import {
|
||||
circuitGroupDeleteSubtreeCommandType,
|
||||
circuitGroupRestoreSubtreeCommandType,
|
||||
assertCircuitGroupDeleteSubtreeProjectCommand,
|
||||
assertCircuitGroupRestoreSubtreeProjectCommand,
|
||||
createCircuitGroupDeleteSubtreeProjectCommand,
|
||||
createCircuitGroupRestoreSubtreeProjectCommand,
|
||||
type CircuitGroupSubtreeProjectCommand,
|
||||
} from "../../domain/models/circuit-group-subtree-project-command.model.js";
|
||||
import type {
|
||||
CircuitGroupSubtreeSnapshot,
|
||||
CircuitProtectionDeviceSnapshot,
|
||||
} from "../../domain/models/circuit-group-subtree-snapshot.model.js";
|
||||
import type {
|
||||
CircuitGroupSubtreeProjectCommandStore,
|
||||
ExecuteCircuitGroupSubtreeCommandInput,
|
||||
} from "../../domain/ports/circuit-group-subtree-project-command.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
|
||||
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
||||
export class CircuitGroupSubtreeProjectCommandRepository
|
||||
implements CircuitGroupSubtreeProjectCommandStore
|
||||
{
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
capture(projectId: string, groupId: string) {
|
||||
return this.captureFrom(this.database, projectId, groupId);
|
||||
}
|
||||
|
||||
execute(input: ExecuteCircuitGroupSubtreeCommandInput) {
|
||||
if (input.command.type === circuitGroupDeleteSubtreeCommandType) {
|
||||
assertCircuitGroupDeleteSubtreeProjectCommand(input.command);
|
||||
} else {
|
||||
assertCircuitGroupRestoreSubtreeProjectCommand(input.command);
|
||||
}
|
||||
return executeProjectCommandTransaction(
|
||||
this.database,
|
||||
input,
|
||||
(tx) => this.applyCommand(tx, input.projectId, input.command)
|
||||
);
|
||||
}
|
||||
|
||||
private applyCommand(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
command: CircuitGroupSubtreeProjectCommand
|
||||
) {
|
||||
if (command.type === circuitGroupDeleteSubtreeCommandType) {
|
||||
const current = this.captureFrom(
|
||||
database,
|
||||
projectId,
|
||||
command.payload.snapshot.group.id
|
||||
);
|
||||
if (!sameSnapshot(current, command.payload.snapshot)) {
|
||||
throw new Error(
|
||||
"Circuit-group subtree changed before deletion."
|
||||
);
|
||||
}
|
||||
const deleted = database
|
||||
.delete(circuitSections)
|
||||
.where(eq(circuitSections.id, current.group.id))
|
||||
.run();
|
||||
if (deleted.changes !== 1) {
|
||||
throw new Error("Circuit-group subtree could not be deleted.");
|
||||
}
|
||||
return createCircuitGroupRestoreSubtreeProjectCommand(
|
||||
command.payload.snapshot
|
||||
);
|
||||
}
|
||||
if (command.type === circuitGroupRestoreSubtreeCommandType) {
|
||||
this.restore(database, projectId, command.payload.snapshot);
|
||||
return createCircuitGroupDeleteSubtreeProjectCommand(
|
||||
command.payload.snapshot
|
||||
);
|
||||
}
|
||||
throw new Error("Unsupported circuit-group subtree command.");
|
||||
}
|
||||
|
||||
private captureFrom(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
groupId: string
|
||||
): CircuitGroupSubtreeSnapshot {
|
||||
const group = database
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.id, groupId))
|
||||
.get();
|
||||
if (!group || group.category === null || group.groupNumber === null) {
|
||||
throw new Error("Circuit group not found.");
|
||||
}
|
||||
this.assertListOwnership(database, projectId, group.circuitListId);
|
||||
const components = database
|
||||
.select()
|
||||
.from(distributionBoardComponents)
|
||||
.where(eq(distributionBoardComponents.sectionId, group.id))
|
||||
.orderBy(
|
||||
asc(distributionBoardComponents.sortOrder),
|
||||
asc(distributionBoardComponents.id)
|
||||
)
|
||||
.all()
|
||||
.map((component) => ({
|
||||
component: {
|
||||
...component,
|
||||
role: component.role as
|
||||
| "group_upstream_protection"
|
||||
| "group_residual_current_protection",
|
||||
},
|
||||
protectionDevice:
|
||||
database
|
||||
.select()
|
||||
.from(distributionBoardComponentProtectionDevices)
|
||||
.where(
|
||||
eq(
|
||||
distributionBoardComponentProtectionDevices.componentId,
|
||||
component.id
|
||||
)
|
||||
)
|
||||
.get() ?? null,
|
||||
}));
|
||||
const circuitSnapshots = database
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.sectionId, group.id))
|
||||
.orderBy(asc(circuits.sortOrder), asc(circuits.id))
|
||||
.all()
|
||||
.map((circuit) => {
|
||||
const { isReserve, ...values } = circuit;
|
||||
return {
|
||||
circuit: {
|
||||
...values,
|
||||
isReserve: Boolean(isReserve),
|
||||
deviceRows: database
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.circuitId, circuit.id))
|
||||
.orderBy(
|
||||
asc(circuitDeviceRows.sortOrder),
|
||||
asc(circuitDeviceRows.id)
|
||||
)
|
||||
.all(),
|
||||
},
|
||||
protectionDevice:
|
||||
(database
|
||||
.select()
|
||||
.from(circuitProtectionDevices)
|
||||
.where(eq(circuitProtectionDevices.circuitId, circuit.id))
|
||||
.get() as CircuitProtectionDeviceSnapshot | undefined) ??
|
||||
null,
|
||||
};
|
||||
});
|
||||
return {
|
||||
group: {
|
||||
...group,
|
||||
category: group.category,
|
||||
groupNumber: group.groupNumber,
|
||||
},
|
||||
components,
|
||||
circuits: circuitSnapshots,
|
||||
};
|
||||
}
|
||||
|
||||
private restore(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
snapshot: CircuitGroupSubtreeSnapshot
|
||||
) {
|
||||
this.assertListOwnership(
|
||||
database,
|
||||
projectId,
|
||||
snapshot.group.circuitListId
|
||||
);
|
||||
const existing = database
|
||||
.select({ id: circuitSections.id })
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.id, snapshot.group.id))
|
||||
.get();
|
||||
if (existing) {
|
||||
throw new Error("Circuit group already exists before restoration.");
|
||||
}
|
||||
database.insert(circuitSections).values(snapshot.group).run();
|
||||
for (const component of snapshot.components) {
|
||||
database
|
||||
.insert(distributionBoardComponents)
|
||||
.values(component.component)
|
||||
.run();
|
||||
if (component.protectionDevice !== null) {
|
||||
database
|
||||
.insert(distributionBoardComponentProtectionDevices)
|
||||
.values(component.protectionDevice)
|
||||
.run();
|
||||
}
|
||||
}
|
||||
for (const entry of snapshot.circuits) {
|
||||
const { deviceRows, isReserve, ...circuit } = entry.circuit;
|
||||
database
|
||||
.insert(circuits)
|
||||
.values({ ...circuit, isReserve: isReserve ? 1 : 0 })
|
||||
.run();
|
||||
if (entry.protectionDevice !== null) {
|
||||
database
|
||||
.insert(circuitProtectionDevices)
|
||||
.values(entry.protectionDevice)
|
||||
.run();
|
||||
}
|
||||
if (deviceRows.length > 0) {
|
||||
database.insert(circuitDeviceRows).values(deviceRows).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private assertListOwnership(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
circuitListId: string
|
||||
) {
|
||||
const list = database
|
||||
.select({ projectId: circuitLists.projectId })
|
||||
.from(circuitLists)
|
||||
.where(eq(circuitLists.id, circuitListId))
|
||||
.get();
|
||||
if (!list || list.projectId !== projectId) {
|
||||
throw new Error("Circuit-group list does not belong to project.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sameSnapshot(
|
||||
left: CircuitGroupSubtreeSnapshot,
|
||||
right: CircuitGroupSubtreeSnapshot
|
||||
) {
|
||||
return canonicalJson(left) === canonicalJson(right);
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(canonicalJson).join(",")}]`;
|
||||
}
|
||||
if (value !== null && typeof value === "object") {
|
||||
return `{${Object.entries(value)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(
|
||||
([key, child]) =>
|
||||
`${JSON.stringify(key)}:${canonicalJson(child)}`
|
||||
)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value) ?? "null";
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
assertCircuitGroupSubtreeSnapshot,
|
||||
type CircuitGroupSubtreeSnapshot,
|
||||
} from "./circuit-group-subtree-snapshot.model.js";
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
|
||||
export const circuitGroupDeleteSubtreeCommandType =
|
||||
"circuit-group.delete-subtree" as const;
|
||||
export const circuitGroupRestoreSubtreeCommandType =
|
||||
"circuit-group.restore-subtree" as const;
|
||||
export const circuitGroupSubtreeCommandSchemaVersion = 1 as const;
|
||||
|
||||
interface Payload {
|
||||
snapshot: CircuitGroupSubtreeSnapshot;
|
||||
}
|
||||
|
||||
export interface CircuitGroupDeleteSubtreeProjectCommand
|
||||
extends SerializedProjectCommand<Payload> {
|
||||
schemaVersion: typeof circuitGroupSubtreeCommandSchemaVersion;
|
||||
type: typeof circuitGroupDeleteSubtreeCommandType;
|
||||
}
|
||||
|
||||
export interface CircuitGroupRestoreSubtreeProjectCommand
|
||||
extends SerializedProjectCommand<Payload> {
|
||||
schemaVersion: typeof circuitGroupSubtreeCommandSchemaVersion;
|
||||
type: typeof circuitGroupRestoreSubtreeCommandType;
|
||||
}
|
||||
|
||||
export type CircuitGroupSubtreeProjectCommand =
|
||||
| CircuitGroupDeleteSubtreeProjectCommand
|
||||
| CircuitGroupRestoreSubtreeProjectCommand;
|
||||
|
||||
export function createCircuitGroupDeleteSubtreeProjectCommand(
|
||||
snapshot: CircuitGroupSubtreeSnapshot
|
||||
): CircuitGroupDeleteSubtreeProjectCommand {
|
||||
const command: CircuitGroupDeleteSubtreeProjectCommand = {
|
||||
schemaVersion: circuitGroupSubtreeCommandSchemaVersion,
|
||||
type: circuitGroupDeleteSubtreeCommandType,
|
||||
payload: { snapshot },
|
||||
};
|
||||
assertCircuitGroupDeleteSubtreeProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function createCircuitGroupRestoreSubtreeProjectCommand(
|
||||
snapshot: CircuitGroupSubtreeSnapshot
|
||||
): CircuitGroupRestoreSubtreeProjectCommand {
|
||||
const command: CircuitGroupRestoreSubtreeProjectCommand = {
|
||||
schemaVersion: circuitGroupSubtreeCommandSchemaVersion,
|
||||
type: circuitGroupRestoreSubtreeCommandType,
|
||||
payload: { snapshot },
|
||||
};
|
||||
assertCircuitGroupRestoreSubtreeProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function assertCircuitGroupDeleteSubtreeProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is CircuitGroupDeleteSubtreeProjectCommand {
|
||||
assertCommand(command, circuitGroupDeleteSubtreeCommandType);
|
||||
}
|
||||
|
||||
export function assertCircuitGroupRestoreSubtreeProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is CircuitGroupRestoreSubtreeProjectCommand {
|
||||
assertCommand(command, circuitGroupRestoreSubtreeCommandType);
|
||||
}
|
||||
|
||||
function assertCommand(
|
||||
command: SerializedProjectCommand<unknown>,
|
||||
type:
|
||||
| typeof circuitGroupDeleteSubtreeCommandType
|
||||
| typeof circuitGroupRestoreSubtreeCommandType
|
||||
) {
|
||||
if (
|
||||
command.schemaVersion !== circuitGroupSubtreeCommandSchemaVersion ||
|
||||
command.type !== type ||
|
||||
!isPlainObject(command.payload) ||
|
||||
Object.keys(command.payload).length !== 1
|
||||
) {
|
||||
throw new Error("Unsupported circuit-group subtree command.");
|
||||
}
|
||||
assertCircuitGroupSubtreeSnapshot(command.payload.snapshot);
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { CircuitGroupSubtreeSnapshot } from "../models/circuit-group-subtree-snapshot.model.js";
|
||||
import type { CircuitGroupSubtreeProjectCommand } from "../models/circuit-group-subtree-project-command.model.js";
|
||||
import type {
|
||||
AppendedProjectRevision,
|
||||
ProjectRevisionSource,
|
||||
} from "./project-revision.store.js";
|
||||
|
||||
export interface ExecuteCircuitGroupSubtreeCommandInput {
|
||||
projectId: string;
|
||||
expectedRevision: number;
|
||||
source: ProjectRevisionSource;
|
||||
description?: string;
|
||||
actorId?: string;
|
||||
historyTargetChangeSetId?: string;
|
||||
command: CircuitGroupSubtreeProjectCommand;
|
||||
}
|
||||
|
||||
export interface CircuitGroupSubtreeProjectCommandStore {
|
||||
capture(projectId: string, groupId: string): CircuitGroupSubtreeSnapshot;
|
||||
execute(input: ExecuteCircuitGroupSubtreeCommandInput): {
|
||||
revision: AppendedProjectRevision;
|
||||
inverse: CircuitGroupSubtreeProjectCommand;
|
||||
};
|
||||
}
|
||||
@@ -101,6 +101,7 @@ import type { CircuitStructureProjectCommandStore } from "../ports/circuit-struc
|
||||
import type { CircuitGroupStructureProjectCommandStore } from "../ports/circuit-group-structure-project-command.store.js";
|
||||
import type { CircuitGroupRenumberProjectCommandStore } from "../ports/circuit-group-renumber-project-command.store.js";
|
||||
import type { CircuitGroupMoveProjectCommandStore } from "../ports/circuit-group-move-project-command.store.js";
|
||||
import type { CircuitGroupSubtreeProjectCommandStore } from "../ports/circuit-group-subtree-project-command.store.js";
|
||||
import type { DistributionBoardStructureProjectCommandStore } from "../ports/distribution-board-structure-project-command.store.js";
|
||||
import type { DistributionBoardComponentStructureProjectCommandStore } from "../ports/distribution-board-component-structure-project-command.store.js";
|
||||
import type {
|
||||
@@ -131,6 +132,12 @@ import {
|
||||
assertCircuitGroupMoveProjectCommand,
|
||||
circuitGroupMoveCommandType,
|
||||
} from "../models/circuit-group-move-project-command.model.js";
|
||||
import {
|
||||
assertCircuitGroupDeleteSubtreeProjectCommand,
|
||||
assertCircuitGroupRestoreSubtreeProjectCommand,
|
||||
circuitGroupDeleteSubtreeCommandType,
|
||||
circuitGroupRestoreSubtreeCommandType,
|
||||
} from "../models/circuit-group-subtree-project-command.model.js";
|
||||
import type { ProjectLocationStructureProjectCommandStore } from "../ports/project-location-structure-project-command.store.js";
|
||||
import type { ProjectDeviceProjectCommandStore } from "../ports/project-device-project-command.store.js";
|
||||
import type { ProjectDeviceRowSyncProjectCommandStore } from "../ports/project-device-row-sync-project-command.store.js";
|
||||
@@ -169,6 +176,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
private readonly circuitGroupStructureStore: CircuitGroupStructureProjectCommandStore,
|
||||
private readonly circuitGroupRenumberStore: CircuitGroupRenumberProjectCommandStore,
|
||||
private readonly circuitGroupMoveStore: CircuitGroupMoveProjectCommandStore,
|
||||
private readonly circuitGroupSubtreeStore: CircuitGroupSubtreeProjectCommandStore,
|
||||
private readonly historyStore: ProjectHistoryStore
|
||||
) {}
|
||||
|
||||
@@ -399,6 +407,20 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case circuitGroupDeleteSubtreeCommandType: {
|
||||
assertCircuitGroupDeleteSubtreeProjectCommand(input.command);
|
||||
return this.circuitGroupSubtreeStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case circuitGroupRestoreSubtreeCommandType: {
|
||||
assertCircuitGroupRestoreSubtreeProjectCommand(input.command);
|
||||
return this.circuitGroupSubtreeStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectFloorInsertCommandType: {
|
||||
assertProjectFloorInsertProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({
|
||||
|
||||
@@ -42,6 +42,8 @@ const commandTypeLabels: Record<string, string> = {
|
||||
"circuit-group.reorder": "Stromkreisgruppen sortiert",
|
||||
"circuit-group.renumber": "Stromkreisgruppen neu nummeriert",
|
||||
"circuit.move-group": "Stromkreis in andere Gruppe verschoben",
|
||||
"circuit-group.delete-subtree": "Stromkreisgruppe vollständig entfernt",
|
||||
"circuit-group.restore-subtree": "Stromkreisgruppe vollständig wiederhergestellt",
|
||||
"project-floor.insert": "Geschoss angelegt",
|
||||
"project-floor.delete": "Geschoss entfernt",
|
||||
"project-room.insert": "Raum angelegt",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CircuitStructureProjectCommandRepository } from "../../db/repositories/
|
||||
import { CircuitGroupStructureProjectCommandRepository } from "../../db/repositories/circuit-group-structure-project-command.repository.js";
|
||||
import { CircuitGroupRenumberProjectCommandRepository } from "../../db/repositories/circuit-group-renumber-project-command.repository.js";
|
||||
import { CircuitGroupMoveProjectCommandRepository } from "../../db/repositories/circuit-group-move-project-command.repository.js";
|
||||
import { CircuitGroupSubtreeProjectCommandRepository } from "../../db/repositories/circuit-group-subtree-project-command.repository.js";
|
||||
import { DistributionBoardStructureProjectCommandRepository } from "../../db/repositories/distribution-board-structure-project-command.repository.js";
|
||||
import { DistributionBoardComponentStructureProjectCommandRepository } from "../../db/repositories/distribution-board-component-structure-project-command.repository.js";
|
||||
import { ProjectHistoryRepository } from "../../db/repositories/project-history.repository.js";
|
||||
@@ -35,6 +36,8 @@ export const circuitGroupRenumberProjectCommandStore =
|
||||
new CircuitGroupRenumberProjectCommandRepository(db);
|
||||
export const circuitGroupMoveProjectCommandStore =
|
||||
new CircuitGroupMoveProjectCommandRepository(db);
|
||||
export const circuitGroupSubtreeProjectCommandStore =
|
||||
new CircuitGroupSubtreeProjectCommandRepository(db);
|
||||
export const distributionBoardStructureProjectCommandStore =
|
||||
new DistributionBoardStructureProjectCommandRepository(db);
|
||||
export const distributionBoardComponentStructureProjectCommandStore =
|
||||
@@ -75,5 +78,6 @@ export const projectCommandService = new ProjectCommandService(
|
||||
circuitGroupStructureProjectCommandStore,
|
||||
circuitGroupRenumberProjectCommandStore,
|
||||
circuitGroupMoveProjectCommandStore,
|
||||
circuitGroupSubtreeProjectCommandStore,
|
||||
projectHistoryStore
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import "./circuit-group-renumber-project-command.repository.test.js";
|
||||
import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js";
|
||||
import "./circuit-group-move-project-command.repository.test.js";
|
||||
import "./circuit-group-subtree-snapshot.test.js";
|
||||
import "./circuit-group-subtree-project-command.repository.test.js";
|
||||
|
||||
describe("circuit group numbering", () => {
|
||||
it("formats the agreed identifiers including the leading hyphen", () => {
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import path from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
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 { CircuitGroupSubtreeProjectCommandRepository } from "../src/db/repositories/circuit-group-subtree-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 { circuitLists } from "../src/db/schema/circuit-lists.js";
|
||||
import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js";
|
||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||
import { circuits } from "../src/db/schema/circuits.js";
|
||||
import { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js";
|
||||
import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js";
|
||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||
import { projects } from "../src/db/schema/projects.js";
|
||||
import { createCircuitGroupDeleteSubtreeProjectCommand } from "../src/domain/models/circuit-group-subtree-project-command.model.js";
|
||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||
|
||||
function createTestDatabase(): DatabaseContext {
|
||||
const context = createDatabaseContext(":memory:");
|
||||
migrate(context.db, {
|
||||
migrationsFolder: path.resolve("src", "db", "migrations"),
|
||||
});
|
||||
context.db
|
||||
.insert(projects)
|
||||
.values({ id: "project-1", name: "Projekt" })
|
||||
.run();
|
||||
new DistributionBoardFixtureRepository(
|
||||
context.db
|
||||
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
||||
const list = context.db.select().from(circuitLists).get()!;
|
||||
const group = context.db
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.category, "lighting"))
|
||||
.get()!;
|
||||
context.db
|
||||
.insert(distributionBoardComponents)
|
||||
.values({
|
||||
id: "subtree-rcd",
|
||||
circuitListId: list.id,
|
||||
sectionId: group.id,
|
||||
equipmentIdentifier: "-1Q1.0",
|
||||
name: "Gruppen-FI",
|
||||
role: "group_residual_current_protection",
|
||||
placement: "group",
|
||||
sortOrder: 10,
|
||||
})
|
||||
.run();
|
||||
context.db
|
||||
.insert(distributionBoardComponentProtectionDevices)
|
||||
.values({
|
||||
componentId: "subtree-rcd",
|
||||
type: "FI",
|
||||
ratedCurrentA: 40,
|
||||
rcdType: "A",
|
||||
ratedResidualCurrentMa: 30,
|
||||
})
|
||||
.run();
|
||||
context.db
|
||||
.insert(circuits)
|
||||
.values({
|
||||
id: "subtree-circuit",
|
||||
circuitListId: list.id,
|
||||
sectionId: group.id,
|
||||
equipmentIdentifier: "-1F1.1",
|
||||
displayName: "Beleuchtung",
|
||||
sortOrder: 10,
|
||||
voltage: 230,
|
||||
isReserve: 0,
|
||||
})
|
||||
.run();
|
||||
context.db
|
||||
.insert(circuitProtectionDevices)
|
||||
.values({
|
||||
circuitId: "subtree-circuit",
|
||||
type: "LS",
|
||||
ratedCurrentA: 10,
|
||||
tripCharacteristic: "B",
|
||||
})
|
||||
.run();
|
||||
context.db
|
||||
.insert(circuitDeviceRows)
|
||||
.values({
|
||||
id: "subtree-row",
|
||||
circuitId: "subtree-circuit",
|
||||
linkedProjectDeviceId: null,
|
||||
legacyConsumerId: null,
|
||||
sortOrder: 10,
|
||||
name: "Leuchte",
|
||||
displayName: "Leuchte",
|
||||
phaseType: "single_phase",
|
||||
connectionKind: null,
|
||||
costGroup: null,
|
||||
category: null,
|
||||
level: null,
|
||||
roomId: null,
|
||||
roomNumberSnapshot: null,
|
||||
roomNameSnapshot: null,
|
||||
quantity: 2,
|
||||
powerPerUnit: 0.03,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 0.95,
|
||||
remark: null,
|
||||
overriddenFields: "[\"displayName\"]",
|
||||
})
|
||||
.run();
|
||||
return context;
|
||||
}
|
||||
|
||||
describe("circuit-group subtree project command", () => {
|
||||
it("deletes and restores the exact populated subtree through undo/redo", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new CircuitGroupSubtreeProjectCommandRepository(context.db);
|
||||
const groupId = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "subtree-circuit"))
|
||||
.get()!.sectionId;
|
||||
const snapshot = repository.capture("project-1", groupId);
|
||||
const command =
|
||||
createCircuitGroupDeleteSubtreeProjectCommand(snapshot);
|
||||
const deleted = repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command,
|
||||
});
|
||||
assert.equal(
|
||||
context.db
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.id, groupId))
|
||||
.get(),
|
||||
undefined
|
||||
);
|
||||
assert.equal(
|
||||
context.db
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.id, "subtree-row"))
|
||||
.get(),
|
||||
undefined
|
||||
);
|
||||
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId: new ProjectHistoryRepository(
|
||||
context.db
|
||||
).getNextCommand("project-1", "undo")?.changeSetId,
|
||||
command: deleted.inverse,
|
||||
});
|
||||
assert.deepEqual(
|
||||
repository.capture("project-1", groupId),
|
||||
snapshot
|
||||
);
|
||||
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 2,
|
||||
source: "redo",
|
||||
historyTargetChangeSetId: new ProjectHistoryRepository(
|
||||
context.db
|
||||
).getNextCommand("project-1", "redo")?.changeSetId,
|
||||
command,
|
||||
});
|
||||
assert.equal(
|
||||
context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "subtree-circuit"))
|
||||
.get(),
|
||||
undefined
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a subtree changed after confirmation", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new CircuitGroupSubtreeProjectCommandRepository(context.db);
|
||||
const groupId = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "subtree-circuit"))
|
||||
.get()!.sectionId;
|
||||
const snapshot = repository.capture("project-1", groupId);
|
||||
context.db
|
||||
.update(circuitDeviceRows)
|
||||
.set({ displayName: "Geändert" })
|
||||
.where(eq(circuitDeviceRows.id, "subtree-row"))
|
||||
.run();
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command:
|
||||
createCircuitGroupDeleteSubtreeProjectCommand(snapshot),
|
||||
}),
|
||||
/changed before deletion/
|
||||
);
|
||||
assert.ok(
|
||||
context.db
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.id, groupId))
|
||||
.get()
|
||||
);
|
||||
assert.equal(
|
||||
context.db.select().from(projectRevisions).all().length,
|
||||
0
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back a cascaded deletion when history persistence fails", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new CircuitGroupSubtreeProjectCommandRepository(context.db);
|
||||
const groupId = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "subtree-circuit"))
|
||||
.get()!.sectionId;
|
||||
const snapshot = repository.capture("project-1", groupId);
|
||||
context.sqlite.exec(`
|
||||
CREATE TRIGGER fail_group_subtree_history
|
||||
BEFORE INSERT ON project_history_stack_entries
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced group subtree history failure');
|
||||
END;
|
||||
`);
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command:
|
||||
createCircuitGroupDeleteSubtreeProjectCommand(snapshot),
|
||||
}),
|
||||
/forced group subtree history failure/
|
||||
);
|
||||
assert.deepEqual(
|
||||
repository.capture("project-1", groupId),
|
||||
snapshot
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import { CircuitStructureProjectCommandRepository } from "../src/db/repositories
|
||||
import { CircuitGroupStructureProjectCommandRepository } from "../src/db/repositories/circuit-group-structure-project-command.repository.js";
|
||||
import { CircuitGroupRenumberProjectCommandRepository } from "../src/db/repositories/circuit-group-renumber-project-command.repository.js";
|
||||
import { CircuitGroupMoveProjectCommandRepository } from "../src/db/repositories/circuit-group-move-project-command.repository.js";
|
||||
import { CircuitGroupSubtreeProjectCommandRepository } from "../src/db/repositories/circuit-group-subtree-project-command.repository.js";
|
||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
||||
import { DistributionBoardComponentStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-component-structure-project-command.repository.js";
|
||||
@@ -59,6 +60,7 @@ import { createCircuitGroupRenumberProjectCommand } from "../src/domain/models/c
|
||||
import { createCircuitGroupRenumberPlan } from "../src/domain/services/circuit-group-renumbering.js";
|
||||
import { createCircuitGroupMoveProjectCommand } from "../src/domain/models/circuit-group-move-project-command.model.js";
|
||||
import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js";
|
||||
import { createCircuitGroupDeleteSubtreeProjectCommand } from "../src/domain/models/circuit-group-subtree-project-command.model.js";
|
||||
import {
|
||||
createDistributionBoardInsertProjectCommand,
|
||||
createDistributionBoardStructureSnapshot,
|
||||
@@ -146,6 +148,7 @@ function createService(context: DatabaseContext) {
|
||||
new CircuitGroupStructureProjectCommandRepository(context.db),
|
||||
new CircuitGroupRenumberProjectCommandRepository(context.db),
|
||||
new CircuitGroupMoveProjectCommandRepository(context.db),
|
||||
new CircuitGroupSubtreeProjectCommandRepository(context.db),
|
||||
new ProjectHistoryRepository(context.db)
|
||||
);
|
||||
}
|
||||
@@ -1447,6 +1450,47 @@ describe("project command service", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("dispatches populated circuit-group deletion and restoration", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository =
|
||||
new CircuitGroupSubtreeProjectCommandRepository(context.db);
|
||||
const groupId = context.db
|
||||
.select()
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, "circuit-1"))
|
||||
.get()!.sectionId;
|
||||
const snapshot = repository.capture("project-1", groupId);
|
||||
const deleted = createService(context).executeUser({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
command:
|
||||
createCircuitGroupDeleteSubtreeProjectCommand(snapshot),
|
||||
});
|
||||
assert.equal(deleted.history.currentRevision, 1);
|
||||
assert.equal(
|
||||
context.db
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.id, groupId))
|
||||
.get(),
|
||||
undefined
|
||||
);
|
||||
|
||||
const restored = createService(context).undo({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
});
|
||||
assert.equal(restored.history.currentRevision, 2);
|
||||
assert.deepEqual(
|
||||
repository.capture("project-1", groupId),
|
||||
snapshot
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("dispatches floor and room setup with their persisted inverses", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { CircuitStructureProjectCommandRepository } from "../src/db/repositories
|
||||
import { CircuitGroupStructureProjectCommandRepository } from "../src/db/repositories/circuit-group-structure-project-command.repository.js";
|
||||
import { CircuitGroupRenumberProjectCommandRepository } from "../src/db/repositories/circuit-group-renumber-project-command.repository.js";
|
||||
import { CircuitGroupMoveProjectCommandRepository } from "../src/db/repositories/circuit-group-move-project-command.repository.js";
|
||||
import { CircuitGroupSubtreeProjectCommandRepository } from "../src/db/repositories/circuit-group-subtree-project-command.repository.js";
|
||||
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
||||
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
|
||||
import { DistributionBoardComponentStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-component-structure-project-command.repository.js";
|
||||
@@ -201,6 +202,7 @@ function createService(context: DatabaseContext) {
|
||||
new CircuitGroupStructureProjectCommandRepository(context.db),
|
||||
new CircuitGroupRenumberProjectCommandRepository(context.db),
|
||||
new CircuitGroupMoveProjectCommandRepository(context.db),
|
||||
new CircuitGroupSubtreeProjectCommandRepository(context.db),
|
||||
new ProjectHistoryRepository(context.db)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,6 +175,22 @@ describe("project version history presentation", () => {
|
||||
),
|
||||
"Stromkreis in andere Gruppe verschoben"
|
||||
);
|
||||
assert.equal(
|
||||
getProjectRevisionDescription(
|
||||
revision(14, {
|
||||
commandType: "circuit-group.delete-subtree",
|
||||
})
|
||||
),
|
||||
"Stromkreisgruppe vollständig entfernt"
|
||||
);
|
||||
assert.equal(
|
||||
getProjectRevisionDescription(
|
||||
revision(15, {
|
||||
commandType: "circuit-group.restore-subtree",
|
||||
})
|
||||
),
|
||||
"Stromkreisgruppe vollständig wiederhergestellt"
|
||||
);
|
||||
assert.equal(getProjectSnapshotKindLabel("named"), "Benannt");
|
||||
assert.equal(
|
||||
getProjectSnapshotKindLabel("automatic"),
|
||||
|
||||
Reference in New Issue
Block a user