221 lines
6.6 KiB
TypeScript
221 lines
6.6 KiB
TypeScript
import { and, asc, eq, inArray } from "drizzle-orm";
|
|
import {
|
|
assertDistributionBoardDeleteProjectCommand,
|
|
assertDistributionBoardInsertProjectCommand,
|
|
createDistributionBoardDeleteProjectCommand,
|
|
createDistributionBoardInsertProjectCommand,
|
|
distributionBoardDeleteCommandType,
|
|
distributionBoardInsertCommandType,
|
|
type DistributionBoardStructureProjectCommand,
|
|
type DistributionBoardStructureSnapshot,
|
|
} from "../../domain/models/distribution-board-structure-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 { distributionBoards } from "../schema/distribution-boards.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)
|
|
);
|
|
}
|
|
|
|
private applyCommand(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
command: DistributionBoardStructureProjectCommand
|
|
): DistributionBoardStructureProjectCommand {
|
|
if (command.type === distributionBoardInsertCommandType) {
|
|
assertDistributionBoardInsertProjectCommand(command);
|
|
return this.insert(
|
|
database,
|
|
projectId,
|
|
command.payload.structure
|
|
);
|
|
}
|
|
if (command.type === distributionBoardDeleteCommandType) {
|
|
assertDistributionBoardDeleteProjectCommand(command);
|
|
return this.delete(
|
|
database,
|
|
projectId,
|
|
command.payload.structure
|
|
);
|
|
}
|
|
throw new Error("Unsupported distribution-board structure command.");
|
|
}
|
|
|
|
private insert(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
structure: DistributionBoardStructureSnapshot
|
|
) {
|
|
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 })
|
|
.from(projects)
|
|
.where(eq(projects.id, projectId))
|
|
.get();
|
|
if (!project) {
|
|
throw new Error("Project not found.");
|
|
}
|
|
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();
|
|
if (existingBoard || existingList || existingSection) {
|
|
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();
|
|
return createDistributionBoardDeleteProjectCommand(structure);
|
|
}
|
|
|
|
private delete(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
structure: DistributionBoardStructureSnapshot
|
|
) {
|
|
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();
|
|
if (
|
|
!board ||
|
|
!list ||
|
|
board.projectId !== projectId ||
|
|
!structureMatches(structure, { board, list, sections })
|
|
) {
|
|
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.");
|
|
}
|
|
return createDistributionBoardInsertProjectCommand(structure);
|
|
}
|
|
}
|
|
|
|
function structureMatches(
|
|
expected: DistributionBoardStructureSnapshot,
|
|
actual: {
|
|
board: typeof distributionBoards.$inferSelect;
|
|
list: typeof circuitLists.$inferSelect;
|
|
sections: Array<typeof circuitSections.$inferSelect>;
|
|
}
|
|
) {
|
|
if (
|
|
!sameRecord(expected.distributionBoard, actual.board) ||
|
|
!sameRecord(expected.circuitList, actual.list) ||
|
|
expected.sections.length !== actual.sections.length
|
|
) {
|
|
return false;
|
|
}
|
|
const expectedSections = [...expected.sections].sort(
|
|
(left, right) =>
|
|
left.sortOrder - right.sortOrder ||
|
|
left.id.localeCompare(right.id)
|
|
);
|
|
return expectedSections.every((section, index) =>
|
|
sameRecord(section, actual.sections[index])
|
|
);
|
|
}
|
|
|
|
function sameRecord(
|
|
expected: Record<string, unknown>,
|
|
actual: Record<string, unknown>
|
|
) {
|
|
const expectedEntries = Object.entries(expected);
|
|
return (
|
|
expectedEntries.length === Object.keys(actual).length &&
|
|
expectedEntries.every(
|
|
([key, value]) => actual[key] === value
|
|
)
|
|
);
|
|
}
|