forked from jappel/leistungsbilanz-ts
Add buildings above floors and a building structure page
Projects with several buildings could not be structured: floors and rooms hung directly off the project. Floors can now belong to a building (migration 0009: table buildings, nullable floors.building_id). Rooms and distribution boards keep their floor; their building follows from it. Existing floors stay without a building. Buildings use the location-structure command store with project-building.insert/.update/.delete, inverses and persistent Undo/Redo; deleting a building with assigned floors is rejected. Floor commands carry buildingId since command version 2. Version 1 floor commands from existing history predate buildings and are upgraded with buildingId = null before they are executed and persisted, so forward and inverse keep one version. Project snapshot schema 7 stores buildings and floor.buildingId; versions 1-6 are upgraded without buildings. Restore and portable transfer include buildings, and the transfer remaps building ids. Buildings, floors and rooms move from the project overview to their own page /projects/:id/struktur with three columns (Gebäude -> Etagen -> Räume) that filter each other, and a sidebar entry. Floor selects in the board and room dialogs are grouped by building. Unchanged dialogs close without a command. Details in docs/building-structure.md; see issue #6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7692699ee2
commit
cd89c4a499
42 changed files with 5883 additions and 373 deletions
|
|
@ -1,21 +1,42 @@
|
|||
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;
|
||||
/** Room commands, building commands and legacy floor 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;
|
||||
|
||||
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;
|
||||
|
|
@ -24,39 +45,93 @@ export interface ProjectRoomSnapshot {
|
|||
roomName: string;
|
||||
}
|
||||
|
||||
interface ProjectBuildingStructureCommandPayload {
|
||||
building: ProjectBuildingSnapshot;
|
||||
}
|
||||
|
||||
interface ProjectFloorStructureCommandPayload {
|
||||
floor: ProjectFloorSnapshot;
|
||||
}
|
||||
|
||||
interface LegacyProjectFloorStructureCommandPayload {
|
||||
floor: LegacyProjectFloorSnapshot;
|
||||
}
|
||||
|
||||
interface ProjectRoomStructureCommandPayload {
|
||||
room: ProjectRoomSnapshot;
|
||||
}
|
||||
|
||||
interface ProjectBuildingUpdateCommandPayload {
|
||||
expected: ProjectBuildingSnapshot;
|
||||
target: ProjectBuildingSnapshot;
|
||||
}
|
||||
|
||||
interface ProjectFloorUpdateCommandPayload {
|
||||
expected: ProjectFloorSnapshot;
|
||||
target: ProjectFloorSnapshot;
|
||||
}
|
||||
|
||||
interface LegacyProjectFloorUpdateCommandPayload {
|
||||
expected: LegacyProjectFloorSnapshot;
|
||||
target: LegacyProjectFloorSnapshot;
|
||||
}
|
||||
|
||||
interface ProjectRoomUpdateCommandPayload {
|
||||
expected: ProjectRoomSnapshot;
|
||||
target: ProjectRoomSnapshot;
|
||||
}
|
||||
|
||||
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 projectLocationStructureCommandSchemaVersion;
|
||||
schemaVersion: typeof projectFloorCommandSchemaVersion;
|
||||
type: typeof projectFloorInsertCommandType;
|
||||
}
|
||||
|
||||
export interface ProjectFloorDeleteProjectCommand
|
||||
extends SerializedProjectCommand<ProjectFloorStructureCommandPayload> {
|
||||
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
|
||||
schemaVersion: typeof projectFloorCommandSchemaVersion;
|
||||
type: typeof projectFloorDeleteCommandType;
|
||||
}
|
||||
|
||||
export interface ProjectFloorUpdateProjectCommand
|
||||
extends SerializedProjectCommand<ProjectFloorUpdateCommandPayload> {
|
||||
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -79,23 +154,46 @@ export interface ProjectRoomUpdateProjectCommand
|
|||
}
|
||||
|
||||
export type ProjectLocationStructureProjectCommand =
|
||||
| ProjectBuildingInsertProjectCommand
|
||||
| ProjectBuildingDeleteProjectCommand
|
||||
| ProjectBuildingUpdateProjectCommand
|
||||
| ProjectFloorInsertProjectCommand
|
||||
| ProjectFloorDeleteProjectCommand
|
||||
| ProjectFloorUpdateProjectCommand
|
||||
| LegacyProjectFloorInsertProjectCommand
|
||||
| LegacyProjectFloorDeleteProjectCommand
|
||||
| LegacyProjectFloorUpdateProjectCommand
|
||||
| ProjectRoomInsertProjectCommand
|
||||
| ProjectRoomDeleteProjectCommand
|
||||
| ProjectRoomUpdateProjectCommand;
|
||||
|
||||
export function createProjectFloorSnapshot(
|
||||
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;
|
||||
|
|
@ -120,12 +218,46 @@ export function createProjectRoomSnapshot(
|
|||
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: projectLocationStructureCommandSchemaVersion,
|
||||
schemaVersion: projectFloorCommandSchemaVersion,
|
||||
type: projectFloorInsertCommandType,
|
||||
payload: { floor },
|
||||
};
|
||||
|
|
@ -136,7 +268,7 @@ export function createProjectFloorDeleteProjectCommand(
|
|||
): ProjectFloorDeleteProjectCommand {
|
||||
assertProjectFloorSnapshot(floor);
|
||||
return {
|
||||
schemaVersion: projectLocationStructureCommandSchemaVersion,
|
||||
schemaVersion: projectFloorCommandSchemaVersion,
|
||||
type: projectFloorDeleteCommandType,
|
||||
payload: { floor },
|
||||
};
|
||||
|
|
@ -170,7 +302,7 @@ export function createProjectFloorUpdateProjectCommand(
|
|||
): ProjectFloorUpdateProjectCommand {
|
||||
assertMatchingFloorUpdate(expected, target);
|
||||
return {
|
||||
schemaVersion: projectLocationStructureCommandSchemaVersion,
|
||||
schemaVersion: projectFloorCommandSchemaVersion,
|
||||
type: projectFloorUpdateCommandType,
|
||||
payload: { expected, target },
|
||||
};
|
||||
|
|
@ -188,9 +320,43 @@ export function createProjectRoomUpdateProjectCommand(
|
|||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
): asserts command is
|
||||
| ProjectFloorInsertProjectCommand
|
||||
| LegacyProjectFloorInsertProjectCommand {
|
||||
assertProjectFloorStructureProjectCommand(
|
||||
command,
|
||||
projectFloorInsertCommandType
|
||||
|
|
@ -199,7 +365,9 @@ export function assertProjectFloorInsertProjectCommand(
|
|||
|
||||
export function assertProjectFloorDeleteProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is ProjectFloorDeleteProjectCommand {
|
||||
): asserts command is
|
||||
| ProjectFloorDeleteProjectCommand
|
||||
| LegacyProjectFloorDeleteProjectCommand {
|
||||
assertProjectFloorStructureProjectCommand(
|
||||
command,
|
||||
projectFloorDeleteCommandType
|
||||
|
|
@ -226,15 +394,26 @@ export function assertProjectRoomDeleteProjectCommand(
|
|||
|
||||
export function assertProjectFloorUpdateProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is ProjectFloorUpdateProjectCommand {
|
||||
): asserts command is
|
||||
| ProjectFloorUpdateProjectCommand
|
||||
| LegacyProjectFloorUpdateProjectCommand {
|
||||
if (
|
||||
command.schemaVersion !== projectLocationStructureCommandSchemaVersion ||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -252,6 +431,68 @@ export function assertProjectRoomUpdateProjectCommand(
|
|||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -259,9 +500,11 @@ function assertMatchingFloorUpdate(expected: unknown, target: unknown) {
|
|||
expected.id !== target.id ||
|
||||
expected.projectId !== target.projectId ||
|
||||
expected.sortOrder !== target.sortOrder ||
|
||||
expected.name === target.name
|
||||
(expected.name === target.name && expected.buildingId === target.buildingId)
|
||||
) {
|
||||
throw new Error("Project-floor update must change only the name.");
|
||||
throw new Error(
|
||||
"Project-floor update must change only the name or the building."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -277,22 +520,49 @@ function assertMatchingRoomUpdate(expected: unknown, target: unknown) {
|
|||
}
|
||||
}
|
||||
|
||||
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");
|
||||
if (
|
||||
typeof floor.sortOrder !== "number" ||
|
||||
!Number.isSafeInteger(floor.sortOrder) ||
|
||||
floor.sortOrder < 0
|
||||
) {
|
||||
throw new Error("floor.sortOrder must be a non-negative integer.");
|
||||
}
|
||||
assertSortOrder(floor.sortOrder, "floor.sortOrder");
|
||||
}
|
||||
|
||||
function upgradeLegacyFloorSnapshot(floor: unknown): ProjectFloorSnapshot {
|
||||
assertLegacyProjectFloorSnapshot(floor);
|
||||
return { ...floor, buildingId: null };
|
||||
}
|
||||
|
||||
export function assertProjectRoomSnapshot(
|
||||
|
|
@ -310,11 +580,11 @@ export function assertProjectRoomSnapshot(
|
|||
assertNormalizedNonEmptyString(room.roomName, "room.roomName");
|
||||
}
|
||||
|
||||
function assertProjectFloorStructureProjectCommand(
|
||||
function assertProjectBuildingStructureProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>,
|
||||
expectedType:
|
||||
| typeof projectFloorInsertCommandType
|
||||
| typeof projectFloorDeleteCommandType
|
||||
| typeof projectBuildingInsertCommandType
|
||||
| typeof projectBuildingDeleteCommandType
|
||||
) {
|
||||
if (
|
||||
command.schemaVersion !== projectLocationStructureCommandSchemaVersion ||
|
||||
|
|
@ -322,6 +592,29 @@ function assertProjectFloorStructureProjectCommand(
|
|||
!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);
|
||||
|
|
@ -344,6 +637,16 @@ function assertProjectRoomStructureProjectCommand(
|
|||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue