leistungsbilanz-ts/src/domain/models/project-state-restore-command.model.ts

145 lines
5 KiB
TypeScript

import {
parseProjectStateSnapshot,
type ProjectStateSnapshot,
} from "./project-state-snapshot.model.js";
import type { SerializedProjectCommand } from "./project-command.model.js";
import {
parseEstimateBalanceSnapshot,
} from "../../estimate-balance/domain/estimate-balance-command.model.js";
import type { EstimateBalanceSnapshot } from "../../shared/estimate-balance/estimate-balance.js";
export const projectStateRestoreCommandType = "project.restore-state";
export const projectStateRestoreCommandSchemaVersion = 2 as const;
const snapshotRestoreCommandSchemaVersion = 1 as const;
export interface ProjectStateRestoreCommandPayload {
expectedStateSha256: string;
targetState: ProjectStateSnapshot;
}
export type SnapshotProjectStateRestoreCommand = SerializedProjectCommand<
ProjectStateRestoreCommandPayload
> & {
schemaVersion: typeof snapshotRestoreCommandSchemaVersion;
type: typeof projectStateRestoreCommandType;
};
export interface ProjectTransferRestoreCommandPayload
extends ProjectStateRestoreCommandPayload {
expectedEstimateBalances: EstimateBalanceSnapshot[];
targetEstimateBalances: EstimateBalanceSnapshot[];
}
export type ProjectTransferRestoreCommand = SerializedProjectCommand<
ProjectTransferRestoreCommandPayload
> & {
schemaVersion: typeof projectStateRestoreCommandSchemaVersion;
type: typeof projectStateRestoreCommandType;
};
export type ProjectStateRestoreCommand =
| SnapshotProjectStateRestoreCommand
| ProjectTransferRestoreCommand;
export function createProjectStateRestoreCommand(
expectedStateSha256: string,
targetState: ProjectStateSnapshot
): ProjectStateRestoreCommand {
const command: SnapshotProjectStateRestoreCommand = {
schemaVersion: snapshotRestoreCommandSchemaVersion,
type: projectStateRestoreCommandType,
payload: {
expectedStateSha256,
targetState: parseProjectStateSnapshot(targetState),
},
};
assertProjectStateRestoreCommand(command);
return command;
}
export function createProjectTransferRestoreCommand(
expectedStateSha256: string,
targetState: ProjectStateSnapshot,
expectedEstimateBalances: EstimateBalanceSnapshot[],
targetEstimateBalances: EstimateBalanceSnapshot[]
): ProjectTransferRestoreCommand {
const command: ProjectTransferRestoreCommand = {
schemaVersion: projectStateRestoreCommandSchemaVersion,
type: projectStateRestoreCommandType,
payload: {
expectedStateSha256,
targetState: parseProjectStateSnapshot(targetState),
expectedEstimateBalances: parseEstimateBalances(
expectedEstimateBalances,
targetState.project.id
),
targetEstimateBalances: parseEstimateBalances(
targetEstimateBalances,
targetState.project.id
),
},
};
assertProjectStateRestoreCommand(command);
return command;
}
export function assertProjectStateRestoreCommand(
value: unknown
): asserts value is ProjectStateRestoreCommand {
if (!value || typeof value !== "object") {
throw new Error("Project state restore command must be an object.");
}
const command = value as Partial<ProjectStateRestoreCommand>;
if (command.type !== projectStateRestoreCommandType) {
throw new Error("Unsupported project state restore command.");
}
if (!command.payload || typeof command.payload !== "object") {
throw new Error("Project state restore payload is required.");
}
const payload = command.payload as Partial<ProjectStateRestoreCommandPayload>;
if (
typeof payload.expectedStateSha256 !== "string" ||
!/^[a-f0-9]{64}$/.test(payload.expectedStateSha256)
) {
throw new Error("Expected project state checksum is invalid.");
}
const targetState = parseProjectStateSnapshot(payload.targetState);
if (command.schemaVersion === snapshotRestoreCommandSchemaVersion) {
if (Object.keys(payload).length !== 2) {
throw new Error("Snapshot restore payload contains unknown fields.");
}
return;
}
if (command.schemaVersion !== projectStateRestoreCommandSchemaVersion) {
throw new Error("Unsupported project state restore command.");
}
const transferPayload = payload as Partial<ProjectTransferRestoreCommandPayload>;
parseEstimateBalances(
transferPayload.expectedEstimateBalances,
targetState.project.id
);
parseEstimateBalances(
transferPayload.targetEstimateBalances,
targetState.project.id
);
if (Object.keys(payload).length !== 4) {
throw new Error("Project transfer restore payload contains unknown fields.");
}
}
function parseEstimateBalances(
value: unknown,
projectId: string
): EstimateBalanceSnapshot[] {
if (!Array.isArray(value)) {
throw new Error("Project transfer estimate balances must be an array.");
}
const balances = value.map(parseEstimateBalanceSnapshot);
if (balances.some((balance) => balance.projectId !== projectId)) {
throw new Error("Project transfer estimate balance belongs to another project.");
}
if (new Set(balances.map((balance) => balance.id)).size !== balances.length) {
throw new Error("Project transfer contains duplicate estimate balances.");
}
return balances;
}