261 lines
8.3 KiB
TypeScript
261 lines
8.3 KiB
TypeScript
import { and, eq } from "drizzle-orm";
|
|
import {
|
|
assertCircuitGroupDeleteProjectCommand,
|
|
assertCircuitGroupInsertProjectCommand,
|
|
assertCircuitGroupReorderProjectCommand,
|
|
assertCircuitGroupUpdateProjectCommand,
|
|
circuitGroupDeleteCommandType,
|
|
circuitGroupInsertCommandType,
|
|
circuitGroupReorderCommandType,
|
|
circuitGroupUpdateCommandType,
|
|
createCircuitGroupDeleteProjectCommand,
|
|
createCircuitGroupInsertProjectCommand,
|
|
createCircuitGroupReorderProjectCommand,
|
|
createCircuitGroupUpdateProjectCommand,
|
|
type CircuitGroupReorderAssignment,
|
|
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
|
|
);
|
|
}
|
|
if (command.type === circuitGroupReorderCommandType) {
|
|
assertCircuitGroupReorderProjectCommand(command);
|
|
const inverseAssignments = this.reorder(
|
|
database,
|
|
projectId,
|
|
command.payload.circuitListId,
|
|
command.payload.assignments
|
|
);
|
|
return createCircuitGroupReorderProjectCommand(
|
|
command.payload.circuitListId,
|
|
inverseAssignments
|
|
);
|
|
}
|
|
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 reorder(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
circuitListId: string,
|
|
assignments: CircuitGroupReorderAssignment[]
|
|
) {
|
|
this.assertListOwnership(database, projectId, circuitListId);
|
|
const persisted = database
|
|
.select({
|
|
id: circuitSections.id,
|
|
sortOrder: circuitSections.sortOrder,
|
|
})
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.circuitListId, circuitListId))
|
|
.all();
|
|
const persistedById = new Map(
|
|
persisted.map((group) => [group.id, group])
|
|
);
|
|
if (
|
|
persisted.length !== assignments.length ||
|
|
assignments.some((assignment) => {
|
|
const group = persistedById.get(assignment.groupId);
|
|
return !group || group.sortOrder !== assignment.expectedSortOrder;
|
|
})
|
|
) {
|
|
throw new Error(
|
|
"Circuit-group reorder must include every unchanged group in the list."
|
|
);
|
|
}
|
|
for (const assignment of assignments) {
|
|
if (assignment.expectedSortOrder === assignment.targetSortOrder) {
|
|
continue;
|
|
}
|
|
const updated = database
|
|
.update(circuitSections)
|
|
.set({ sortOrder: assignment.targetSortOrder })
|
|
.where(
|
|
and(
|
|
eq(circuitSections.id, assignment.groupId),
|
|
eq(circuitSections.circuitListId, circuitListId),
|
|
eq(circuitSections.sortOrder, assignment.expectedSortOrder)
|
|
)
|
|
)
|
|
.run();
|
|
if (updated.changes !== 1) {
|
|
throw new Error("Circuit group changed during reorder.");
|
|
}
|
|
}
|
|
return assignments.map((assignment) => ({
|
|
groupId: assignment.groupId,
|
|
expectedSortOrder: assignment.targetSortOrder,
|
|
targetSortOrder: assignment.expectedSortOrder,
|
|
}));
|
|
}
|
|
|
|
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
|
|
)
|
|
);
|
|
}
|