Add versioned project settings modal
This commit is contained in:
@@ -2,23 +2,42 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
|
||||
export const projectSettingsUpdateCommandType =
|
||||
"project.update-settings" as const;
|
||||
export const projectSettingsUpdateCommandSchemaVersion = 1 as const;
|
||||
export const legacyProjectSettingsUpdateCommandSchemaVersion = 1 as const;
|
||||
export const projectSettingsUpdateCommandSchemaVersion = 2 as const;
|
||||
|
||||
export interface ProjectSettingsValues {
|
||||
export interface LegacyProjectSettingsValues {
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
}
|
||||
|
||||
export interface ProjectSettingsUpdateProjectCommand
|
||||
export interface ProjectSettingsValues extends LegacyProjectSettingsValues {
|
||||
name: string;
|
||||
internalProjectNumber: string | null;
|
||||
externalProjectNumber: string | null;
|
||||
buildingOwner: string | null;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface LegacyProjectSettingsUpdateProjectCommand
|
||||
extends SerializedProjectCommand<LegacyProjectSettingsValues> {
|
||||
schemaVersion: typeof legacyProjectSettingsUpdateCommandSchemaVersion;
|
||||
type: typeof projectSettingsUpdateCommandType;
|
||||
}
|
||||
|
||||
export interface CurrentProjectSettingsUpdateProjectCommand
|
||||
extends SerializedProjectCommand<ProjectSettingsValues> {
|
||||
schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion;
|
||||
type: typeof projectSettingsUpdateCommandType;
|
||||
}
|
||||
|
||||
export type ProjectSettingsUpdateProjectCommand =
|
||||
| LegacyProjectSettingsUpdateProjectCommand
|
||||
| CurrentProjectSettingsUpdateProjectCommand;
|
||||
|
||||
export function createProjectSettingsUpdateProjectCommand(
|
||||
values: ProjectSettingsValues
|
||||
): ProjectSettingsUpdateProjectCommand {
|
||||
const command: ProjectSettingsUpdateProjectCommand = {
|
||||
): CurrentProjectSettingsUpdateProjectCommand {
|
||||
const command: CurrentProjectSettingsUpdateProjectCommand = {
|
||||
schemaVersion: projectSettingsUpdateCommandSchemaVersion,
|
||||
type: projectSettingsUpdateCommandType,
|
||||
payload: { ...values },
|
||||
@@ -31,16 +50,45 @@ export function assertProjectSettingsUpdateProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is ProjectSettingsUpdateProjectCommand {
|
||||
if (
|
||||
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion ||
|
||||
command.type !== projectSettingsUpdateCommandType
|
||||
command.type !== projectSettingsUpdateCommandType ||
|
||||
(command.schemaVersion !==
|
||||
legacyProjectSettingsUpdateCommandSchemaVersion &&
|
||||
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion)
|
||||
) {
|
||||
throw new Error("Unsupported project settings update command.");
|
||||
}
|
||||
if (!isPlainObject(command.payload)) {
|
||||
throw new Error(
|
||||
"Project settings update command contains invalid values."
|
||||
);
|
||||
}
|
||||
if (command.schemaVersion === legacyProjectSettingsUpdateCommandSchemaVersion) {
|
||||
assertLegacyValues(command.payload);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!isPlainObject(command.payload) ||
|
||||
!isTrimmedString(command.payload.name, 1, 200) ||
|
||||
!isNullableTrimmedString(command.payload.internalProjectNumber, 100) ||
|
||||
!isNullableTrimmedString(command.payload.externalProjectNumber, 100) ||
|
||||
!isNullableTrimmedString(command.payload.buildingOwner, 200) ||
|
||||
!isNullableTrimmedString(command.payload.description, 2000) ||
|
||||
!isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) ||
|
||||
!isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ||
|
||||
Object.keys(command.payload).length !== 2
|
||||
Object.keys(command.payload).length !== 7
|
||||
) {
|
||||
throw new Error(
|
||||
"Project settings update command contains invalid values."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertLegacyValues(
|
||||
payload: Record<string, unknown>
|
||||
): asserts payload is Record<string, unknown> & LegacyProjectSettingsValues {
|
||||
if (
|
||||
!isPositiveFiniteNumber(payload.singlePhaseVoltageV) ||
|
||||
!isPositiveFiniteNumber(payload.threePhaseVoltageV) ||
|
||||
Object.keys(payload).length !== 2
|
||||
) {
|
||||
throw new Error(
|
||||
"Project settings update command contains invalid voltages."
|
||||
@@ -59,3 +107,26 @@ function isPositiveFiniteNumber(value: unknown): value is number {
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isTrimmedString(
|
||||
value: unknown,
|
||||
minimumLength: number,
|
||||
maximumLength: number
|
||||
): value is string {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
value === value.trim() &&
|
||||
value.length >= minimumLength &&
|
||||
value.length <= maximumLength
|
||||
);
|
||||
}
|
||||
|
||||
function isNullableTrimmedString(
|
||||
value: unknown,
|
||||
maximumLength: number
|
||||
): value is string | null {
|
||||
return (
|
||||
value === null ||
|
||||
(isTrimmedString(value, 1, maximumLength))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const projectStateSnapshotSchemaVersion = 1 as const;
|
||||
export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
|
||||
export const projectStateSnapshotSchemaVersion = 2 as const;
|
||||
|
||||
const idSchema = z.string().trim().min(1);
|
||||
const nullableStringSchema = z.string().nullable();
|
||||
const finiteNumberSchema = z.number().finite();
|
||||
|
||||
const projectSchema = z
|
||||
const legacyProjectSchema = z
|
||||
.object({
|
||||
id: idSchema,
|
||||
name: z.string().trim().min(1),
|
||||
@@ -15,6 +16,13 @@ const projectSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
const projectSchema = legacyProjectSchema.extend({
|
||||
internalProjectNumber: z.string().trim().min(1).max(100).nullable(),
|
||||
externalProjectNumber: z.string().trim().min(1).max(100).nullable(),
|
||||
buildingOwner: z.string().trim().min(1).max(200).nullable(),
|
||||
description: z.string().trim().min(1).max(2000).nullable(),
|
||||
});
|
||||
|
||||
const distributionBoardSchema = z
|
||||
.object({
|
||||
id: idSchema,
|
||||
@@ -132,17 +140,29 @@ const roomSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
const projectStateSnapshotContents = {
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
circuitLists: z.array(circuitListSchema),
|
||||
circuitSections: z.array(circuitSectionSchema),
|
||||
circuits: z.array(circuitSchema),
|
||||
projectDevices: z.array(projectDeviceSchema),
|
||||
floors: z.array(floorSchema),
|
||||
rooms: z.array(roomSchema),
|
||||
};
|
||||
|
||||
const legacyProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion),
|
||||
project: legacyProjectSchema,
|
||||
...projectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const projectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
|
||||
project: projectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
circuitLists: z.array(circuitListSchema),
|
||||
circuitSections: z.array(circuitSectionSchema),
|
||||
circuits: z.array(circuitSchema),
|
||||
projectDevices: z.array(projectDeviceSchema),
|
||||
floors: z.array(floorSchema),
|
||||
rooms: z.array(roomSchema),
|
||||
...projectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -153,11 +173,37 @@ export type ProjectStateSnapshot = z.infer<
|
||||
export function parseProjectStateSnapshot(
|
||||
value: unknown
|
||||
): ProjectStateSnapshot {
|
||||
const snapshot = projectStateSnapshotSchema.parse(value);
|
||||
const version = isPlainObject(value) ? value.schemaVersion : undefined;
|
||||
const snapshot =
|
||||
version === legacyProjectStateSnapshotSchemaVersion
|
||||
? upgradeLegacyProjectStateSnapshot(
|
||||
legacyProjectStateSnapshotSchema.parse(value)
|
||||
)
|
||||
: projectStateSnapshotSchema.parse(value);
|
||||
assertProjectStateSnapshotRelations(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function upgradeLegacyProjectStateSnapshot(
|
||||
snapshot: z.infer<typeof legacyProjectStateSnapshotSchema>
|
||||
): ProjectStateSnapshot {
|
||||
return {
|
||||
...snapshot,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
project: {
|
||||
...snapshot.project,
|
||||
internalProjectNumber: null,
|
||||
externalProjectNumber: null,
|
||||
buildingOwner: null,
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function serializeProjectStateSnapshot(
|
||||
snapshot: ProjectStateSnapshot
|
||||
): string {
|
||||
|
||||
Reference in New Issue
Block a user