Add atomic device row updates
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
assertCircuitDeviceRowUpdateProjectCommand,
|
||||
createCircuitDeviceRowUpdateProjectCommand,
|
||||
type CircuitDeviceRowUpdateField,
|
||||
type CircuitDeviceRowUpdatePatch,
|
||||
type CircuitDeviceRowUpdateValues,
|
||||
} from "../../domain/models/circuit-device-row-project-command.model.js";
|
||||
import type {
|
||||
CircuitDeviceRowProjectCommandStore,
|
||||
ExecuteCircuitDeviceRowUpdateCommandInput,
|
||||
} from "../../domain/ports/circuit-device-row-project-command.store.js";
|
||||
import { deriveOverriddenFieldsForLocalEdit } from "../../domain/services/project-device-overrides.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||
import { circuitLists } from "../schema/circuit-lists.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import {
|
||||
toCircuitDeviceRowPatchValues,
|
||||
type CircuitDeviceRowPatchInput,
|
||||
} from "./circuit-device-row.persistence.js";
|
||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
||||
|
||||
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
||||
|
||||
export class CircuitDeviceRowProjectCommandRepository
|
||||
implements CircuitDeviceRowProjectCommandStore
|
||||
{
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
executeUpdate(input: ExecuteCircuitDeviceRowUpdateCommandInput) {
|
||||
assertCircuitDeviceRowUpdateProjectCommand(input.command);
|
||||
|
||||
return this.database.transaction((tx) => {
|
||||
const current = tx
|
||||
.select()
|
||||
.from(circuitDeviceRows)
|
||||
.where(eq(circuitDeviceRows.id, input.command.payload.rowId))
|
||||
.get();
|
||||
if (!current) {
|
||||
throw new Error("Invalid device row id.");
|
||||
}
|
||||
|
||||
const circuit = tx
|
||||
.select({ circuitListId: circuits.circuitListId })
|
||||
.from(circuits)
|
||||
.where(eq(circuits.id, current.circuitId))
|
||||
.get();
|
||||
const owningList = circuit
|
||||
? tx
|
||||
.select({ id: circuitLists.id })
|
||||
.from(circuitLists)
|
||||
.where(
|
||||
and(
|
||||
eq(circuitLists.id, circuit.circuitListId),
|
||||
eq(circuitLists.projectId, input.projectId)
|
||||
)
|
||||
)
|
||||
.get()
|
||||
: null;
|
||||
if (!owningList) {
|
||||
throw new Error("Circuit device row does not belong to project.");
|
||||
}
|
||||
|
||||
const patch = Object.fromEntries(
|
||||
input.command.payload.changes.map((change) => [
|
||||
change.field,
|
||||
change.value,
|
||||
])
|
||||
) as CircuitDeviceRowPatchInput;
|
||||
this.assertLinkedProjectDevice(tx, input.projectId, patch);
|
||||
this.assertRoom(tx, input.projectId, patch);
|
||||
|
||||
const overriddenFields = deriveOverriddenFieldsForLocalEdit(
|
||||
current,
|
||||
patch
|
||||
);
|
||||
if (
|
||||
overriddenFields !== undefined &&
|
||||
patch.overriddenFields === undefined
|
||||
) {
|
||||
patch.overriddenFields = overriddenFields;
|
||||
}
|
||||
|
||||
const appliedForward = createCircuitDeviceRowUpdateProjectCommand(
|
||||
current.id,
|
||||
patch as CircuitDeviceRowUpdatePatch
|
||||
);
|
||||
const inversePatch = Object.fromEntries(
|
||||
appliedForward.payload.changes.map((change) => [
|
||||
change.field,
|
||||
getCircuitDeviceRowFieldValue(current, change.field),
|
||||
])
|
||||
) as CircuitDeviceRowUpdatePatch;
|
||||
const inverse = createCircuitDeviceRowUpdateProjectCommand(
|
||||
current.id,
|
||||
inversePatch
|
||||
);
|
||||
|
||||
const update = tx
|
||||
.update(circuitDeviceRows)
|
||||
.set(toCircuitDeviceRowPatchValues(patch))
|
||||
.where(eq(circuitDeviceRows.id, current.id))
|
||||
.run();
|
||||
if (update.changes !== 1) {
|
||||
throw new Error("Circuit device row changed before command execution.");
|
||||
}
|
||||
|
||||
const revision = appendProjectRevision(tx, {
|
||||
projectId: input.projectId,
|
||||
expectedRevision: input.expectedRevision,
|
||||
source: input.source,
|
||||
description: input.description,
|
||||
actorId: input.actorId,
|
||||
forward: appliedForward,
|
||||
inverse,
|
||||
});
|
||||
|
||||
return { revision, inverse };
|
||||
});
|
||||
}
|
||||
|
||||
private assertLinkedProjectDevice(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
patch: CircuitDeviceRowPatchInput
|
||||
) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(patch, "linkedProjectDeviceId") ||
|
||||
patch.linkedProjectDeviceId === null ||
|
||||
patch.linkedProjectDeviceId === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const device = database
|
||||
.select({ id: projectDevices.id })
|
||||
.from(projectDevices)
|
||||
.where(
|
||||
and(
|
||||
eq(projectDevices.id, patch.linkedProjectDeviceId),
|
||||
eq(projectDevices.projectId, projectId)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
if (!device) {
|
||||
throw new Error("Invalid linked project device id.");
|
||||
}
|
||||
}
|
||||
|
||||
private assertRoom(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
patch: CircuitDeviceRowPatchInput
|
||||
) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(patch, "roomId") ||
|
||||
patch.roomId === null ||
|
||||
patch.roomId === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const room = database
|
||||
.select({ id: rooms.id })
|
||||
.from(rooms)
|
||||
.where(and(eq(rooms.id, patch.roomId), eq(rooms.projectId, projectId)))
|
||||
.get();
|
||||
if (!room) {
|
||||
throw new Error("Invalid room id.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCircuitDeviceRowFieldValue<
|
||||
TField extends CircuitDeviceRowUpdateField,
|
||||
>(
|
||||
row: CircuitDeviceRow,
|
||||
field: TField
|
||||
): CircuitDeviceRowUpdateValues[TField] {
|
||||
return row[field] as unknown as CircuitDeviceRowUpdateValues[TField];
|
||||
}
|
||||
Reference in New Issue
Block a user