forked from jappel/leistungsbilanz-ts
348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
import { and, eq } from "drizzle-orm";
|
|
import {
|
|
assertCircuitGroupRenumberProjectCommand,
|
|
createCircuitGroupRenumberProjectCommand,
|
|
type CircuitGroupRenumberProjectCommand,
|
|
} from "../../domain/models/circuit-group-renumber-project-command.model.js";
|
|
import type { CircuitGroupRenumberProjectCommandStore } from "../../domain/ports/circuit-group-renumber-project-command.store.js";
|
|
import type { ExecuteCircuitGroupRenumberCommandInput } from "../../domain/ports/circuit-group-renumber-project-command.store.js";
|
|
import type { CircuitGroupRenumberPlan } from "../../domain/services/circuit-group-renumbering.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 CircuitGroupRenumberProjectCommandRepository
|
|
implements CircuitGroupRenumberProjectCommandStore
|
|
{
|
|
constructor(private readonly database: AppDatabase) {}
|
|
|
|
execute(input: ExecuteCircuitGroupRenumberCommandInput) {
|
|
assertCircuitGroupRenumberProjectCommand(input.command);
|
|
return executeProjectCommandTransaction(
|
|
this.database,
|
|
input,
|
|
(tx) => this.applyCommand(tx, input.projectId, input.command)
|
|
);
|
|
}
|
|
|
|
private applyCommand(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
command: CircuitGroupRenumberProjectCommand
|
|
) {
|
|
const { circuitListId, groups } = command.payload;
|
|
this.assertCurrentState(database, projectId, circuitListId, groups);
|
|
const inverse = createCircuitGroupRenumberProjectCommand(
|
|
circuitListId,
|
|
invertPlan(groups)
|
|
);
|
|
const occupiedIdentifiers = new Set(
|
|
[
|
|
...database
|
|
.select({ value: circuits.equipmentIdentifier })
|
|
.from(circuits)
|
|
.where(eq(circuits.circuitListId, circuitListId))
|
|
.all(),
|
|
...database
|
|
.select({ value: distributionBoardComponents.equipmentIdentifier })
|
|
.from(distributionBoardComponents)
|
|
.where(
|
|
eq(distributionBoardComponents.circuitListId, circuitListId)
|
|
)
|
|
.all(),
|
|
].map(({ value }) => normalizeIdentifier(value))
|
|
);
|
|
const temporaryIdentifiers = new Map<string, string>();
|
|
for (const group of groups) {
|
|
for (const assignment of [
|
|
...group.circuits.map((entry) => ({
|
|
id: entry.circuitId,
|
|
expected: entry.expectedEquipmentIdentifier,
|
|
table: "circuit" as const,
|
|
})),
|
|
...group.components.map((entry) => ({
|
|
id: entry.componentId,
|
|
expected: entry.expectedEquipmentIdentifier,
|
|
table: "component" as const,
|
|
})),
|
|
]) {
|
|
const key = `${assignment.table}:${assignment.id}`;
|
|
const temporary = createTemporaryIdentifier(
|
|
key,
|
|
occupiedIdentifiers
|
|
);
|
|
temporaryIdentifiers.set(key, temporary);
|
|
this.updateEntityIdentifier(
|
|
database,
|
|
assignment.table,
|
|
assignment.id,
|
|
circuitListId,
|
|
assignment.expected,
|
|
temporary
|
|
);
|
|
}
|
|
}
|
|
|
|
const occupiedPrefixes = new Set(
|
|
database
|
|
.select({ prefix: circuitSections.prefix })
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.circuitListId, circuitListId))
|
|
.all()
|
|
.map(({ prefix }) => prefix)
|
|
);
|
|
const temporaryPrefixes = new Map<string, string>();
|
|
for (const group of groups) {
|
|
const temporaryPrefix = createTemporaryValue(
|
|
`__tmp_group_prefix_${group.groupId}`,
|
|
occupiedPrefixes
|
|
);
|
|
temporaryPrefixes.set(group.groupId, temporaryPrefix);
|
|
const updated = database
|
|
.update(circuitSections)
|
|
.set({ groupNumber: null, prefix: temporaryPrefix })
|
|
.where(
|
|
and(
|
|
eq(circuitSections.id, group.groupId),
|
|
eq(circuitSections.circuitListId, circuitListId),
|
|
eq(circuitSections.groupNumber, group.expectedGroupNumber),
|
|
eq(circuitSections.prefix, group.expectedPrefix)
|
|
)
|
|
)
|
|
.run();
|
|
if (updated.changes !== 1) {
|
|
throw new Error("Circuit group changed during renumbering.");
|
|
}
|
|
}
|
|
|
|
for (const group of groups) {
|
|
const updated = database
|
|
.update(circuitSections)
|
|
.set({
|
|
groupNumber: group.targetGroupNumber,
|
|
prefix: group.targetPrefix,
|
|
})
|
|
.where(
|
|
and(
|
|
eq(circuitSections.id, group.groupId),
|
|
eq(circuitSections.circuitListId, circuitListId),
|
|
eq(circuitSections.prefix, temporaryPrefixes.get(group.groupId)!)
|
|
)
|
|
)
|
|
.run();
|
|
if (updated.changes !== 1) {
|
|
throw new Error("Circuit group changed during final renumbering.");
|
|
}
|
|
}
|
|
|
|
for (const group of groups) {
|
|
for (const assignment of group.circuits) {
|
|
this.updateEntityIdentifier(
|
|
database,
|
|
"circuit",
|
|
assignment.circuitId,
|
|
circuitListId,
|
|
temporaryIdentifiers.get(`circuit:${assignment.circuitId}`)!,
|
|
assignment.targetEquipmentIdentifier
|
|
);
|
|
}
|
|
for (const assignment of group.components) {
|
|
this.updateEntityIdentifier(
|
|
database,
|
|
"component",
|
|
assignment.componentId,
|
|
circuitListId,
|
|
temporaryIdentifiers.get(`component:${assignment.componentId}`)!,
|
|
assignment.targetEquipmentIdentifier
|
|
);
|
|
}
|
|
}
|
|
return inverse;
|
|
}
|
|
|
|
private assertCurrentState(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
circuitListId: string,
|
|
groups: CircuitGroupRenumberPlan["groups"]
|
|
) {
|
|
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.");
|
|
}
|
|
const selectedIds = new Set(groups.map((group) => group.groupId));
|
|
const allGroups = database
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.circuitListId, circuitListId))
|
|
.all();
|
|
for (const group of groups) {
|
|
const current = allGroups.find((entry) => entry.id === group.groupId);
|
|
if (
|
|
!current ||
|
|
current.category !== group.category ||
|
|
current.groupNumber !== group.expectedGroupNumber ||
|
|
current.prefix !== group.expectedPrefix
|
|
) {
|
|
throw new Error("Circuit group changed before renumbering.");
|
|
}
|
|
const currentCircuits = database
|
|
.select({
|
|
id: circuits.id,
|
|
equipmentIdentifier: circuits.equipmentIdentifier,
|
|
})
|
|
.from(circuits)
|
|
.where(eq(circuits.sectionId, group.groupId))
|
|
.all();
|
|
const currentComponents = database
|
|
.select({
|
|
id: distributionBoardComponents.id,
|
|
equipmentIdentifier:
|
|
distributionBoardComponents.equipmentIdentifier,
|
|
})
|
|
.from(distributionBoardComponents)
|
|
.where(eq(distributionBoardComponents.sectionId, group.groupId))
|
|
.all();
|
|
if (
|
|
!matchesAssignments(currentCircuits, group.circuits, "circuitId") ||
|
|
!matchesAssignments(
|
|
currentComponents,
|
|
group.components,
|
|
"componentId"
|
|
)
|
|
) {
|
|
throw new Error(
|
|
"Circuit-group renumber must include every unchanged child."
|
|
);
|
|
}
|
|
}
|
|
for (const group of groups) {
|
|
if (
|
|
allGroups.some(
|
|
(entry) =>
|
|
!selectedIds.has(entry.id) &&
|
|
entry.category === group.category &&
|
|
entry.groupNumber === group.targetGroupNumber
|
|
)
|
|
) {
|
|
throw new Error(
|
|
"Target group number belongs to an unchanged group."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private updateEntityIdentifier(
|
|
database: AppDatabase,
|
|
table: "circuit" | "component",
|
|
id: string,
|
|
circuitListId: string,
|
|
expected: string,
|
|
target: string
|
|
) {
|
|
const schema =
|
|
table === "circuit" ? circuits : distributionBoardComponents;
|
|
const updated = database
|
|
.update(schema)
|
|
.set({ equipmentIdentifier: target })
|
|
.where(
|
|
and(
|
|
eq(schema.id, id),
|
|
eq(schema.circuitListId, circuitListId),
|
|
eq(schema.equipmentIdentifier, expected)
|
|
)
|
|
)
|
|
.run();
|
|
if (updated.changes !== 1) {
|
|
throw new Error(
|
|
"Circuit-group child changed during renumbering."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function matchesAssignments<
|
|
TAssignment extends {
|
|
expectedEquipmentIdentifier: string;
|
|
},
|
|
>(
|
|
current: Array<{ id: string; equipmentIdentifier: string }>,
|
|
assignments: TAssignment[],
|
|
idField: keyof TAssignment
|
|
) {
|
|
if (current.length !== assignments.length) {
|
|
return false;
|
|
}
|
|
const currentById = new Map(current.map((entry) => [entry.id, entry]));
|
|
return assignments.every((assignment) => {
|
|
const id = assignment[idField];
|
|
return (
|
|
typeof id === "string" &&
|
|
currentById.get(id)?.equipmentIdentifier ===
|
|
assignment.expectedEquipmentIdentifier
|
|
);
|
|
});
|
|
}
|
|
|
|
function invertPlan(
|
|
groups: CircuitGroupRenumberPlan["groups"]
|
|
): CircuitGroupRenumberPlan {
|
|
return {
|
|
groups: groups.map((group) => ({
|
|
...group,
|
|
expectedGroupNumber: group.targetGroupNumber,
|
|
targetGroupNumber: group.expectedGroupNumber,
|
|
expectedPrefix: group.targetPrefix,
|
|
targetPrefix: group.expectedPrefix,
|
|
circuits: group.circuits.map((circuit) => ({
|
|
...circuit,
|
|
expectedEquipmentIdentifier:
|
|
circuit.targetEquipmentIdentifier,
|
|
targetEquipmentIdentifier:
|
|
circuit.expectedEquipmentIdentifier,
|
|
})),
|
|
components: group.components.map((component) => ({
|
|
...component,
|
|
expectedEquipmentIdentifier:
|
|
component.targetEquipmentIdentifier,
|
|
targetEquipmentIdentifier:
|
|
component.expectedEquipmentIdentifier,
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
|
|
function createTemporaryIdentifier(
|
|
key: string,
|
|
occupied: Set<string>
|
|
) {
|
|
return createTemporaryValue(
|
|
`__tmp_group_renumber_${key.replace(":", "_")}`,
|
|
occupied,
|
|
normalizeIdentifier
|
|
);
|
|
}
|
|
|
|
function createTemporaryValue(
|
|
base: string,
|
|
occupied: Set<string>,
|
|
normalize: (value: string) => string = (value) => value
|
|
) {
|
|
let candidate = base;
|
|
while (occupied.has(normalize(candidate))) {
|
|
candidate += "_";
|
|
}
|
|
occupied.add(normalize(candidate));
|
|
return candidate;
|
|
}
|
|
|
|
function normalizeIdentifier(value: string) {
|
|
return value.trim().toLocaleLowerCase();
|
|
}
|