import crypto from "node:crypto"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const projectFloorInsertCommandType = "project-floor.insert" as const; export const projectFloorDeleteCommandType = "project-floor.delete" as const; export const projectRoomInsertCommandType = "project-room.insert" as const; export const projectRoomDeleteCommandType = "project-room.delete" as const; export const projectLocationStructureCommandSchemaVersion = 1 as const; export interface ProjectFloorSnapshot { id: string; projectId: string; name: string; sortOrder: number; } export interface ProjectRoomSnapshot { id: string; projectId: string; floorId: string | null; roomNumber: string; roomName: string; } interface ProjectFloorStructureCommandPayload { floor: ProjectFloorSnapshot; } interface ProjectRoomStructureCommandPayload { room: ProjectRoomSnapshot; } export interface ProjectFloorInsertProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectLocationStructureCommandSchemaVersion; type: typeof projectFloorInsertCommandType; } export interface ProjectFloorDeleteProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectLocationStructureCommandSchemaVersion; type: typeof projectFloorDeleteCommandType; } export interface ProjectRoomInsertProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectLocationStructureCommandSchemaVersion; type: typeof projectRoomInsertCommandType; } export interface ProjectRoomDeleteProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectLocationStructureCommandSchemaVersion; type: typeof projectRoomDeleteCommandType; } export type ProjectLocationStructureProjectCommand = | ProjectFloorInsertProjectCommand | ProjectFloorDeleteProjectCommand | ProjectRoomInsertProjectCommand | ProjectRoomDeleteProjectCommand; export function createProjectFloorSnapshot( projectId: string, name: string, sortOrder: number ): ProjectFloorSnapshot { const floor: ProjectFloorSnapshot = { id: crypto.randomUUID(), projectId, name: name.trim(), sortOrder, }; assertProjectFloorSnapshot(floor); return floor; } export function createProjectRoomSnapshot( projectId: string, input: { floorId?: string; roomNumber: string; roomName: string; } ): ProjectRoomSnapshot { const room: ProjectRoomSnapshot = { id: crypto.randomUUID(), projectId, floorId: input.floorId?.trim() || null, roomNumber: input.roomNumber.trim(), roomName: input.roomName.trim(), }; assertProjectRoomSnapshot(room); return room; } export function createProjectFloorInsertProjectCommand( floor: ProjectFloorSnapshot ): ProjectFloorInsertProjectCommand { assertProjectFloorSnapshot(floor); return { schemaVersion: projectLocationStructureCommandSchemaVersion, type: projectFloorInsertCommandType, payload: { floor }, }; } export function createProjectFloorDeleteProjectCommand( floor: ProjectFloorSnapshot ): ProjectFloorDeleteProjectCommand { assertProjectFloorSnapshot(floor); return { schemaVersion: projectLocationStructureCommandSchemaVersion, type: projectFloorDeleteCommandType, payload: { floor }, }; } export function createProjectRoomInsertProjectCommand( room: ProjectRoomSnapshot ): ProjectRoomInsertProjectCommand { assertProjectRoomSnapshot(room); return { schemaVersion: projectLocationStructureCommandSchemaVersion, type: projectRoomInsertCommandType, payload: { room }, }; } export function createProjectRoomDeleteProjectCommand( room: ProjectRoomSnapshot ): ProjectRoomDeleteProjectCommand { assertProjectRoomSnapshot(room); return { schemaVersion: projectLocationStructureCommandSchemaVersion, type: projectRoomDeleteCommandType, payload: { room }, }; } export function assertProjectFloorInsertProjectCommand( command: SerializedProjectCommand ): asserts command is ProjectFloorInsertProjectCommand { assertProjectFloorStructureProjectCommand( command, projectFloorInsertCommandType ); } export function assertProjectFloorDeleteProjectCommand( command: SerializedProjectCommand ): asserts command is ProjectFloorDeleteProjectCommand { assertProjectFloorStructureProjectCommand( command, projectFloorDeleteCommandType ); } export function assertProjectRoomInsertProjectCommand( command: SerializedProjectCommand ): asserts command is ProjectRoomInsertProjectCommand { assertProjectRoomStructureProjectCommand( command, projectRoomInsertCommandType ); } export function assertProjectRoomDeleteProjectCommand( command: SerializedProjectCommand ): asserts command is ProjectRoomDeleteProjectCommand { assertProjectRoomStructureProjectCommand( command, projectRoomDeleteCommandType ); } export function assertProjectFloorSnapshot( floor: unknown ): asserts floor is ProjectFloorSnapshot { if (!isPlainObject(floor) || Object.keys(floor).length !== 4) { throw new Error("Project-floor snapshot is invalid."); } assertNormalizedNonEmptyString(floor.id, "floor.id"); assertNormalizedNonEmptyString(floor.projectId, "floor.projectId"); assertNormalizedNonEmptyString(floor.name, "floor.name"); if ( typeof floor.sortOrder !== "number" || !Number.isSafeInteger(floor.sortOrder) || floor.sortOrder < 0 ) { throw new Error("floor.sortOrder must be a non-negative integer."); } } export function assertProjectRoomSnapshot( room: unknown ): asserts room is ProjectRoomSnapshot { 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 assertProjectFloorStructureProjectCommand( command: SerializedProjectCommand, expectedType: | typeof projectFloorInsertCommandType | typeof projectFloorDeleteCommandType ) { if ( command.schemaVersion !== projectLocationStructureCommandSchemaVersion || command.type !== expectedType || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 1 ) { throw new Error("Unsupported project-floor structure command."); } assertProjectFloorSnapshot(command.payload.floor); } function assertProjectRoomStructureProjectCommand( command: SerializedProjectCommand, expectedType: | typeof projectRoomInsertCommandType | typeof projectRoomDeleteCommandType ) { if ( command.schemaVersion !== projectLocationStructureCommandSchemaVersion || command.type !== expectedType || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 1 ) { throw new Error("Unsupported project-room structure command."); } assertProjectRoomSnapshot(command.payload.room); } 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 { return value !== null && typeof value === "object" && !Array.isArray(value); }