leistungsbilanz-ts/src/db/repositories/external-object-row-assignment-project-command.repository.ts

143 lines
6 KiB
TypeScript

import { and, eq, inArray } from "drizzle-orm";
import {
assertCircuitDeviceRowQuantity,
} from "../../domain/calculations/circuit-device-row-quantity.js";
import {
assertExternalObjectRowAssignmentProjectCommand,
invertExternalObjectRowAssignmentProjectCommand,
} from "../../domain/models/external-object-row-assignment-project-command.model.js";
import type { ExternalObjectRowAssignmentProjectCommandStore } from "../../domain/ports/external-object-row-assignment-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 { circuitSections } from "../schema/circuit-sections.js";
import { circuits } from "../schema/circuits.js";
import { externalModelObjects } from "../schema/external-model-objects.js";
import { toCircuitDeviceRowSnapshot } from "./circuit-device-row-structure.persistence.js";
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
import {
applyExternalObjectLinks,
assertExternalObjectsCompatibleWithRow,
loadExpectedExternalObjectTransitions,
snapshotsEqual,
toExternalObjectSnapshot,
} from "./external-object-assignment.persistence.js";
export class ExternalObjectRowAssignmentProjectCommandRepository
implements ExternalObjectRowAssignmentProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
execute(input: Parameters<ExternalObjectRowAssignmentProjectCommandStore["execute"]>[0]) {
assertExternalObjectRowAssignmentProjectCommand(input.command);
return executeProjectCommandTransaction(this.database, input, (tx) => {
this.apply(tx, input.projectId, input.command);
return invertExternalObjectRowAssignmentProjectCommand(input.command);
});
}
private apply(
database: AppDatabase,
projectId: string,
command: Parameters<ExternalObjectRowAssignmentProjectCommandStore["execute"]>[0]["command"]
) {
const rowTransitions = new Map(
command.payload.rows.map((transition) => [transition.expected.id, transition])
);
const objectTransitions = new Map(
command.payload.objects.map((transition) => [transition.expected.id, transition])
);
const rowIds = [...rowTransitions.keys()];
const currentRows = database.select().from(circuitDeviceRows)
.where(inArray(circuitDeviceRows.id, rowIds)).all();
if (currentRows.length !== rowIds.length) {
throw new Error("External object assignment target no longer exists.");
}
for (const row of currentRows) {
const expected = rowTransitions.get(row.id)!.expected;
if (!snapshotsEqual(toCircuitDeviceRowSnapshot(row), expected)) {
throw new Error("Circuit device row changed before external object assignment.");
}
}
loadExpectedExternalObjectTransitions(
database,
projectId,
command.payload.objects,
"External object changed before row assignment."
);
const rowContexts = database
.select({
rowId: circuitDeviceRows.id,
projectId: circuitLists.projectId,
distributionBoardId: circuitLists.distributionBoardId,
category: circuitSections.category,
})
.from(circuitDeviceRows)
.innerJoin(circuits, eq(circuits.id, circuitDeviceRows.circuitId))
.innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId))
.innerJoin(circuitSections, eq(circuitSections.id, circuits.sectionId))
.where(inArray(circuitDeviceRows.id, rowIds))
.all();
if (
rowContexts.length !== rowIds.length ||
rowContexts.some((context) =>
context.projectId !== projectId || context.category === null
)
) {
throw new Error("Circuit device row belongs to another project.");
}
const contextByRowId = new Map(rowContexts.map((context) => [context.rowId, context]));
const allProjectObjects = database.select().from(externalModelObjects)
.where(eq(externalModelObjects.projectId, projectId)).all()
.map(toExternalObjectSnapshot);
const targetObjects = allProjectObjects.map((object) =>
objectTransitions.get(object.id)?.target ?? object
);
const confirmedConflicts = new Set(command.payload.confirmedConflictObjectIds);
for (const transition of command.payload.rows) {
const linkedObjects = targetObjects.filter(
(object) => object.circuitDeviceRowId === transition.target.id
);
const assignedObjects = command.payload.objects
.map(({ target }) => target)
.filter((object) => object.circuitDeviceRowId === transition.target.id);
const context = contextByRowId.get(transition.target.id)!;
assertExternalObjectsCompatibleWithRow({
database,
projectId,
row: transition.target,
distributionBoardId: context.distributionBoardId,
category: context.category!,
assignedObjects,
allLinkedObjects: linkedObjects,
confirmedConflictObjectIds: confirmedConflicts,
});
assertCircuitDeviceRowQuantity({
quantity: transition.target.quantity,
manualQuantity: transition.target.manualQuantity ?? transition.target.quantity,
externalObjects: linkedObjects.map((object) => ({
effectiveQuantity: object.planningValues.effectiveQuantity,
})),
});
}
for (const transition of command.payload.rows) {
const result = database.update(circuitDeviceRows)
.set({ quantity: transition.target.quantity })
.where(and(
eq(circuitDeviceRows.id, transition.expected.id),
eq(circuitDeviceRows.quantity, transition.expected.quantity),
eq(circuitDeviceRows.manualQuantity, transition.expected.manualQuantity ?? transition.expected.quantity)
)).run();
if (result.changes !== 1) throw new Error("Circuit device row changed during assignment.");
}
applyExternalObjectLinks(
database,
command.payload.objects,
"External object changed during assignment."
);
}
}