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; 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; 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; 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; }