leistungsbilanz-ts/src/db/repositories/project-device-project-command.repository.ts

134 lines
3.8 KiB
TypeScript

import { and, eq } from "drizzle-orm";
import {
assertProjectDeviceUpdateProjectCommand,
createProjectDeviceUpdateProjectCommand,
type ProjectDeviceUpdateField,
type ProjectDeviceUpdatePatch,
type ProjectDeviceUpdateValues,
} from "../../domain/models/project-device-project-command.model.js";
import type {
ExecuteProjectDeviceUpdateCommandInput,
ProjectDeviceProjectCommandStore,
} from "../../domain/ports/project-device-project-command.store.js";
import type { AppDatabase } from "../database-context.js";
import { projectDevices } from "../schema/project-devices.js";
import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js";
import { resolveProjectDeviceVoltage } from "./project-voltage.persistence.js";
type ProjectDeviceRow = typeof projectDevices.$inferSelect;
export class ProjectDeviceProjectCommandRepository
implements ProjectDeviceProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
executeUpdate(input: ExecuteProjectDeviceUpdateCommandInput) {
assertProjectDeviceUpdateProjectCommand(input.command);
return executeProjectCommandTransactionWithAppliedForward(
this.database,
input,
(tx) => this.applyCommand(tx, input)
);
}
private applyCommand(
tx: AppDatabase,
input: ExecuteProjectDeviceUpdateCommandInput
) {
const current = tx
.select()
.from(projectDevices)
.where(
and(
eq(
projectDevices.id,
input.command.payload.projectDeviceId
),
eq(projectDevices.projectId, input.projectId)
)
)
.get();
if (!current) {
throw new Error(
"Project device does not belong to project."
);
}
const patch = Object.fromEntries(
input.command.payload.changes.map((change) => [
change.field,
change.value,
])
) as ProjectDeviceUpdatePatch;
if (input.source === "user") {
if (
input.command.payload.changes.every(
(change) => change.field === "voltageV"
)
) {
throw new Error(
"Project-device voltage is derived and cannot be edited."
);
}
const targetPhaseType = patch.phaseType ?? current.phaseType;
if (
targetPhaseType !== "single_phase" &&
targetPhaseType !== "three_phase"
) {
throw new Error("Project-device phase type is invalid.");
}
const derivedVoltage = resolveProjectDeviceVoltage(
tx,
input.projectId,
targetPhaseType
);
if (derivedVoltage === current.voltageV) {
delete patch.voltageV;
} else {
patch.voltageV = derivedVoltage;
}
}
const appliedForward = createProjectDeviceUpdateProjectCommand(
current.id,
patch
);
const inversePatch = Object.fromEntries(
appliedForward.payload.changes.map((change) => [
change.field,
getProjectDeviceFieldValue(current, change.field),
])
) as ProjectDeviceUpdatePatch;
const inverse = createProjectDeviceUpdateProjectCommand(
current.id,
inversePatch
);
const updated = tx
.update(projectDevices)
.set(patch)
.where(
and(
eq(projectDevices.id, current.id),
eq(projectDevices.projectId, input.projectId)
)
)
.run();
if (updated.changes !== 1) {
throw new Error(
"Project device changed before command execution."
);
}
return { forward: appliedForward, inverse };
}
}
function getProjectDeviceFieldValue<
TField extends ProjectDeviceUpdateField,
>(
projectDevice: ProjectDeviceRow,
field: TField
): ProjectDeviceUpdateValues[TField] {
return projectDevice[field] as ProjectDeviceUpdateValues[TField];
}