forked from jappel/leistungsbilanz-ts
803 lines
26 KiB
TypeScript
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
|
|
);
|
|
}
|