449 lines
13 KiB
TypeScript
449 lines
13 KiB
TypeScript
import { and, asc, eq, inArray } from "drizzle-orm";
|
|
import {
|
|
assertDistributionBoardDeleteProjectCommand,
|
|
assertDistributionBoardInsertProjectCommand,
|
|
distributionBoardDeleteCommandType,
|
|
distributionBoardInsertCommandType,
|
|
invertDistributionBoardStructureProjectCommand,
|
|
normalizeDistributionBoardStructureProjectCommand,
|
|
type DistributionBoardStructureProjectCommand,
|
|
type NormalizedDistributionBoardStructureSnapshot,
|
|
} from "../../domain/models/distribution-board-structure-project-command.model.js";
|
|
import {
|
|
assertDistributionBoardUpdateProjectCommand,
|
|
createDistributionBoardUpdateProjectCommand,
|
|
legacyDistributionBoardUpdateCommandSchemaVersion,
|
|
type DistributionBoardUpdateField,
|
|
type DistributionBoardUpdatePatch,
|
|
type DistributionBoardUpdateProjectCommand,
|
|
type DistributionBoardUpdateValues,
|
|
} from "../../domain/models/distribution-board-project-command.model.js";
|
|
import type {
|
|
DistributionBoardStructureProjectCommandStore,
|
|
ExecuteDistributionBoardStructureCommandInput,
|
|
} from "../../domain/ports/distribution-board-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 { distributionBoards } from "../schema/distribution-boards.js";
|
|
import { floors } from "../schema/floors.js";
|
|
import { projects } from "../schema/projects.js";
|
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
|
|
|
export class DistributionBoardStructureProjectCommandRepository
|
|
implements DistributionBoardStructureProjectCommandStore
|
|
{
|
|
constructor(private readonly database: AppDatabase) {}
|
|
|
|
execute(input: ExecuteDistributionBoardStructureCommandInput) {
|
|
return executeProjectCommandTransaction(
|
|
this.database,
|
|
input,
|
|
(tx) =>
|
|
this.applyCommand(tx, input.projectId, input.command)
|
|
);
|
|
}
|
|
|
|
executeUpdate(
|
|
input: Omit<
|
|
ExecuteDistributionBoardStructureCommandInput,
|
|
"command"
|
|
> & {
|
|
command: DistributionBoardUpdateProjectCommand;
|
|
}
|
|
) {
|
|
assertDistributionBoardUpdateProjectCommand(input.command);
|
|
return executeProjectCommandTransaction(
|
|
this.database,
|
|
input,
|
|
(tx) => this.update(tx, input)
|
|
);
|
|
}
|
|
|
|
private applyCommand(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
command: DistributionBoardStructureProjectCommand
|
|
): DistributionBoardStructureProjectCommand {
|
|
const structure =
|
|
normalizeDistributionBoardStructureProjectCommand(command);
|
|
if (command.type === distributionBoardInsertCommandType) {
|
|
this.insert(database, projectId, structure);
|
|
return invertDistributionBoardStructureProjectCommand(command);
|
|
}
|
|
if (command.type === distributionBoardDeleteCommandType) {
|
|
this.delete(database, projectId, structure);
|
|
return invertDistributionBoardStructureProjectCommand(command);
|
|
}
|
|
throw new Error("Unsupported distribution-board structure command.");
|
|
}
|
|
|
|
private insert(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
structure: NormalizedDistributionBoardStructureSnapshot
|
|
) {
|
|
if (
|
|
structure.distributionBoard.projectId !== projectId ||
|
|
structure.circuitList.projectId !== projectId
|
|
) {
|
|
throw new Error(
|
|
"Distribution-board structure does not belong to project."
|
|
);
|
|
}
|
|
const project = database
|
|
.select({
|
|
id: projects.id,
|
|
enabledSupplyTypes:
|
|
projects.enabledDistributionBoardSupplyTypes,
|
|
})
|
|
.from(projects)
|
|
.where(eq(projects.id, projectId))
|
|
.get();
|
|
if (!project) {
|
|
throw new Error("Project not found.");
|
|
}
|
|
assertSupplyTypeEnabled(
|
|
project.enabledSupplyTypes,
|
|
structure.distributionBoard.supplyType
|
|
);
|
|
if (structure.distributionBoard.floorId !== null) {
|
|
const floor = database
|
|
.select({ projectId: floors.projectId })
|
|
.from(floors)
|
|
.where(eq(floors.id, structure.distributionBoard.floorId))
|
|
.get();
|
|
if (!floor || floor.projectId !== projectId) {
|
|
throw new Error(
|
|
"Distribution-board floor does not belong to project."
|
|
);
|
|
}
|
|
}
|
|
const existingBoard = database
|
|
.select({ id: distributionBoards.id })
|
|
.from(distributionBoards)
|
|
.where(eq(distributionBoards.id, structure.distributionBoard.id))
|
|
.get();
|
|
const existingList = database
|
|
.select({ id: circuitLists.id })
|
|
.from(circuitLists)
|
|
.where(eq(circuitLists.id, structure.circuitList.id))
|
|
.get();
|
|
const existingSection = database
|
|
.select({ id: circuitSections.id })
|
|
.from(circuitSections)
|
|
.where(
|
|
inArray(
|
|
circuitSections.id,
|
|
structure.sections.map((section) => section.id)
|
|
)
|
|
)
|
|
.limit(1)
|
|
.get();
|
|
const existingComponent =
|
|
structure.components.length === 0
|
|
? undefined
|
|
: database
|
|
.select({ id: distributionBoardComponents.id })
|
|
.from(distributionBoardComponents)
|
|
.where(
|
|
inArray(
|
|
distributionBoardComponents.id,
|
|
structure.components.map((component) => component.id)
|
|
)
|
|
)
|
|
.limit(1)
|
|
.get();
|
|
if (
|
|
existingBoard ||
|
|
existingList ||
|
|
existingSection ||
|
|
existingComponent
|
|
) {
|
|
throw new Error("Distribution-board structure id already exists.");
|
|
}
|
|
|
|
database
|
|
.insert(distributionBoards)
|
|
.values(structure.distributionBoard)
|
|
.run();
|
|
database.insert(circuitLists).values(structure.circuitList).run();
|
|
database
|
|
.insert(circuitSections)
|
|
.values(structure.sections)
|
|
.run();
|
|
if (structure.components.length > 0) {
|
|
database
|
|
.insert(distributionBoardComponents)
|
|
.values(structure.components)
|
|
.run();
|
|
}
|
|
}
|
|
|
|
private delete(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
structure: NormalizedDistributionBoardStructureSnapshot
|
|
) {
|
|
const board = database
|
|
.select()
|
|
.from(distributionBoards)
|
|
.where(eq(distributionBoards.id, structure.distributionBoard.id))
|
|
.get();
|
|
const list = database
|
|
.select()
|
|
.from(circuitLists)
|
|
.where(eq(circuitLists.id, structure.circuitList.id))
|
|
.get();
|
|
const sections = database
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(
|
|
eq(circuitSections.circuitListId, structure.circuitList.id)
|
|
)
|
|
.orderBy(
|
|
asc(circuitSections.sortOrder),
|
|
asc(circuitSections.id)
|
|
)
|
|
.all();
|
|
const components = database
|
|
.select()
|
|
.from(distributionBoardComponents)
|
|
.where(
|
|
eq(
|
|
distributionBoardComponents.circuitListId,
|
|
structure.circuitList.id
|
|
)
|
|
)
|
|
.orderBy(
|
|
asc(distributionBoardComponents.sortOrder),
|
|
asc(distributionBoardComponents.id)
|
|
)
|
|
.all();
|
|
if (
|
|
!board ||
|
|
!list ||
|
|
board.projectId !== projectId ||
|
|
!structureMatches(structure, {
|
|
board,
|
|
list,
|
|
sections,
|
|
components,
|
|
})
|
|
) {
|
|
throw new Error(
|
|
"Distribution-board structure changed before deletion."
|
|
);
|
|
}
|
|
const populatedCircuit = database
|
|
.select({ id: circuits.id })
|
|
.from(circuits)
|
|
.where(eq(circuits.circuitListId, structure.circuitList.id))
|
|
.limit(1)
|
|
.get();
|
|
if (populatedCircuit) {
|
|
throw new Error(
|
|
"A populated distribution board cannot be removed by history."
|
|
);
|
|
}
|
|
|
|
const deleted = database
|
|
.delete(distributionBoards)
|
|
.where(
|
|
and(
|
|
eq(
|
|
distributionBoards.id,
|
|
structure.distributionBoard.id
|
|
),
|
|
eq(distributionBoards.projectId, projectId)
|
|
)
|
|
)
|
|
.run();
|
|
if (deleted.changes !== 1) {
|
|
throw new Error("Distribution board could not be deleted.");
|
|
}
|
|
}
|
|
|
|
private update(
|
|
database: AppDatabase,
|
|
input: Omit<
|
|
ExecuteDistributionBoardStructureCommandInput,
|
|
"command"
|
|
> & {
|
|
command: DistributionBoardUpdateProjectCommand;
|
|
}
|
|
) {
|
|
const current = database
|
|
.select()
|
|
.from(distributionBoards)
|
|
.where(
|
|
and(
|
|
eq(
|
|
distributionBoards.id,
|
|
input.command.payload.distributionBoardId
|
|
),
|
|
eq(distributionBoards.projectId, input.projectId)
|
|
)
|
|
)
|
|
.get();
|
|
if (!current) {
|
|
throw new Error("Distribution board does not belong to project.");
|
|
}
|
|
const project = database
|
|
.select({
|
|
enabledSupplyTypes:
|
|
projects.enabledDistributionBoardSupplyTypes,
|
|
})
|
|
.from(projects)
|
|
.where(eq(projects.id, input.projectId))
|
|
.get();
|
|
if (!project) {
|
|
throw new Error("Project not found.");
|
|
}
|
|
const patch = Object.fromEntries(
|
|
input.command.payload.changes.map((change) => [
|
|
change.field,
|
|
change.value,
|
|
])
|
|
) as DistributionBoardUpdatePatch;
|
|
if (patch.floorId !== undefined && patch.floorId !== null) {
|
|
const floor = database
|
|
.select({ projectId: floors.projectId })
|
|
.from(floors)
|
|
.where(eq(floors.id, patch.floorId))
|
|
.get();
|
|
if (!floor || floor.projectId !== input.projectId) {
|
|
throw new Error(
|
|
"Distribution-board floor does not belong to project."
|
|
);
|
|
}
|
|
}
|
|
if (patch.supplyType !== undefined) {
|
|
assertSupplyTypeEnabled(
|
|
project.enabledSupplyTypes,
|
|
patch.supplyType
|
|
);
|
|
}
|
|
const inversePatch = Object.fromEntries(
|
|
input.command.payload.changes.map((change) => [
|
|
change.field,
|
|
getDistributionBoardFieldValue(current, change.field),
|
|
])
|
|
) as DistributionBoardUpdatePatch;
|
|
const currentInverse = createDistributionBoardUpdateProjectCommand(
|
|
current.id,
|
|
inversePatch
|
|
);
|
|
const inverse =
|
|
input.command.schemaVersion ===
|
|
legacyDistributionBoardUpdateCommandSchemaVersion
|
|
? ({
|
|
...currentInverse,
|
|
schemaVersion:
|
|
legacyDistributionBoardUpdateCommandSchemaVersion,
|
|
} as DistributionBoardUpdateProjectCommand)
|
|
: currentInverse;
|
|
const updated = database
|
|
.update(distributionBoards)
|
|
.set(patch)
|
|
.where(
|
|
and(
|
|
eq(distributionBoards.id, current.id),
|
|
eq(distributionBoards.projectId, input.projectId)
|
|
)
|
|
)
|
|
.run();
|
|
if (updated.changes !== 1) {
|
|
throw new Error(
|
|
"Distribution board changed before command execution."
|
|
);
|
|
}
|
|
return inverse;
|
|
}
|
|
}
|
|
|
|
function assertSupplyTypeEnabled(
|
|
enabledSupplyTypes: (typeof projects.$inferSelect)["enabledDistributionBoardSupplyTypes"],
|
|
supplyType: (typeof distributionBoards.$inferSelect)["supplyType"]
|
|
) {
|
|
if (
|
|
supplyType !== null &&
|
|
!enabledSupplyTypes.includes(supplyType)
|
|
) {
|
|
throw new Error(
|
|
`Die Netzart ${supplyType} ist in den Projekteinstellungen nicht aktiviert.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function getDistributionBoardFieldValue<
|
|
TField extends DistributionBoardUpdateField,
|
|
>(
|
|
distributionBoard: typeof distributionBoards.$inferSelect,
|
|
field: TField
|
|
): DistributionBoardUpdateValues[TField] {
|
|
return distributionBoard[field] as DistributionBoardUpdateValues[TField];
|
|
}
|
|
|
|
function structureMatches(
|
|
expected: NormalizedDistributionBoardStructureSnapshot,
|
|
actual: {
|
|
board: typeof distributionBoards.$inferSelect;
|
|
list: typeof circuitLists.$inferSelect;
|
|
sections: Array<typeof circuitSections.$inferSelect>;
|
|
components: Array<typeof distributionBoardComponents.$inferSelect>;
|
|
}
|
|
) {
|
|
const {
|
|
simultaneityFactor,
|
|
...actualDistributionBoard
|
|
} = actual.board;
|
|
if (
|
|
simultaneityFactor !== 1 ||
|
|
!sameRecord(
|
|
expected.distributionBoard,
|
|
actualDistributionBoard
|
|
) ||
|
|
!sameRecord(expected.circuitList, actual.list) ||
|
|
expected.sections.length !== actual.sections.length ||
|
|
expected.components.length !== actual.components.length
|
|
) {
|
|
return false;
|
|
}
|
|
const expectedSections = [...expected.sections].sort(
|
|
(left, right) =>
|
|
left.sortOrder - right.sortOrder ||
|
|
left.id.localeCompare(right.id)
|
|
);
|
|
const sectionsMatch = expectedSections.every((section, index) =>
|
|
sameRecord(section, actual.sections[index])
|
|
);
|
|
const expectedComponents = [...expected.components].sort(
|
|
(left, right) =>
|
|
left.sortOrder - right.sortOrder ||
|
|
left.id.localeCompare(right.id)
|
|
);
|
|
return (
|
|
sectionsMatch &&
|
|
expectedComponents.every((component, index) =>
|
|
sameRecord(component, actual.components[index])
|
|
)
|
|
);
|
|
}
|
|
|
|
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
|
|
)
|
|
);
|
|
}
|