forked from jappel/leistungsbilanz-ts
Integrate rough estimate balance
This commit is contained in:
commit
573487666c
45 changed files with 9378 additions and 58 deletions
|
|
@ -3,32 +3,80 @@ import {
|
|||
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 = 1 as const;
|
||||
export const projectStateRestoreCommandSchemaVersion = 2 as const;
|
||||
const snapshotRestoreCommandSchemaVersion = 1 as const;
|
||||
|
||||
export interface ProjectStateRestoreCommandPayload {
|
||||
expectedStateSha256: string;
|
||||
targetState: ProjectStateSnapshot;
|
||||
}
|
||||
|
||||
export type ProjectStateRestoreCommand = SerializedProjectCommand<
|
||||
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: 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);
|
||||
|
|
@ -42,10 +90,7 @@ export function assertProjectStateRestoreCommand(
|
|||
throw new Error("Project state restore command must be an object.");
|
||||
}
|
||||
const command = value as Partial<ProjectStateRestoreCommand>;
|
||||
if (
|
||||
command.type !== projectStateRestoreCommandType ||
|
||||
command.schemaVersion !== projectStateRestoreCommandSchemaVersion
|
||||
) {
|
||||
if (command.type !== projectStateRestoreCommandType) {
|
||||
throw new Error("Unsupported project state restore command.");
|
||||
}
|
||||
if (!command.payload || typeof command.payload !== "object") {
|
||||
|
|
@ -58,5 +103,43 @@ export function assertProjectStateRestoreCommand(
|
|||
) {
|
||||
throw new Error("Expected project state checksum is invalid.");
|
||||
}
|
||||
parseProjectStateSnapshot(payload.targetState);
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import {
|
|||
projectStateSnapshotSchemaVersion,
|
||||
type ProjectStateSnapshot,
|
||||
} from "./project-state-snapshot.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 projectTransferFormat = "leistungsbilanz.project" as const;
|
||||
export const projectTransferFormatVersion = 1 as const;
|
||||
export const projectTransferFormatVersion = 2 as const;
|
||||
const legacyProjectTransferFormatVersion = 1 as const;
|
||||
|
||||
export interface ProjectTransferEnvelope {
|
||||
format: typeof projectTransferFormat;
|
||||
|
|
@ -13,12 +16,14 @@ export interface ProjectTransferEnvelope {
|
|||
exportedAtIso: string;
|
||||
payloadSha256: string;
|
||||
projectState: ProjectStateSnapshot;
|
||||
estimateBalances: EstimateBalanceSnapshot[];
|
||||
}
|
||||
|
||||
export function createProjectTransferEnvelope(
|
||||
exportedAtIso: string,
|
||||
payloadSha256: string,
|
||||
projectState: ProjectStateSnapshot
|
||||
projectState: ProjectStateSnapshot,
|
||||
estimateBalances: EstimateBalanceSnapshot[]
|
||||
): ProjectTransferEnvelope {
|
||||
return parseProjectTransferEnvelope({
|
||||
format: projectTransferFormat,
|
||||
|
|
@ -26,6 +31,7 @@ export function createProjectTransferEnvelope(
|
|||
exportedAtIso,
|
||||
payloadSha256,
|
||||
projectState,
|
||||
estimateBalances,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +43,8 @@ export function parseProjectTransferEnvelope(
|
|||
}
|
||||
if (
|
||||
value.format !== projectTransferFormat ||
|
||||
value.formatVersion !== projectTransferFormatVersion
|
||||
(value.formatVersion !== legacyProjectTransferFormatVersion &&
|
||||
value.formatVersion !== projectTransferFormatVersion)
|
||||
) {
|
||||
throw new Error("Project transfer format is not supported.");
|
||||
}
|
||||
|
|
@ -57,7 +64,13 @@ export function parseProjectTransferEnvelope(
|
|||
if (projectState.schemaVersion !== projectStateSnapshotSchemaVersion) {
|
||||
throw new Error("Project transfer snapshot version is not supported.");
|
||||
}
|
||||
if (Object.keys(value).length !== 5) {
|
||||
const estimateBalances =
|
||||
value.formatVersion === legacyProjectTransferFormatVersion
|
||||
? []
|
||||
: parseTransferEstimateBalances(value.estimateBalances, projectState.project.id);
|
||||
const expectedFieldCount =
|
||||
value.formatVersion === legacyProjectTransferFormatVersion ? 5 : 6;
|
||||
if (Object.keys(value).length !== expectedFieldCount) {
|
||||
throw new Error("Project transfer file contains unknown fields.");
|
||||
}
|
||||
return {
|
||||
|
|
@ -66,9 +79,43 @@ export function parseProjectTransferEnvelope(
|
|||
exportedAtIso: value.exportedAtIso,
|
||||
payloadSha256: value.payloadSha256,
|
||||
projectState,
|
||||
estimateBalances,
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeProjectTransferPayload(
|
||||
projectState: ProjectStateSnapshot,
|
||||
estimateBalances: EstimateBalanceSnapshot[]
|
||||
) {
|
||||
const parsedState = parseProjectStateSnapshot(projectState);
|
||||
return JSON.stringify({
|
||||
projectState: parsedState,
|
||||
estimateBalances: parseTransferEstimateBalances(
|
||||
estimateBalances,
|
||||
parsedState.project.id
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function remapEstimateBalances(
|
||||
source: EstimateBalanceSnapshot[],
|
||||
targetProjectId: string,
|
||||
createId: () => string
|
||||
): EstimateBalanceSnapshot[] {
|
||||
return source.map((balance) =>
|
||||
parseEstimateBalanceSnapshot({
|
||||
...balance,
|
||||
id: createId(),
|
||||
projectId: targetProjectId,
|
||||
groups: balance.groups.map((group) => ({
|
||||
...group,
|
||||
id: createId(),
|
||||
rows: group.rows.map((row) => ({ ...row, id: createId() })),
|
||||
})),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function remapProjectState(
|
||||
source: ProjectStateSnapshot,
|
||||
targetProjectId: string,
|
||||
|
|
@ -282,6 +329,37 @@ function requiredId(ids: ReadonlyMap<string, string>, sourceId: string) {
|
|||
return targetId;
|
||||
}
|
||||
|
||||
function parseTransferEstimateBalances(
|
||||
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.");
|
||||
}
|
||||
assertUniqueIds(balances.map((balance) => balance.id), "estimate balance");
|
||||
assertUniqueIds(
|
||||
balances.flatMap((balance) => balance.groups.map((group) => group.id)),
|
||||
"estimate group"
|
||||
);
|
||||
assertUniqueIds(
|
||||
balances.flatMap((balance) =>
|
||||
balance.groups.flatMap((group) => group.rows.map((row) => row.id))
|
||||
),
|
||||
"estimate row"
|
||||
);
|
||||
return balances;
|
||||
}
|
||||
|
||||
function assertUniqueIds(ids: string[], label: string) {
|
||||
if (new Set(ids).size !== ids.length) {
|
||||
throw new Error(`Project transfer contains a duplicate ${label} id.`);
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue