Files
backend/src/modules/investments/investment-resource.mapper.ts
T
ksv741 bc631e72de
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 13s
Backend verify / verify (push) Failing after 6m21s
test: align resource availability metadata
2026-10-09 23:10:50 +03:00

125 lines
4.4 KiB
TypeScript

import { InvestmentResourceMetaDto } from './investment-resource.dto';
type LegacyAvailability = 'available' | 'complete' | 'connection-unavailable' | 'initial-loading' | 'missing' | 'partial' | 'temporarily-unavailable';
type Work = null | { errorCode: null | string; id: string; state: string };
type Attempt = NonNullable<InvestmentResourceMetaDto['lastAttempt']>;
export function investmentResourceMeta(input: {
availability: LegacyAvailability;
coverage?: NonNullable<InvestmentResourceMetaDto['coverage']>;
dataRevision: string;
dependencies?: InvestmentResourceMetaDto[];
fetchedAt: Date | null;
freshForMs: number;
lastAttempt?: Attempt;
resourceKey: string;
unavailableReason?: string;
work: Work;
}): InvestmentResourceMetaDto {
const ownSync = sourceSync(input.work);
const syncStates = [
ownSync,
...(input.dependencies ?? [])
.filter((dependency) => !dependency.unavailableReason)
.map((dependency) => dependency.sync)
.filter((state): state is typeof ownSync => Boolean(state)),
];
const sourceWork =
syncStates.find((state) => state.state === 'running') ??
syncStates.find((state) => state.state === 'failed') ??
ownSync;
const sourceFreshness = freshnessOf(input.fetchedAt, input.freshForMs);
const freshness = [
sourceFreshness,
...(input.dependencies ?? [])
.filter((dependency) => !dependency.unavailableReason)
.map((dependency) => dependency.freshness),
];
const timestamps = [
input.fetchedAt?.toISOString(),
...(input.dependencies ?? []).map(
(dependency) => dependency.lastSuccessfulAt,
),
]
.filter((timestamp): timestamp is string => Boolean(timestamp))
// eslint-disable-next-line sonarjs/no-alphabetical-sort -- Code-unit ordering is intentional for canonical keys and ISO dates; locale collation would change persisted bindings.
.sort();
const meta = {
resourceKey: input.resourceKey,
...(input.unavailableReason
? { unavailableReason: input.unavailableReason }
: {}),
dataRevision: input.dataRevision,
availability: availability(input.availability, input.work),
completeness:
availability(input.availability, input.work) === 'available'
? ('complete' as const)
: ('incomplete' as const),
freshness: combinedFreshness(freshness),
lastSuccessfulAt: timestamps[0] ?? null,
lastAttempt: input.lastAttempt ??
attempt(input.work),
...(input.coverage ? { coverage: input.coverage } : {}),
...(input.dependencies ? { dependencies: input.dependencies } : {}),
};
return Object.defineProperty(meta, 'sync', {
enumerable: false,
value: sourceWork,
}) as InvestmentResourceMetaDto;
}
function sourceSync(work: Work) {
let state: 'failed' | 'idle' | 'running' = 'idle';
if (work?.state === 'running') state = 'running';
else if (work?.state === 'failed') state = 'failed';
return { id: work?.id ?? null, state, errorCode: work?.errorCode ?? null };
}
function freshnessOf(fetchedAt: Date | null, freshForMs: number) {
if (!fetchedAt) return 'unknown' as const;
return Date.now() - fetchedAt.valueOf() < freshForMs
? ('fresh' as const)
: ('stale' as const);
}
function combinedFreshness(values: InvestmentResourceMetaDto['freshness'][]) {
if (values.includes('unknown')) return 'unknown' as const;
if (values.includes('stale')) return 'stale' as const;
return 'fresh' as const;
}
function attempt(work: Work): Attempt {
const base = { startedAt: null, finishedAt: null };
if (work?.state === 'running') return { ...base, state: 'running', problems: [] };
if (work?.state === 'failed') {
return {
...base,
state: 'failed',
problems: work.errorCode
? [{ section: 'membership', accountId: null, errorCode: work.errorCode }]
: [],
};
}
return { ...base, state: 'none', problems: [] };
}
function availability(value: LegacyAvailability, work: Work) {
if (
value === 'available' ||
value === 'connection-unavailable' ||
value === 'initial-loading' ||
value === 'temporarily-unavailable'
) {
return value;
}
if (value === 'complete') return 'available' as const;
if (value === 'partial') {
return work?.state === 'running'
? ('initial-loading' as const)
: ('temporarily-unavailable' as const);
}
return work?.state === 'running'
? ('initial-loading' as const)
: ('temporarily-unavailable' as const);
}