Add circuit group commands
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
assertCircuitGroupDeleteProjectCommand,
|
||||
assertCircuitGroupInsertProjectCommand,
|
||||
assertCircuitGroupUpdateProjectCommand,
|
||||
circuitGroupDeleteCommandType,
|
||||
circuitGroupInsertCommandType,
|
||||
circuitGroupUpdateCommandType,
|
||||
createCircuitGroupDeleteProjectCommand,
|
||||
createCircuitGroupInsertProjectCommand,
|
||||
createCircuitGroupUpdateProjectCommand,
|
||||
type CircuitGroupSnapshot,
|
||||
type CircuitGroupStructureProjectCommand,
|
||||
} from "../../domain/models/circuit-group-structure-project-command.model.js";
|
||||
import type {
|
||||
CircuitGroupStructureProjectCommandStore,
|
||||
ExecuteCircuitGroupStructureCommandInput,
|
||||
} from "../../domain/ports/circuit-group-structure-project-command.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuitSections } from "../schema/circuit-sections.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
||||
export class CircuitGroupStructureProjectCommandRepository
|
||||
implements CircuitGroupStructureProjectCommandStore
|
||||
{
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
execute(input: ExecuteCircuitGroupStructureCommandInput) {
|
||||
return executeProjectCommandTransaction(
|
||||
this.database,
|
||||
input,
|
||||
(tx) => this.applyCommand(tx, input.projectId, input.command)
|
||||
);
|
||||
}
|
||||
|
||||
private applyCommand(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
command: CircuitGroupStructureProjectCommand
|
||||
): CircuitGroupStructureProjectCommand {
|
||||
if (command.type === circuitGroupInsertCommandType) {
|
||||
assertCircuitGroupInsertProjectCommand(command);
|
||||
this.insert(database, projectId, command.payload.snapshot);
|
||||
return createCircuitGroupDeleteProjectCommand(
|
||||
command.payload.snapshot
|
||||
);
|
||||
}
|
||||
if (command.type === circuitGroupDeleteCommandType) {
|
||||
assertCircuitGroupDeleteProjectCommand(command);
|
||||
this.delete(database, projectId, command.payload.snapshot);
|
||||
return createCircuitGroupInsertProjectCommand(
|
||||
command.payload.snapshot
|
||||
);
|
||||
}
|
||||
if (command.type === circuitGroupUpdateCommandType) {
|
||||
assertCircuitGroupUpdateProjectCommand(command);
|
||||
this.update(
|
||||
database,
|
||||
projectId,
|
||||
command.payload.expected,
|
||||
command.payload.target
|
||||
);
|
||||
return createCircuitGroupUpdateProjectCommand(
|
||||
command.payload.target,
|
||||
command.payload.expected
|
||||
);
|
||||
}
|
||||
throw new Error("Unsupported circuit-group structure command.");
|
||||
}
|
||||
|
||||
private insert(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
snapshot: CircuitGroupSnapshot
|
||||
) {
|
||||
this.assertListOwnership(database, projectId, snapshot.circuitListId);
|
||||
const existing = database
|
||||
.select({ id: circuitSections.id })
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.id, snapshot.id))
|
||||
.get();
|
||||
if (existing) {
|
||||
throw new Error("Circuit-group id already exists.");
|
||||
}
|
||||
database.insert(circuitSections).values(snapshot).run();
|
||||
}
|
||||
|
||||
private delete(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
snapshot: CircuitGroupSnapshot
|
||||
) {
|
||||
this.assertListOwnership(database, projectId, snapshot.circuitListId);
|
||||
const current = this.getCurrent(database, snapshot.id);
|
||||
if (!current || !sameRecord(snapshot, current)) {
|
||||
throw new Error("Circuit group changed before deletion.");
|
||||
}
|
||||
const circuit = database
|
||||
.select({ id: circuits.id })
|
||||
.from(circuits)
|
||||
.where(eq(circuits.sectionId, snapshot.id))
|
||||
.get();
|
||||
const component = database
|
||||
.select({ id: distributionBoardComponents.id })
|
||||
.from(distributionBoardComponents)
|
||||
.where(eq(distributionBoardComponents.sectionId, snapshot.id))
|
||||
.get();
|
||||
if (circuit || component) {
|
||||
throw new Error(
|
||||
"Only empty circuit groups can be deleted by this command."
|
||||
);
|
||||
}
|
||||
const deleted = database
|
||||
.delete(circuitSections)
|
||||
.where(
|
||||
and(
|
||||
eq(circuitSections.id, snapshot.id),
|
||||
eq(circuitSections.circuitListId, snapshot.circuitListId)
|
||||
)
|
||||
)
|
||||
.run();
|
||||
if (deleted.changes !== 1) {
|
||||
throw new Error("Circuit group could not be deleted.");
|
||||
}
|
||||
}
|
||||
|
||||
private update(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
expected: CircuitGroupSnapshot,
|
||||
target: CircuitGroupSnapshot
|
||||
) {
|
||||
this.assertListOwnership(database, projectId, expected.circuitListId);
|
||||
const current = this.getCurrent(database, expected.id);
|
||||
if (!current || !sameRecord(expected, current)) {
|
||||
throw new Error("Circuit group changed before update.");
|
||||
}
|
||||
const updated = database
|
||||
.update(circuitSections)
|
||||
.set({ displayName: target.displayName })
|
||||
.where(
|
||||
and(
|
||||
eq(circuitSections.id, expected.id),
|
||||
eq(circuitSections.circuitListId, expected.circuitListId)
|
||||
)
|
||||
)
|
||||
.run();
|
||||
if (updated.changes !== 1) {
|
||||
throw new Error("Circuit group could not be updated.");
|
||||
}
|
||||
}
|
||||
|
||||
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 does not belong to project.");
|
||||
}
|
||||
}
|
||||
|
||||
private getCurrent(database: AppDatabase, id: string) {
|
||||
return database
|
||||
.select()
|
||||
.from(circuitSections)
|
||||
.where(eq(circuitSections.id, id))
|
||||
.get();
|
||||
}
|
||||
}
|
||||
|
||||
function sameRecord(expected: object, actual: object) {
|
||||
const expectedEntries = Object.entries(expected);
|
||||
const actualRecord = actual as Record<string, unknown>;
|
||||
return (
|
||||
expectedEntries.length === Object.keys(actual).length &&
|
||||
expectedEntries.every(
|
||||
([key, value]) => actualRecord[key] === value
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import {
|
||||
circuitGroupCategories,
|
||||
type CircuitGroupCategory,
|
||||
} from "../../shared/constants/circuit-group.js";
|
||||
import { formatCircuitGroupPrefix } from "../services/circuit-group-numbering.js";
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
|
||||
export const circuitGroupInsertCommandType = "circuit-group.insert" as const;
|
||||
export const circuitGroupDeleteCommandType = "circuit-group.delete" as const;
|
||||
export const circuitGroupUpdateCommandType = "circuit-group.update" as const;
|
||||
export const circuitGroupStructureCommandSchemaVersion = 1 as const;
|
||||
|
||||
export interface CircuitGroupSnapshot {
|
||||
id: string;
|
||||
circuitListId: string;
|
||||
key: string;
|
||||
displayName: string;
|
||||
prefix: string;
|
||||
sortOrder: number;
|
||||
category: CircuitGroupCategory;
|
||||
groupNumber: number;
|
||||
}
|
||||
|
||||
interface SnapshotPayload {
|
||||
snapshot: CircuitGroupSnapshot;
|
||||
}
|
||||
|
||||
interface UpdatePayload {
|
||||
expected: CircuitGroupSnapshot;
|
||||
target: CircuitGroupSnapshot;
|
||||
}
|
||||
|
||||
export interface CircuitGroupInsertProjectCommand
|
||||
extends SerializedProjectCommand<SnapshotPayload> {
|
||||
schemaVersion: typeof circuitGroupStructureCommandSchemaVersion;
|
||||
type: typeof circuitGroupInsertCommandType;
|
||||
}
|
||||
|
||||
export interface CircuitGroupDeleteProjectCommand
|
||||
extends SerializedProjectCommand<SnapshotPayload> {
|
||||
schemaVersion: typeof circuitGroupStructureCommandSchemaVersion;
|
||||
type: typeof circuitGroupDeleteCommandType;
|
||||
}
|
||||
|
||||
export interface CircuitGroupUpdateProjectCommand
|
||||
extends SerializedProjectCommand<UpdatePayload> {
|
||||
schemaVersion: typeof circuitGroupStructureCommandSchemaVersion;
|
||||
type: typeof circuitGroupUpdateCommandType;
|
||||
}
|
||||
|
||||
export type CircuitGroupStructureProjectCommand =
|
||||
| CircuitGroupInsertProjectCommand
|
||||
| CircuitGroupDeleteProjectCommand
|
||||
| CircuitGroupUpdateProjectCommand;
|
||||
|
||||
export function createCircuitGroupInsertProjectCommand(
|
||||
snapshot: CircuitGroupSnapshot
|
||||
): CircuitGroupInsertProjectCommand {
|
||||
const command: CircuitGroupInsertProjectCommand = {
|
||||
schemaVersion: circuitGroupStructureCommandSchemaVersion,
|
||||
type: circuitGroupInsertCommandType,
|
||||
payload: { snapshot },
|
||||
};
|
||||
assertCircuitGroupInsertProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function createCircuitGroupDeleteProjectCommand(
|
||||
snapshot: CircuitGroupSnapshot
|
||||
): CircuitGroupDeleteProjectCommand {
|
||||
const command: CircuitGroupDeleteProjectCommand = {
|
||||
schemaVersion: circuitGroupStructureCommandSchemaVersion,
|
||||
type: circuitGroupDeleteCommandType,
|
||||
payload: { snapshot },
|
||||
};
|
||||
assertCircuitGroupDeleteProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function createCircuitGroupUpdateProjectCommand(
|
||||
expected: CircuitGroupSnapshot,
|
||||
target: CircuitGroupSnapshot
|
||||
): CircuitGroupUpdateProjectCommand {
|
||||
const command: CircuitGroupUpdateProjectCommand = {
|
||||
schemaVersion: circuitGroupStructureCommandSchemaVersion,
|
||||
type: circuitGroupUpdateCommandType,
|
||||
payload: { expected, target },
|
||||
};
|
||||
assertCircuitGroupUpdateProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function assertCircuitGroupInsertProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is CircuitGroupInsertProjectCommand {
|
||||
assertSnapshotCommand(command, circuitGroupInsertCommandType);
|
||||
}
|
||||
|
||||
export function assertCircuitGroupDeleteProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is CircuitGroupDeleteProjectCommand {
|
||||
assertSnapshotCommand(command, circuitGroupDeleteCommandType);
|
||||
}
|
||||
|
||||
export function assertCircuitGroupUpdateProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is CircuitGroupUpdateProjectCommand {
|
||||
if (
|
||||
command.schemaVersion !== circuitGroupStructureCommandSchemaVersion ||
|
||||
command.type !== circuitGroupUpdateCommandType ||
|
||||
!isPlainObject(command.payload) ||
|
||||
Object.keys(command.payload).length !== 2
|
||||
) {
|
||||
throw new Error("Unsupported circuit-group update command.");
|
||||
}
|
||||
assertCircuitGroupSnapshot(command.payload.expected);
|
||||
assertCircuitGroupSnapshot(command.payload.target);
|
||||
for (const field of [
|
||||
"id",
|
||||
"circuitListId",
|
||||
"key",
|
||||
"prefix",
|
||||
"sortOrder",
|
||||
"category",
|
||||
"groupNumber",
|
||||
] as const) {
|
||||
if (command.payload.expected[field] !== command.payload.target[field]) {
|
||||
throw new Error(
|
||||
"Circuit-group updates can only change the display name."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertSnapshotCommand(
|
||||
command: SerializedProjectCommand<unknown>,
|
||||
type:
|
||||
| typeof circuitGroupInsertCommandType
|
||||
| typeof circuitGroupDeleteCommandType
|
||||
) {
|
||||
if (
|
||||
command.schemaVersion !== circuitGroupStructureCommandSchemaVersion ||
|
||||
command.type !== type ||
|
||||
!isPlainObject(command.payload) ||
|
||||
Object.keys(command.payload).length !== 1
|
||||
) {
|
||||
throw new Error("Unsupported circuit-group structure command.");
|
||||
}
|
||||
assertCircuitGroupSnapshot(command.payload.snapshot);
|
||||
}
|
||||
|
||||
export function assertCircuitGroupSnapshot(
|
||||
value: unknown
|
||||
): asserts value is CircuitGroupSnapshot {
|
||||
if (!isPlainObject(value) || Object.keys(value).length !== 8) {
|
||||
throw new Error("Circuit-group snapshot is invalid.");
|
||||
}
|
||||
for (const field of [
|
||||
"id",
|
||||
"circuitListId",
|
||||
"key",
|
||||
"displayName",
|
||||
"prefix",
|
||||
] as const) {
|
||||
if (typeof value[field] !== "string" || !value[field].trim()) {
|
||||
throw new Error(`${field} must be a non-empty string.`);
|
||||
}
|
||||
}
|
||||
if (
|
||||
!circuitGroupCategories.includes(
|
||||
value.category as CircuitGroupCategory
|
||||
) ||
|
||||
!Number.isInteger(value.groupNumber) ||
|
||||
(value.groupNumber as number) < 1 ||
|
||||
!Number.isFinite(value.sortOrder)
|
||||
) {
|
||||
throw new Error("Circuit-group category, number or order is invalid.");
|
||||
}
|
||||
if (
|
||||
value.prefix !==
|
||||
formatCircuitGroupPrefix(
|
||||
value.category as CircuitGroupCategory,
|
||||
value.groupNumber as number
|
||||
)
|
||||
) {
|
||||
throw new Error("Circuit-group prefix does not match its category and number.");
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { CircuitGroupStructureProjectCommand } from "../models/circuit-group-structure-project-command.model.js";
|
||||
import type {
|
||||
AppendedProjectRevision,
|
||||
ProjectRevisionSource,
|
||||
} from "./project-revision.store.js";
|
||||
|
||||
export interface ExecuteCircuitGroupStructureCommandInput {
|
||||
projectId: string;
|
||||
expectedRevision: number;
|
||||
source: ProjectRevisionSource;
|
||||
description?: string;
|
||||
actorId?: string;
|
||||
historyTargetChangeSetId?: string;
|
||||
command: CircuitGroupStructureProjectCommand;
|
||||
}
|
||||
|
||||
export interface CircuitGroupStructureProjectCommandStore {
|
||||
execute(input: ExecuteCircuitGroupStructureCommandInput): {
|
||||
revision: AppendedProjectRevision;
|
||||
inverse: CircuitGroupStructureProjectCommand;
|
||||
};
|
||||
}
|
||||
@@ -36,6 +36,13 @@ function getGroupStem(
|
||||
return `-${circuitGroupCategoryNumbers[category]}${functionLetter}${groupNumber}`;
|
||||
}
|
||||
|
||||
export function formatCircuitGroupPrefix(
|
||||
category: CircuitGroupCategory,
|
||||
groupNumber: number
|
||||
) {
|
||||
return `${getGroupStem(category, "F", groupNumber)}.`;
|
||||
}
|
||||
|
||||
export function formatGroupUpstreamProtectionIdentifier(
|
||||
category: CircuitGroupCategory,
|
||||
groupNumber: number
|
||||
|
||||
@@ -98,6 +98,7 @@ import type { CircuitProjectCommandStore } from "../ports/circuit-project-comman
|
||||
import type { CircuitSectionReorderProjectCommandStore } from "../ports/circuit-section-reorder-project-command.store.js";
|
||||
import type { CircuitSectionRenumberProjectCommandStore } from "../ports/circuit-section-renumber-project-command.store.js";
|
||||
import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js";
|
||||
import type { CircuitGroupStructureProjectCommandStore } from "../ports/circuit-group-structure-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 {
|
||||
@@ -110,6 +111,14 @@ import type {
|
||||
ProjectHistoryDirection,
|
||||
ProjectHistoryStore,
|
||||
} from "../ports/project-history.store.js";
|
||||
import {
|
||||
assertCircuitGroupDeleteProjectCommand,
|
||||
assertCircuitGroupInsertProjectCommand,
|
||||
assertCircuitGroupUpdateProjectCommand,
|
||||
circuitGroupDeleteCommandType,
|
||||
circuitGroupInsertCommandType,
|
||||
circuitGroupUpdateCommandType,
|
||||
} from "../models/circuit-group-structure-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";
|
||||
@@ -145,6 +154,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
private readonly projectSettingsStore: ProjectSettingsProjectCommandStore,
|
||||
private readonly projectStateRestoreStore: ProjectStateRestoreCommandStore,
|
||||
private readonly distributionBoardComponentStructureStore: DistributionBoardComponentStructureProjectCommandStore,
|
||||
private readonly circuitGroupStructureStore: CircuitGroupStructureProjectCommandStore,
|
||||
private readonly historyStore: ProjectHistoryStore
|
||||
) {}
|
||||
|
||||
@@ -333,6 +343,27 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case circuitGroupInsertCommandType: {
|
||||
assertCircuitGroupInsertProjectCommand(input.command);
|
||||
return this.circuitGroupStructureStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case circuitGroupDeleteCommandType: {
|
||||
assertCircuitGroupDeleteProjectCommand(input.command);
|
||||
return this.circuitGroupStructureStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case circuitGroupUpdateCommandType: {
|
||||
assertCircuitGroupUpdateProjectCommand(input.command);
|
||||
return this.circuitGroupStructureStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectFloorInsertCommandType: {
|
||||
assertProjectFloorInsertProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({
|
||||
|
||||
@@ -36,6 +36,9 @@ const commandTypeLabels: Record<string, string> = {
|
||||
"distribution-board-component.insert": "Verteilergerät angelegt",
|
||||
"distribution-board-component.delete": "Verteilergerät entfernt",
|
||||
"distribution-board-component.update": "Verteilergerät bearbeitet",
|
||||
"circuit-group.insert": "Stromkreisgruppe angelegt",
|
||||
"circuit-group.delete": "Stromkreisgruppe entfernt",
|
||||
"circuit-group.update": "Stromkreisgruppe bearbeitet",
|
||||
"project-floor.insert": "Geschoss angelegt",
|
||||
"project-floor.delete": "Geschoss entfernt",
|
||||
"project-room.insert": "Raum angelegt",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CircuitProjectCommandRepository } from "../../db/repositories/circuit-p
|
||||
import { CircuitSectionReorderProjectCommandRepository } from "../../db/repositories/circuit-section-reorder-project-command.repository.js";
|
||||
import { CircuitSectionRenumberProjectCommandRepository } from "../../db/repositories/circuit-section-renumber-project-command.repository.js";
|
||||
import { CircuitStructureProjectCommandRepository } from "../../db/repositories/circuit-structure-project-command.repository.js";
|
||||
import { CircuitGroupStructureProjectCommandRepository } from "../../db/repositories/circuit-group-structure-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";
|
||||
@@ -26,6 +27,8 @@ export const circuitDeviceRowMoveProjectCommandStore =
|
||||
new CircuitDeviceRowMoveProjectCommandRepository(db);
|
||||
export const circuitStructureProjectCommandStore =
|
||||
new CircuitStructureProjectCommandRepository(db);
|
||||
export const circuitGroupStructureProjectCommandStore =
|
||||
new CircuitGroupStructureProjectCommandRepository(db);
|
||||
export const distributionBoardStructureProjectCommandStore =
|
||||
new DistributionBoardStructureProjectCommandRepository(db);
|
||||
export const distributionBoardComponentStructureProjectCommandStore =
|
||||
@@ -63,5 +66,6 @@ export const projectCommandService = new ProjectCommandService(
|
||||
projectSettingsProjectCommandStore,
|
||||
projectStateRestoreCommandStore,
|
||||
distributionBoardComponentStructureProjectCommandStore,
|
||||
circuitGroupStructureProjectCommandStore,
|
||||
projectHistoryStore
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user