forked from jappel/leistungsbilanz-ts
276 lines
7.9 KiB
TypeScript
276 lines
7.9 KiB
TypeScript
import { and, eq } from "drizzle-orm";
|
|
import {
|
|
assertProjectFloorDeleteProjectCommand,
|
|
assertProjectFloorInsertProjectCommand,
|
|
assertProjectRoomDeleteProjectCommand,
|
|
assertProjectRoomInsertProjectCommand,
|
|
createProjectFloorDeleteProjectCommand,
|
|
createProjectFloorInsertProjectCommand,
|
|
createProjectRoomDeleteProjectCommand,
|
|
createProjectRoomInsertProjectCommand,
|
|
projectFloorDeleteCommandType,
|
|
projectFloorInsertCommandType,
|
|
projectRoomDeleteCommandType,
|
|
projectRoomInsertCommandType,
|
|
type ProjectFloorSnapshot,
|
|
type ProjectLocationStructureProjectCommand,
|
|
type ProjectRoomSnapshot,
|
|
} from "../../domain/models/project-location-structure-project-command.model.js";
|
|
import type {
|
|
ExecuteProjectLocationStructureCommandInput,
|
|
ProjectLocationStructureProjectCommandStore,
|
|
} from "../../domain/ports/project-location-structure-project-command.store.js";
|
|
import type { AppDatabase } from "../database-context.js";
|
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
|
import { consumers } from "../schema/consumers.js";
|
|
import { floors } from "../schema/floors.js";
|
|
import { projects } from "../schema/projects.js";
|
|
import { rooms } from "../schema/rooms.js";
|
|
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
|
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
|
|
export class ProjectLocationStructureProjectCommandRepository
|
|
implements ProjectLocationStructureProjectCommandStore
|
|
{
|
|
constructor(private readonly database: AppDatabase) {}
|
|
|
|
execute(input: ExecuteProjectLocationStructureCommandInput) {
|
|
return this.database.transaction((tx) => {
|
|
const inverse = this.applyCommand(
|
|
tx,
|
|
input.projectId,
|
|
input.command
|
|
);
|
|
const revision = appendProjectRevision(tx, {
|
|
projectId: input.projectId,
|
|
expectedRevision: input.expectedRevision,
|
|
source: input.source,
|
|
description: input.description,
|
|
actorId: input.actorId,
|
|
forward: input.command,
|
|
inverse,
|
|
});
|
|
applyProjectHistoryTransition(tx, {
|
|
projectId: input.projectId,
|
|
source: input.source,
|
|
recordedChangeSetId: revision.changeSetId,
|
|
targetChangeSetId: input.historyTargetChangeSetId,
|
|
});
|
|
return { revision, inverse };
|
|
});
|
|
}
|
|
|
|
private applyCommand(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
command: ProjectLocationStructureProjectCommand
|
|
): ProjectLocationStructureProjectCommand {
|
|
switch (command.type) {
|
|
case projectFloorInsertCommandType:
|
|
assertProjectFloorInsertProjectCommand(command);
|
|
return this.insertFloor(
|
|
database,
|
|
projectId,
|
|
command.payload.floor
|
|
);
|
|
case projectFloorDeleteCommandType:
|
|
assertProjectFloorDeleteProjectCommand(command);
|
|
return this.deleteFloor(
|
|
database,
|
|
projectId,
|
|
command.payload.floor
|
|
);
|
|
case projectRoomInsertCommandType:
|
|
assertProjectRoomInsertProjectCommand(command);
|
|
return this.insertRoom(
|
|
database,
|
|
projectId,
|
|
command.payload.room
|
|
);
|
|
case projectRoomDeleteCommandType:
|
|
assertProjectRoomDeleteProjectCommand(command);
|
|
return this.deleteRoom(
|
|
database,
|
|
projectId,
|
|
command.payload.room
|
|
);
|
|
}
|
|
}
|
|
|
|
private insertFloor(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
floor: ProjectFloorSnapshot
|
|
) {
|
|
this.assertProjectExists(database, projectId);
|
|
if (floor.projectId !== projectId) {
|
|
throw new Error("Project-floor snapshot does not belong to project.");
|
|
}
|
|
const existing = database
|
|
.select({ id: floors.id })
|
|
.from(floors)
|
|
.where(eq(floors.id, floor.id))
|
|
.get();
|
|
if (existing) {
|
|
throw new Error("Project-floor id already exists.");
|
|
}
|
|
database.insert(floors).values(floor).run();
|
|
return createProjectFloorDeleteProjectCommand(floor);
|
|
}
|
|
|
|
private deleteFloor(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
expected: ProjectFloorSnapshot
|
|
) {
|
|
const persisted = database
|
|
.select()
|
|
.from(floors)
|
|
.where(
|
|
and(
|
|
eq(floors.id, expected.id),
|
|
eq(floors.projectId, projectId)
|
|
)
|
|
)
|
|
.get();
|
|
if (!persisted || !sameRecord(expected, persisted)) {
|
|
throw new Error("Project floor changed before deletion.");
|
|
}
|
|
const referencedRoom = database
|
|
.select({ id: rooms.id })
|
|
.from(rooms)
|
|
.where(eq(rooms.floorId, expected.id))
|
|
.limit(1)
|
|
.get();
|
|
if (referencedRoom) {
|
|
throw new Error(
|
|
"A project floor with assigned rooms cannot be removed by history."
|
|
);
|
|
}
|
|
const deleted = database
|
|
.delete(floors)
|
|
.where(
|
|
and(
|
|
eq(floors.id, expected.id),
|
|
eq(floors.projectId, projectId)
|
|
)
|
|
)
|
|
.run();
|
|
if (deleted.changes !== 1) {
|
|
throw new Error("Project floor could not be deleted.");
|
|
}
|
|
return createProjectFloorInsertProjectCommand(expected);
|
|
}
|
|
|
|
private insertRoom(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
room: ProjectRoomSnapshot
|
|
) {
|
|
this.assertProjectExists(database, projectId);
|
|
if (room.projectId !== projectId) {
|
|
throw new Error("Project-room snapshot does not belong to project.");
|
|
}
|
|
if (room.floorId) {
|
|
const floor = database
|
|
.select({ id: floors.id })
|
|
.from(floors)
|
|
.where(
|
|
and(
|
|
eq(floors.id, room.floorId),
|
|
eq(floors.projectId, projectId)
|
|
)
|
|
)
|
|
.get();
|
|
if (!floor) {
|
|
throw new Error("Project room references a foreign floor.");
|
|
}
|
|
}
|
|
const existing = database
|
|
.select({ id: rooms.id })
|
|
.from(rooms)
|
|
.where(eq(rooms.id, room.id))
|
|
.get();
|
|
if (existing) {
|
|
throw new Error("Project-room id already exists.");
|
|
}
|
|
database.insert(rooms).values(room).run();
|
|
return createProjectRoomDeleteProjectCommand(room);
|
|
}
|
|
|
|
private deleteRoom(
|
|
database: AppDatabase,
|
|
projectId: string,
|
|
expected: ProjectRoomSnapshot
|
|
) {
|
|
const persisted = database
|
|
.select()
|
|
.from(rooms)
|
|
.where(
|
|
and(
|
|
eq(rooms.id, expected.id),
|
|
eq(rooms.projectId, projectId)
|
|
)
|
|
)
|
|
.get();
|
|
if (!persisted || !sameRecord(expected, persisted)) {
|
|
throw new Error("Project room changed before deletion.");
|
|
}
|
|
const referencedDeviceRow = database
|
|
.select({ id: circuitDeviceRows.id })
|
|
.from(circuitDeviceRows)
|
|
.where(eq(circuitDeviceRows.roomId, expected.id))
|
|
.limit(1)
|
|
.get();
|
|
const referencedLegacyConsumer = database
|
|
.select({ id: consumers.id })
|
|
.from(consumers)
|
|
.where(eq(consumers.roomId, expected.id))
|
|
.limit(1)
|
|
.get();
|
|
if (referencedDeviceRow || referencedLegacyConsumer) {
|
|
throw new Error(
|
|
"A referenced project room cannot be removed by history."
|
|
);
|
|
}
|
|
const deleted = database
|
|
.delete(rooms)
|
|
.where(
|
|
and(
|
|
eq(rooms.id, expected.id),
|
|
eq(rooms.projectId, projectId)
|
|
)
|
|
)
|
|
.run();
|
|
if (deleted.changes !== 1) {
|
|
throw new Error("Project room could not be deleted.");
|
|
}
|
|
return createProjectRoomInsertProjectCommand(expected);
|
|
}
|
|
|
|
private assertProjectExists(
|
|
database: AppDatabase,
|
|
projectId: string
|
|
) {
|
|
const project = database
|
|
.select({ id: projects.id })
|
|
.from(projects)
|
|
.where(eq(projects.id, projectId))
|
|
.get();
|
|
if (!project) {
|
|
throw new Error("Project does not exist.");
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
);
|
|
}
|