Add external object new circuit command
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js";
|
||||
import {
|
||||
assertCircuitInsertProjectCommand,
|
||||
circuitInsertCommandType,
|
||||
circuitStructureCommandSchemaVersion,
|
||||
type CircuitSnapshot,
|
||||
} from "./circuit-structure-project-command.model.js";
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js";
|
||||
|
||||
export const externalObjectAssignToNewCircuitCommandType =
|
||||
"external-object.assign-to-new-circuit" as const;
|
||||
export const externalObjectDeleteCreatedCircuitCommandType =
|
||||
"external-object.unassign-and-delete-created-circuit" as const;
|
||||
export const externalObjectNewCircuitCommandSchemaVersion = 1 as const;
|
||||
|
||||
export interface ExternalObjectNewCircuitPayload {
|
||||
circuit: CircuitSnapshot;
|
||||
objects: Array<{
|
||||
expected: ExternalModelObjectSnapshot;
|
||||
target: ExternalModelObjectSnapshot;
|
||||
}>;
|
||||
confirmedConflictObjectIds: string[];
|
||||
}
|
||||
|
||||
export interface ExternalObjectNewCircuitProjectCommand
|
||||
extends SerializedProjectCommand<ExternalObjectNewCircuitPayload> {
|
||||
schemaVersion: typeof externalObjectNewCircuitCommandSchemaVersion;
|
||||
type:
|
||||
| typeof externalObjectAssignToNewCircuitCommandType
|
||||
| typeof externalObjectDeleteCreatedCircuitCommandType;
|
||||
}
|
||||
|
||||
export function createExternalObjectNewCircuitProjectCommand(
|
||||
type: ExternalObjectNewCircuitProjectCommand["type"],
|
||||
payload: ExternalObjectNewCircuitPayload
|
||||
): ExternalObjectNewCircuitProjectCommand {
|
||||
const row = payload.circuit.deviceRows[0];
|
||||
const command: ExternalObjectNewCircuitProjectCommand = {
|
||||
schemaVersion: externalObjectNewCircuitCommandSchemaVersion,
|
||||
type,
|
||||
payload: {
|
||||
circuit: row
|
||||
? {
|
||||
...payload.circuit,
|
||||
deviceRows: [{
|
||||
...row,
|
||||
manualQuantity: row.manualQuantity ?? row.quantity,
|
||||
}],
|
||||
}
|
||||
: payload.circuit,
|
||||
objects: payload.objects,
|
||||
confirmedConflictObjectIds: [...payload.confirmedConflictObjectIds],
|
||||
},
|
||||
};
|
||||
assertExternalObjectNewCircuitProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function invertExternalObjectNewCircuitProjectCommand(
|
||||
command: ExternalObjectNewCircuitProjectCommand
|
||||
) {
|
||||
return createExternalObjectNewCircuitProjectCommand(
|
||||
command.type === externalObjectAssignToNewCircuitCommandType
|
||||
? externalObjectDeleteCreatedCircuitCommandType
|
||||
: externalObjectAssignToNewCircuitCommandType,
|
||||
{
|
||||
circuit: command.payload.circuit,
|
||||
objects: command.payload.objects.map(({ expected, target }) => ({
|
||||
expected: target,
|
||||
target: expected,
|
||||
})),
|
||||
confirmedConflictObjectIds: command.payload.confirmedConflictObjectIds,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function assertExternalObjectNewCircuitProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is ExternalObjectNewCircuitProjectCommand {
|
||||
if (
|
||||
command.schemaVersion !== externalObjectNewCircuitCommandSchemaVersion ||
|
||||
(command.type !== externalObjectAssignToNewCircuitCommandType &&
|
||||
command.type !== externalObjectDeleteCreatedCircuitCommandType) ||
|
||||
!isRecord(command.payload) ||
|
||||
!Array.isArray(command.payload.objects) ||
|
||||
command.payload.objects.length === 0 ||
|
||||
!Array.isArray(command.payload.confirmedConflictObjectIds)
|
||||
) {
|
||||
throw new Error("Unsupported external object new-circuit command.");
|
||||
}
|
||||
assertCircuitInsertProjectCommand({
|
||||
schemaVersion: circuitStructureCommandSchemaVersion,
|
||||
type: circuitInsertCommandType,
|
||||
payload: { circuit: command.payload.circuit },
|
||||
});
|
||||
const circuit = command.payload.circuit as CircuitSnapshot;
|
||||
if (
|
||||
circuit.deviceRows.length !== 1 ||
|
||||
circuit.deviceRows[0]!.manualQuantity !== 0 ||
|
||||
circuit.protectionDevice === undefined ||
|
||||
circuit.protectionDevice === null
|
||||
) {
|
||||
throw new Error(
|
||||
"An external circuit must contain one external-only row and protection device."
|
||||
);
|
||||
}
|
||||
const rowId = circuit.deviceRows[0]!.id;
|
||||
const objectIds = new Set<string>();
|
||||
for (const transition of command.payload.objects) {
|
||||
if (!isRecord(transition)) throw new Error("Invalid external object transition.");
|
||||
const expected = parseObject(transition.expected);
|
||||
const target = parseObject(transition.target);
|
||||
if (expected.id !== target.id || objectIds.has(expected.id)) {
|
||||
throw new Error("New-circuit assignment contains mismatched or duplicate objects.");
|
||||
}
|
||||
objectIds.add(expected.id);
|
||||
const assigning = command.type === externalObjectAssignToNewCircuitCommandType;
|
||||
if (
|
||||
(assigning && (expected.circuitDeviceRowId !== null || target.circuitDeviceRowId !== rowId)) ||
|
||||
(!assigning && (expected.circuitDeviceRowId !== rowId || target.circuitDeviceRowId !== null))
|
||||
) {
|
||||
throw new Error("External object links do not match the new-circuit action.");
|
||||
}
|
||||
assertOnlyAssignmentChanged(expected, target);
|
||||
}
|
||||
const confirmedIds = new Set<string>();
|
||||
for (const objectId of command.payload.confirmedConflictObjectIds) {
|
||||
if (typeof objectId !== "string" || !objectIds.has(objectId) || confirmedIds.has(objectId)) {
|
||||
throw new Error("New-circuit assignment contains an invalid conflict confirmation.");
|
||||
}
|
||||
confirmedIds.add(objectId);
|
||||
}
|
||||
}
|
||||
|
||||
function parseObject(value: unknown): ExternalModelObjectSnapshot {
|
||||
return parseExternalModelStateSnapshot({
|
||||
source: null,
|
||||
importBatches: [],
|
||||
roomMappings: [],
|
||||
objects: [value],
|
||||
}).objects[0]!;
|
||||
}
|
||||
|
||||
function assertOnlyAssignmentChanged(
|
||||
expected: ExternalModelObjectSnapshot,
|
||||
target: ExternalModelObjectSnapshot
|
||||
) {
|
||||
const expectedRest = { ...expected, circuitDeviceRowId: null };
|
||||
const targetRest = { ...target, circuitDeviceRowId: null };
|
||||
if (canonicalJson(expectedRest) !== canonicalJson(targetRest)) {
|
||||
throw new Error("New-circuit assignment may only change the external object row link.");
|
||||
}
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
||||
if (isRecord(value)) {
|
||||
return `{${Object.keys(value).sort().map((key) =>
|
||||
`${JSON.stringify(key)}:${canonicalJson(value[key])}`
|
||||
).join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user