leistungsbilanz-ts/src/domain/models/project-location-structure-project-command.model.ts

803 lines
26 KiB
TypeScript

import crypto from "node:crypto";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const projectBuildingInsertCommandType = "project-building.insert" as const;
export const projectBuildingDeleteCommandType = "project-building.delete" as const;
export const projectBuildingUpdateCommandType = "project-building.update" as const;
export const projectFloorInsertCommandType = "project-floor.insert" as const;
export const projectFloorDeleteCommandType = "project-floor.delete" as const;
export const projectFloorUpdateCommandType = "project-floor.update" as const;
export const projectRoomInsertCommandType = "project-room.insert" as const;
export const projectRoomDeleteCommandType = "project-room.delete" as const;
export const projectRoomUpdateCommandType = "project-room.update" as const;
/** Building commands and legacy floor/room commands. */
export const projectLocationStructureCommandSchemaVersion = 1 as const;
/**
* Floor commands carry `buildingId` since version 2. Version 1 floor
* commands stay executable from persisted history: they were recorded before
* buildings existed, so they are upgraded with `buildingId = null`.
*/
export const projectFloorCommandSchemaVersion = 2 as const;
const legacyProjectFloorCommandSchemaVersion = 1 as const;
/** Room commands carry `areaM2` since version 2. */
export const projectRoomCommandSchemaVersion = 2 as const;
const legacyProjectRoomCommandSchemaVersion = 1 as const;
export interface ProjectBuildingSnapshot {
id: string;
projectId: string;
name: string;
sortOrder: number;
}
export interface ProjectFloorSnapshot {
id: string;
projectId: string;
name: string;
sortOrder: number;
buildingId: string | null;
}
export type LegacyProjectFloorSnapshot = Omit<ProjectFloorSnapshot, "buildingId">;
export interface ProjectRoomSnapshot {
id: string;
projectId: string;
floorId: string | null;
roomNumber: string;
roomName: string;
areaM2: number | null;
}
export type LegacyProjectRoomSnapshot = Omit<ProjectRoomSnapshot, "areaM2">;
interface ProjectBuildingStructureCommandPayload {
building: ProjectBuildingSnapshot;
}
interface ProjectFloorStructureCommandPayload {
floor: ProjectFloorSnapshot;
}
interface LegacyProjectFloorStructureCommandPayload {
floor: LegacyProjectFloorSnapshot;
}
interface ProjectRoomStructureCommandPayload {
room: ProjectRoomSnapshot;
}
interface LegacyProjectRoomStructureCommandPayload {
room: LegacyProjectRoomSnapshot;
}
interface ProjectBuildingUpdateCommandPayload {
expected: ProjectBuildingSnapshot;
target: ProjectBuildingSnapshot;
}
interface ProjectFloorUpdateCommandPayload {
expected: ProjectFloorSnapshot;
target: ProjectFloorSnapshot;
}
interface LegacyProjectFloorUpdateCommandPayload {
expected: LegacyProjectFloorSnapshot;
target: LegacyProjectFloorSnapshot;
}
interface ProjectRoomUpdateCommandPayload {
expected: ProjectRoomSnapshot;
target: ProjectRoomSnapshot;
}
interface LegacyProjectRoomUpdateCommandPayload {
expected: LegacyProjectRoomSnapshot;
target: LegacyProjectRoomSnapshot;
}
export interface ProjectBuildingInsertProjectCommand
extends SerializedProjectCommand<ProjectBuildingStructureCommandPayload> {
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
type: typeof projectBuildingInsertCommandType;
}
export interface ProjectBuildingDeleteProjectCommand
extends SerializedProjectCommand<ProjectBuildingStructureCommandPayload> {
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
type: typeof projectBuildingDeleteCommandType;
}
export interface ProjectBuildingUpdateProjectCommand
extends SerializedProjectCommand<ProjectBuildingUpdateCommandPayload> {
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
type: typeof projectBuildingUpdateCommandType;
}
export interface ProjectFloorInsertProjectCommand
extends SerializedProjectCommand<ProjectFloorStructureCommandPayload> {
schemaVersion: typeof projectFloorCommandSchemaVersion;
type: typeof projectFloorInsertCommandType;
}
export interface ProjectFloorDeleteProjectCommand
extends SerializedProjectCommand<ProjectFloorStructureCommandPayload> {
schemaVersion: typeof projectFloorCommandSchemaVersion;
type: typeof projectFloorDeleteCommandType;
}
export interface ProjectFloorUpdateProjectCommand
extends SerializedProjectCommand<ProjectFloorUpdateCommandPayload> {
schemaVersion: typeof projectFloorCommandSchemaVersion;
type: typeof projectFloorUpdateCommandType;
}
export interface LegacyProjectFloorInsertProjectCommand
extends SerializedProjectCommand<LegacyProjectFloorStructureCommandPayload> {
schemaVersion: typeof legacyProjectFloorCommandSchemaVersion;
type: typeof projectFloorInsertCommandType;
}
export interface LegacyProjectFloorDeleteProjectCommand
extends SerializedProjectCommand<LegacyProjectFloorStructureCommandPayload> {
schemaVersion: typeof legacyProjectFloorCommandSchemaVersion;
type: typeof projectFloorDeleteCommandType;
}
export interface LegacyProjectFloorUpdateProjectCommand
extends SerializedProjectCommand<LegacyProjectFloorUpdateCommandPayload> {
schemaVersion: typeof legacyProjectFloorCommandSchemaVersion;
type: typeof projectFloorUpdateCommandType;
}
export interface ProjectRoomInsertProjectCommand
extends SerializedProjectCommand<ProjectRoomStructureCommandPayload> {
schemaVersion: typeof projectRoomCommandSchemaVersion;
type: typeof projectRoomInsertCommandType;
}
export interface ProjectRoomDeleteProjectCommand
extends SerializedProjectCommand<ProjectRoomStructureCommandPayload> {
schemaVersion: typeof projectRoomCommandSchemaVersion;
type: typeof projectRoomDeleteCommandType;
}
export interface ProjectRoomUpdateProjectCommand
extends SerializedProjectCommand<ProjectRoomUpdateCommandPayload> {
schemaVersion: typeof projectRoomCommandSchemaVersion;
type: typeof projectRoomUpdateCommandType;
}
export interface LegacyProjectRoomInsertProjectCommand
extends SerializedProjectCommand<LegacyProjectRoomStructureCommandPayload> {
schemaVersion: typeof legacyProjectRoomCommandSchemaVersion;
type: typeof projectRoomInsertCommandType;
}
export interface LegacyProjectRoomDeleteProjectCommand
extends SerializedProjectCommand<LegacyProjectRoomStructureCommandPayload> {
schemaVersion: typeof legacyProjectRoomCommandSchemaVersion;
type: typeof projectRoomDeleteCommandType;
}
export interface LegacyProjectRoomUpdateProjectCommand
extends SerializedProjectCommand<LegacyProjectRoomUpdateCommandPayload> {
schemaVersion: typeof legacyProjectRoomCommandSchemaVersion;
type: typeof projectRoomUpdateCommandType;
}
export type ProjectLocationStructureProjectCommand =
| ProjectBuildingInsertProjectCommand
| ProjectBuildingDeleteProjectCommand
| ProjectBuildingUpdateProjectCommand
| ProjectFloorInsertProjectCommand
| ProjectFloorDeleteProjectCommand
| ProjectFloorUpdateProjectCommand
| LegacyProjectFloorInsertProjectCommand
| LegacyProjectFloorDeleteProjectCommand
| LegacyProjectFloorUpdateProjectCommand
| ProjectRoomInsertProjectCommand
| ProjectRoomDeleteProjectCommand
| ProjectRoomUpdateProjectCommand
| LegacyProjectRoomInsertProjectCommand
| LegacyProjectRoomDeleteProjectCommand
| LegacyProjectRoomUpdateProjectCommand;
export function createProjectBuildingSnapshot(
projectId: string,
name: string,
sortOrder: number
): ProjectBuildingSnapshot {
const building: ProjectBuildingSnapshot = {
id: crypto.randomUUID(),
projectId,
name: name.trim(),
sortOrder,
};
assertProjectBuildingSnapshot(building);
return building;
}
export function createProjectFloorSnapshot(
projectId: string,
name: string,
sortOrder: number,
buildingId: string | null = null
): ProjectFloorSnapshot {
const floor: ProjectFloorSnapshot = {
id: crypto.randomUUID(),
projectId,
name: name.trim(),
sortOrder,
buildingId: buildingId?.trim() || null,
};
assertProjectFloorSnapshot(floor);
return floor;
}
export function createProjectRoomSnapshot(
projectId: string,
input: {
floorId?: string;
roomNumber: string;
roomName: string;
areaM2?: number | null;
}
): ProjectRoomSnapshot {
const room: ProjectRoomSnapshot = {
id: crypto.randomUUID(),
projectId,
floorId: input.floorId?.trim() || null,
roomNumber: input.roomNumber.trim(),
roomName: input.roomName.trim(),
areaM2: input.areaM2 ?? null,
};
assertProjectRoomSnapshot(room);
return room;
}
export function createProjectBuildingInsertProjectCommand(
building: ProjectBuildingSnapshot
): ProjectBuildingInsertProjectCommand {
assertProjectBuildingSnapshot(building);
return {
schemaVersion: projectLocationStructureCommandSchemaVersion,
type: projectBuildingInsertCommandType,
payload: { building },
};
}
export function createProjectBuildingDeleteProjectCommand(
building: ProjectBuildingSnapshot
): ProjectBuildingDeleteProjectCommand {
assertProjectBuildingSnapshot(building);
return {
schemaVersion: projectLocationStructureCommandSchemaVersion,
type: projectBuildingDeleteCommandType,
payload: { building },
};
}
export function createProjectBuildingUpdateProjectCommand(
expected: ProjectBuildingSnapshot,
target: ProjectBuildingSnapshot
): ProjectBuildingUpdateProjectCommand {
assertMatchingBuildingUpdate(expected, target);
return {
schemaVersion: projectLocationStructureCommandSchemaVersion,
type: projectBuildingUpdateCommandType,
payload: { expected, target },
};
}
export function createProjectFloorInsertProjectCommand(
floor: ProjectFloorSnapshot
): ProjectFloorInsertProjectCommand {
assertProjectFloorSnapshot(floor);
return {
schemaVersion: projectFloorCommandSchemaVersion,
type: projectFloorInsertCommandType,
payload: { floor },
};
}
export function createProjectFloorDeleteProjectCommand(
floor: ProjectFloorSnapshot
): ProjectFloorDeleteProjectCommand {
assertProjectFloorSnapshot(floor);
return {
schemaVersion: projectFloorCommandSchemaVersion,
type: projectFloorDeleteCommandType,
payload: { floor },
};
}
export function createProjectRoomInsertProjectCommand(
room: ProjectRoomSnapshot
): ProjectRoomInsertProjectCommand {
assertProjectRoomSnapshot(room);
return {
schemaVersion: projectRoomCommandSchemaVersion,
type: projectRoomInsertCommandType,
payload: { room },
};
}
export function createProjectRoomDeleteProjectCommand(
room: ProjectRoomSnapshot
): ProjectRoomDeleteProjectCommand {
assertProjectRoomSnapshot(room);
return {
schemaVersion: projectRoomCommandSchemaVersion,
type: projectRoomDeleteCommandType,
payload: { room },
};
}
export function createProjectFloorUpdateProjectCommand(
expected: ProjectFloorSnapshot,
target: ProjectFloorSnapshot
): ProjectFloorUpdateProjectCommand {
assertMatchingFloorUpdate(expected, target);
return {
schemaVersion: projectFloorCommandSchemaVersion,
type: projectFloorUpdateCommandType,
payload: { expected, target },
};
}
export function createProjectRoomUpdateProjectCommand(
expected: ProjectRoomSnapshot,
target: ProjectRoomSnapshot
): ProjectRoomUpdateProjectCommand {
assertMatchingRoomUpdate(expected, target);
return {
schemaVersion: projectRoomCommandSchemaVersion,
type: projectRoomUpdateCommandType,
payload: { expected, target },
};
}
export function assertProjectBuildingInsertProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is ProjectBuildingInsertProjectCommand {
assertProjectBuildingStructureProjectCommand(
command,
projectBuildingInsertCommandType
);
}
export function assertProjectBuildingDeleteProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is ProjectBuildingDeleteProjectCommand {
assertProjectBuildingStructureProjectCommand(
command,
projectBuildingDeleteCommandType
);
}
export function assertProjectBuildingUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is ProjectBuildingUpdateProjectCommand {
if (
command.schemaVersion !== projectLocationStructureCommandSchemaVersion ||
command.type !== projectBuildingUpdateCommandType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 2
) {
throw new Error("Unsupported project-building update command.");
}
assertMatchingBuildingUpdate(command.payload.expected, command.payload.target);
}
export function assertProjectFloorInsertProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is
| ProjectFloorInsertProjectCommand
| LegacyProjectFloorInsertProjectCommand {
assertProjectFloorStructureProjectCommand(
command,
projectFloorInsertCommandType
);
}
export function assertProjectFloorDeleteProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is
| ProjectFloorDeleteProjectCommand
| LegacyProjectFloorDeleteProjectCommand {
assertProjectFloorStructureProjectCommand(
command,
projectFloorDeleteCommandType
);
}
export function assertProjectRoomInsertProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is
| ProjectRoomInsertProjectCommand
| LegacyProjectRoomInsertProjectCommand {
assertProjectRoomStructureProjectCommand(
command,
projectRoomInsertCommandType
);
}
export function assertProjectRoomDeleteProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is
| ProjectRoomDeleteProjectCommand
| LegacyProjectRoomDeleteProjectCommand {
assertProjectRoomStructureProjectCommand(
command,
projectRoomDeleteCommandType
);
}
export function assertProjectFloorUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is
| ProjectFloorUpdateProjectCommand
| LegacyProjectFloorUpdateProjectCommand {
if (
command.type !== projectFloorUpdateCommandType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 2
) {
throw new Error("Unsupported project-floor update command.");
}
if (command.schemaVersion === legacyProjectFloorCommandSchemaVersion) {
assertMatchingFloorUpdate(
upgradeLegacyFloorSnapshot(command.payload.expected),
upgradeLegacyFloorSnapshot(command.payload.target)
);
return;
}
if (command.schemaVersion !== projectFloorCommandSchemaVersion) {
throw new Error("Unsupported project-floor update command.");
}
assertMatchingFloorUpdate(command.payload.expected, command.payload.target);
}
export function assertProjectRoomUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is
| ProjectRoomUpdateProjectCommand
| LegacyProjectRoomUpdateProjectCommand {
if (
command.type !== projectRoomUpdateCommandType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 2
) {
throw new Error("Unsupported project-room update command.");
}
if (command.schemaVersion === legacyProjectRoomCommandSchemaVersion) {
assertMatchingRoomUpdate(
upgradeLegacyRoomSnapshot(command.payload.expected),
upgradeLegacyRoomSnapshot(command.payload.target)
);
return;
}
if (command.schemaVersion !== projectRoomCommandSchemaVersion) {
throw new Error("Unsupported project-room update command.");
}
assertMatchingRoomUpdate(command.payload.expected, command.payload.target);
}
/**
* Upgrades a (validated) floor command of any supported version to the
* current version. Version 1 commands predate buildings; their floors had no
* building, so `buildingId` becomes null.
*/
export function upgradeProjectFloorCommand(
command:
| ProjectFloorInsertProjectCommand
| LegacyProjectFloorInsertProjectCommand
): ProjectFloorInsertProjectCommand;
export function upgradeProjectFloorCommand(
command:
| ProjectFloorDeleteProjectCommand
| LegacyProjectFloorDeleteProjectCommand
): ProjectFloorDeleteProjectCommand;
export function upgradeProjectFloorCommand(
command:
| ProjectFloorUpdateProjectCommand
| LegacyProjectFloorUpdateProjectCommand
): ProjectFloorUpdateProjectCommand;
export function upgradeProjectFloorCommand(
command:
| ProjectFloorInsertProjectCommand
| ProjectFloorDeleteProjectCommand
| ProjectFloorUpdateProjectCommand
| LegacyProjectFloorInsertProjectCommand
| LegacyProjectFloorDeleteProjectCommand
| LegacyProjectFloorUpdateProjectCommand
) {
if (command.schemaVersion === projectFloorCommandSchemaVersion) {
return command;
}
switch (command.type) {
case projectFloorInsertCommandType:
return createProjectFloorInsertProjectCommand(
upgradeLegacyFloorSnapshot(command.payload.floor)
);
case projectFloorDeleteCommandType:
return createProjectFloorDeleteProjectCommand(
upgradeLegacyFloorSnapshot(command.payload.floor)
);
case projectFloorUpdateCommandType:
return createProjectFloorUpdateProjectCommand(
upgradeLegacyFloorSnapshot(command.payload.expected),
upgradeLegacyFloorSnapshot(command.payload.target)
);
}
}
/** Upgrades persisted version-1 room commands with an empty area. */
export function upgradeProjectRoomCommand(
command:
| ProjectRoomInsertProjectCommand
| LegacyProjectRoomInsertProjectCommand
): ProjectRoomInsertProjectCommand;
export function upgradeProjectRoomCommand(
command:
| ProjectRoomDeleteProjectCommand
| LegacyProjectRoomDeleteProjectCommand
): ProjectRoomDeleteProjectCommand;
export function upgradeProjectRoomCommand(
command:
| ProjectRoomUpdateProjectCommand
| LegacyProjectRoomUpdateProjectCommand
): ProjectRoomUpdateProjectCommand;
export function upgradeProjectRoomCommand(
command:
| ProjectRoomInsertProjectCommand
| ProjectRoomDeleteProjectCommand
| ProjectRoomUpdateProjectCommand
| LegacyProjectRoomInsertProjectCommand
| LegacyProjectRoomDeleteProjectCommand
| LegacyProjectRoomUpdateProjectCommand
) {
if (command.schemaVersion === projectRoomCommandSchemaVersion) {
return command;
}
switch (command.type) {
case projectRoomInsertCommandType:
return createProjectRoomInsertProjectCommand(
upgradeLegacyRoomSnapshot(command.payload.room)
);
case projectRoomDeleteCommandType:
return createProjectRoomDeleteProjectCommand(
upgradeLegacyRoomSnapshot(command.payload.room)
);
case projectRoomUpdateCommandType:
return createProjectRoomUpdateProjectCommand(
upgradeLegacyRoomSnapshot(command.payload.expected),
upgradeLegacyRoomSnapshot(command.payload.target)
);
}
}
function assertMatchingBuildingUpdate(expected: unknown, target: unknown) {
assertProjectBuildingSnapshot(expected);
assertProjectBuildingSnapshot(target);
if (
expected.id !== target.id ||
expected.projectId !== target.projectId ||
expected.sortOrder !== target.sortOrder ||
expected.name === target.name
) {
throw new Error("Project-building update must change only the name.");
}
}
function assertMatchingFloorUpdate(expected: unknown, target: unknown) {
assertProjectFloorSnapshot(expected);
assertProjectFloorSnapshot(target);
if (
expected.id !== target.id ||
expected.projectId !== target.projectId ||
expected.sortOrder !== target.sortOrder ||
(expected.name === target.name && expected.buildingId === target.buildingId)
) {
throw new Error(
"Project-floor update must change only the name or the building."
);
}
}
function assertMatchingRoomUpdate(expected: unknown, target: unknown) {
assertProjectRoomSnapshot(expected);
assertProjectRoomSnapshot(target);
if (
expected.id !== target.id ||
expected.projectId !== target.projectId ||
sameSnapshot(expected, target)
) {
throw new Error("Project-room update must change room values.");
}
}
export function assertProjectBuildingSnapshot(
building: unknown
): asserts building is ProjectBuildingSnapshot {
if (!isPlainObject(building) || Object.keys(building).length !== 4) {
throw new Error("Project-building snapshot is invalid.");
}
assertNormalizedNonEmptyString(building.id, "building.id");
assertNormalizedNonEmptyString(building.projectId, "building.projectId");
assertNormalizedNonEmptyString(building.name, "building.name");
assertSortOrder(building.sortOrder, "building.sortOrder");
}
export function assertProjectFloorSnapshot(
floor: unknown
): asserts floor is ProjectFloorSnapshot {
if (!isPlainObject(floor) || Object.keys(floor).length !== 5) {
throw new Error("Project-floor snapshot is invalid.");
}
assertLegacyFloorFields(floor);
if (floor.buildingId !== null) {
assertNormalizedNonEmptyString(floor.buildingId, "floor.buildingId");
}
}
function assertLegacyProjectFloorSnapshot(
floor: unknown
): asserts floor is LegacyProjectFloorSnapshot {
if (!isPlainObject(floor) || Object.keys(floor).length !== 4) {
throw new Error("Project-floor snapshot is invalid.");
}
assertLegacyFloorFields(floor);
}
function assertLegacyFloorFields(floor: Record<string, unknown>) {
assertNormalizedNonEmptyString(floor.id, "floor.id");
assertNormalizedNonEmptyString(floor.projectId, "floor.projectId");
assertNormalizedNonEmptyString(floor.name, "floor.name");
assertSortOrder(floor.sortOrder, "floor.sortOrder");
}
function upgradeLegacyFloorSnapshot(floor: unknown): ProjectFloorSnapshot {
assertLegacyProjectFloorSnapshot(floor);
return { ...floor, buildingId: null };
}
export function assertProjectRoomSnapshot(
room: unknown
): asserts room is ProjectRoomSnapshot {
if (!isPlainObject(room) || Object.keys(room).length !== 6) {
throw new Error("Project-room snapshot is invalid.");
}
assertNormalizedNonEmptyString(room.id, "room.id");
assertNormalizedNonEmptyString(room.projectId, "room.projectId");
if (room.floorId !== null) {
assertNormalizedNonEmptyString(room.floorId, "room.floorId");
}
assertNormalizedNonEmptyString(room.roomNumber, "room.roomNumber");
assertNormalizedNonEmptyString(room.roomName, "room.roomName");
if (
room.areaM2 !== null &&
(typeof room.areaM2 !== "number" ||
!Number.isFinite(room.areaM2) ||
room.areaM2 < 0)
) {
throw new Error("room.areaM2 must be null or a non-negative finite number.");
}
}
function assertLegacyProjectRoomSnapshot(
room: unknown
): asserts room is LegacyProjectRoomSnapshot {
if (!isPlainObject(room) || Object.keys(room).length !== 5) {
throw new Error("Project-room snapshot is invalid.");
}
assertNormalizedNonEmptyString(room.id, "room.id");
assertNormalizedNonEmptyString(room.projectId, "room.projectId");
if (room.floorId !== null) {
assertNormalizedNonEmptyString(room.floorId, "room.floorId");
}
assertNormalizedNonEmptyString(room.roomNumber, "room.roomNumber");
assertNormalizedNonEmptyString(room.roomName, "room.roomName");
}
function upgradeLegacyRoomSnapshot(room: unknown): ProjectRoomSnapshot {
assertLegacyProjectRoomSnapshot(room);
return { ...room, areaM2: null };
}
function assertProjectBuildingStructureProjectCommand(
command: SerializedProjectCommand<unknown>,
expectedType:
| typeof projectBuildingInsertCommandType
| typeof projectBuildingDeleteCommandType
) {
if (
command.schemaVersion !== projectLocationStructureCommandSchemaVersion ||
command.type !== expectedType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 1
) {
throw new Error("Unsupported project-building structure command.");
}
assertProjectBuildingSnapshot(command.payload.building);
}
function assertProjectFloorStructureProjectCommand(
command: SerializedProjectCommand<unknown>,
expectedType:
| typeof projectFloorInsertCommandType
| typeof projectFloorDeleteCommandType
) {
if (
command.type !== expectedType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 1
) {
throw new Error("Unsupported project-floor structure command.");
}
if (command.schemaVersion === legacyProjectFloorCommandSchemaVersion) {
assertLegacyProjectFloorSnapshot(command.payload.floor);
return;
}
if (command.schemaVersion !== projectFloorCommandSchemaVersion) {
throw new Error("Unsupported project-floor structure command.");
}
assertProjectFloorSnapshot(command.payload.floor);
}
function assertProjectRoomStructureProjectCommand(
command: SerializedProjectCommand<unknown>,
expectedType:
| typeof projectRoomInsertCommandType
| typeof projectRoomDeleteCommandType
) {
if (
command.type !== expectedType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 1
) {
throw new Error("Unsupported project-room structure command.");
}
if (command.schemaVersion === legacyProjectRoomCommandSchemaVersion) {
assertLegacyProjectRoomSnapshot(command.payload.room);
return;
}
if (command.schemaVersion !== projectRoomCommandSchemaVersion) {
throw new Error("Unsupported project-room structure command.");
}
assertProjectRoomSnapshot(command.payload.room);
}
function assertSortOrder(value: unknown, field: string) {
if (
typeof value !== "number" ||
!Number.isSafeInteger(value) ||
value < 0
) {
throw new Error(`${field} must be a non-negative integer.`);
}
}
function assertNormalizedNonEmptyString(
value: unknown,
field: string
): asserts value is string {
if (
typeof value !== "string" ||
!value ||
value !== value.trim()
) {
throw new Error(`${field} must be a normalized non-empty string.`);
}
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function sameSnapshot(left: object, right: object) {
return Object.entries(left).every(
([key, value]) => (right as Record<string, unknown>)[key] === value
);
}