Persist project locations
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
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<ProjectFloorStructureCommandPayload> {
|
||||
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
|
||||
type: typeof projectFloorInsertCommandType;
|
||||
}
|
||||
|
||||
export interface ProjectFloorDeleteProjectCommand
|
||||
extends SerializedProjectCommand<ProjectFloorStructureCommandPayload> {
|
||||
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
|
||||
type: typeof projectFloorDeleteCommandType;
|
||||
}
|
||||
|
||||
export interface ProjectRoomInsertProjectCommand
|
||||
extends SerializedProjectCommand<ProjectRoomStructureCommandPayload> {
|
||||
schemaVersion: typeof projectLocationStructureCommandSchemaVersion;
|
||||
type: typeof projectRoomInsertCommandType;
|
||||
}
|
||||
|
||||
export interface ProjectRoomDeleteProjectCommand
|
||||
extends SerializedProjectCommand<ProjectRoomStructureCommandPayload> {
|
||||
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<unknown>
|
||||
): asserts command is ProjectFloorInsertProjectCommand {
|
||||
assertProjectFloorStructureProjectCommand(
|
||||
command,
|
||||
projectFloorInsertCommandType
|
||||
);
|
||||
}
|
||||
|
||||
export function assertProjectFloorDeleteProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is ProjectFloorDeleteProjectCommand {
|
||||
assertProjectFloorStructureProjectCommand(
|
||||
command,
|
||||
projectFloorDeleteCommandType
|
||||
);
|
||||
}
|
||||
|
||||
export function assertProjectRoomInsertProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is ProjectRoomInsertProjectCommand {
|
||||
assertProjectRoomStructureProjectCommand(
|
||||
command,
|
||||
projectRoomInsertCommandType
|
||||
);
|
||||
}
|
||||
|
||||
export function assertProjectRoomDeleteProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): 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<unknown>,
|
||||
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<unknown>,
|
||||
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<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ProjectLocationStructureProjectCommand } from "../models/project-location-structure-project-command.model.js";
|
||||
import type {
|
||||
AppendedProjectRevision,
|
||||
ProjectRevisionSource,
|
||||
} from "./project-revision.store.js";
|
||||
|
||||
export interface ExecuteProjectLocationStructureCommandInput {
|
||||
projectId: string;
|
||||
expectedRevision: number;
|
||||
source: ProjectRevisionSource;
|
||||
description?: string;
|
||||
actorId?: string;
|
||||
historyTargetChangeSetId?: string;
|
||||
command: ProjectLocationStructureProjectCommand;
|
||||
}
|
||||
|
||||
export interface ExecutedProjectLocationStructureCommand {
|
||||
revision: AppendedProjectRevision;
|
||||
inverse: ProjectLocationStructureProjectCommand;
|
||||
}
|
||||
|
||||
export interface ProjectLocationStructureProjectCommandStore {
|
||||
execute(
|
||||
input: ExecuteProjectLocationStructureCommandInput
|
||||
): ExecutedProjectLocationStructureCommand;
|
||||
}
|
||||
@@ -47,6 +47,16 @@ import {
|
||||
assertSerializedProjectCommand,
|
||||
type SerializedProjectCommand,
|
||||
} from "../models/project-command.model.js";
|
||||
import {
|
||||
assertProjectFloorDeleteProjectCommand,
|
||||
assertProjectFloorInsertProjectCommand,
|
||||
assertProjectRoomDeleteProjectCommand,
|
||||
assertProjectRoomInsertProjectCommand,
|
||||
projectFloorDeleteCommandType,
|
||||
projectFloorInsertCommandType,
|
||||
projectRoomDeleteCommandType,
|
||||
projectRoomInsertCommandType,
|
||||
} from "../models/project-location-structure-project-command.model.js";
|
||||
import {
|
||||
assertProjectStateRestoreCommand,
|
||||
projectStateRestoreCommandType,
|
||||
@@ -87,6 +97,7 @@ import type {
|
||||
ProjectHistoryDirection,
|
||||
ProjectHistoryStore,
|
||||
} from "../ports/project-history.store.js";
|
||||
import type { ProjectLocationStructureProjectCommandStore } from "../ports/project-location-structure-project-command.store.js";
|
||||
import type { ProjectDeviceProjectCommandStore } from "../ports/project-device-project-command.store.js";
|
||||
import type { ProjectDeviceRowSyncProjectCommandStore } from "../ports/project-device-row-sync-project-command.store.js";
|
||||
import type { ProjectDeviceStructureProjectCommandStore } from "../ports/project-device-structure-project-command.store.js";
|
||||
@@ -112,6 +123,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
private readonly deviceRowMoveStore: CircuitDeviceRowMoveProjectCommandStore,
|
||||
private readonly circuitStructureStore: CircuitStructureProjectCommandStore,
|
||||
private readonly distributionBoardStructureStore: DistributionBoardStructureProjectCommandStore,
|
||||
private readonly projectLocationStructureStore: ProjectLocationStructureProjectCommandStore,
|
||||
private readonly circuitSectionReorderStore: CircuitSectionReorderProjectCommandStore,
|
||||
private readonly circuitSectionRenumberStore: CircuitSectionRenumberProjectCommandStore,
|
||||
private readonly projectDeviceStructureStore: ProjectDeviceStructureProjectCommandStore,
|
||||
@@ -273,6 +285,34 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectFloorInsertCommandType: {
|
||||
assertProjectFloorInsertProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectFloorDeleteCommandType: {
|
||||
assertProjectFloorDeleteProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectRoomInsertCommandType: {
|
||||
assertProjectRoomInsertProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectRoomDeleteCommandType: {
|
||||
assertProjectRoomDeleteProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case circuitSectionReorderCommandType: {
|
||||
assertCircuitSectionReorderProjectCommand(input.command);
|
||||
return this.circuitSectionReorderStore.execute({
|
||||
|
||||
Reference in New Issue
Block a user