@actuarial-ts/data 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -7
- package/dist/customizationContracts.d.ts +43 -9
- package/dist/customizationContracts.d.ts.map +1 -1
- package/dist/customizationContracts.js +14 -5
- package/dist/customizationContracts.js.map +1 -1
- package/dist/diagnosticInput.d.ts.map +1 -1
- package/dist/diagnosticInput.js +236 -23
- package/dist/diagnosticInput.js.map +1 -1
- package/dist/historyMapping.d.ts +199 -9
- package/dist/historyMapping.d.ts.map +1 -1
- package/dist/historyMapping.js +34 -8
- package/dist/historyMapping.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -2
- package/src/customizationContracts.ts +15 -6
- package/src/diagnosticInput.ts +226 -21
- package/src/historyMapping.ts +41 -10
- package/src/version.ts +1 -1
|
@@ -250,7 +250,10 @@ export const historicalDatasetInputSchema = z
|
|
|
250
250
|
sources: z
|
|
251
251
|
.array(
|
|
252
252
|
z
|
|
253
|
-
.object({
|
|
253
|
+
.object({
|
|
254
|
+
namespace: token, artifactId: token, mappingVersion: token,
|
|
255
|
+
additionalArtifacts: z.array(z.object({ artifactId: token, mappingVersion: token }).strict()).min(1).optional(),
|
|
256
|
+
})
|
|
254
257
|
.strict(),
|
|
255
258
|
)
|
|
256
259
|
.min(1),
|
|
@@ -273,7 +276,7 @@ export const historicalDatasetInputSchema = z
|
|
|
273
276
|
.strict()
|
|
274
277
|
.superRefine((input, context) => {
|
|
275
278
|
const namespaces = new Set<string>();
|
|
276
|
-
const artifactByNamespace = new Map<string, string
|
|
279
|
+
const artifactByNamespace = new Map<string, Set<string>>();
|
|
277
280
|
input.sources.forEach((source, index) => {
|
|
278
281
|
if (namespaces.has(source.namespace))
|
|
279
282
|
context.addIssue({
|
|
@@ -282,7 +285,13 @@ export const historicalDatasetInputSchema = z
|
|
|
282
285
|
message: "Source namespace is duplicated",
|
|
283
286
|
});
|
|
284
287
|
namespaces.add(source.namespace);
|
|
285
|
-
|
|
288
|
+
const artifacts = new Set([source.artifactId]);
|
|
289
|
+
source.additionalArtifacts?.forEach((artifact, position) => {
|
|
290
|
+
if (artifacts.has(artifact.artifactId))
|
|
291
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["sources", index, "additionalArtifacts", position, "artifactId"], message: "Artifact ID is duplicated within the source namespace" });
|
|
292
|
+
artifacts.add(artifact.artifactId);
|
|
293
|
+
});
|
|
294
|
+
artifactByNamespace.set(source.namespace, artifacts);
|
|
286
295
|
});
|
|
287
296
|
input.records.forEach((record, index) => {
|
|
288
297
|
if (!namespaces.has(record.sourceNamespace))
|
|
@@ -293,7 +302,7 @@ export const historicalDatasetInputSchema = z
|
|
|
293
302
|
});
|
|
294
303
|
if (
|
|
295
304
|
artifactByNamespace.has(record.sourceNamespace) &&
|
|
296
|
-
artifactByNamespace.get(record.sourceNamespace)
|
|
305
|
+
!artifactByNamespace.get(record.sourceNamespace)!.has(record.source.artifactId)
|
|
297
306
|
)
|
|
298
307
|
context.addIssue({
|
|
299
308
|
code: z.ZodIssueCode.custom,
|
|
@@ -391,7 +400,7 @@ export const historicalDatasetInputSchema = z
|
|
|
391
400
|
});
|
|
392
401
|
if (
|
|
393
402
|
artifactByNamespace.has(balance.sourceNamespace) &&
|
|
394
|
-
artifactByNamespace.get(balance.sourceNamespace)
|
|
403
|
+
!artifactByNamespace.get(balance.sourceNamespace)!.has(balance.source.artifactId)
|
|
395
404
|
)
|
|
396
405
|
context.addIssue({
|
|
397
406
|
code: z.ZodIssueCode.custom,
|
|
@@ -422,7 +431,7 @@ const statistic = z.discriminatedUnion("kind", [
|
|
|
422
431
|
numeratorMeasureId: token,
|
|
423
432
|
denominatorMeasureId: token,
|
|
424
433
|
scale: finite,
|
|
425
|
-
denominatorRule: z.
|
|
434
|
+
denominatorRule: z.literal("positive"),
|
|
426
435
|
})
|
|
427
436
|
.strict(),
|
|
428
437
|
z
|
package/src/diagnosticInput.ts
CHANGED
|
@@ -350,12 +350,33 @@ const compactInputByCompletedRun = new WeakMap<
|
|
|
350
350
|
CompletedCompactMetricDiagnosticsRun,
|
|
351
351
|
CompactValidatedDiagnosticRunInput
|
|
352
352
|
>();
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
353
|
+
// Validated inputs are JSON-shaped trees (the schema copies every row), so a
|
|
354
|
+
// shared node is a small leaf visited once per reference rather than tracked in
|
|
355
|
+
// a table of every node. Ancestors are recorded only past this depth, which a
|
|
356
|
+
// cycle in a caller-supplied graph must exceed before it is cut.
|
|
357
|
+
const UNGUARDED_FREEZE_DEPTH = 64;
|
|
358
|
+
function freeze<T>(value: T, skip?: WeakSet<object>): DiagnosticDeepReadonly<T> {
|
|
359
|
+
let ancestors: Set<object> | undefined;
|
|
360
|
+
const visit = (node: unknown, depth: number): void => {
|
|
361
|
+
if (node === null || typeof node !== "object" || skip?.has(node)) return;
|
|
362
|
+
const guarded = depth > UNGUARDED_FREEZE_DEPTH;
|
|
363
|
+
if (guarded) {
|
|
364
|
+
ancestors ??= new Set();
|
|
365
|
+
if (ancestors.has(node)) return;
|
|
366
|
+
ancestors.add(node);
|
|
367
|
+
}
|
|
368
|
+
if (Array.isArray(node)) {
|
|
369
|
+
for (let index = 0; index < node.length; index++) visit(node[index], depth + 1);
|
|
370
|
+
} else {
|
|
371
|
+
const record = node as Record<string, unknown>;
|
|
372
|
+
const keys = Object.keys(record);
|
|
373
|
+
for (let index = 0; index < keys.length; index++) visit(record[keys[index]!], depth + 1);
|
|
374
|
+
}
|
|
375
|
+
if (guarded) ancestors!.delete(node);
|
|
376
|
+
Object.freeze(node);
|
|
377
|
+
};
|
|
378
|
+
visit(value, 0);
|
|
379
|
+
return value as DiagnosticDeepReadonly<T>;
|
|
359
380
|
}
|
|
360
381
|
function issues(error: z.ZodError): DiagnosticValidationError {
|
|
361
382
|
return new DiagnosticValidationError(
|
|
@@ -382,22 +403,39 @@ function sortedRecord<T>(value: Readonly<Record<string, T>>): Readonly<Record<st
|
|
|
382
403
|
return result;
|
|
383
404
|
}
|
|
384
405
|
|
|
406
|
+
interface UndefinedScanFrame {
|
|
407
|
+
readonly value: unknown;
|
|
408
|
+
readonly parent: UndefinedScanFrame | null;
|
|
409
|
+
readonly key: string | null;
|
|
410
|
+
}
|
|
411
|
+
/** Renders the JSON path of a frame only when an issue must cite it. */
|
|
412
|
+
function scanFramePath(frame: UndefinedScanFrame): string {
|
|
413
|
+
if (frame.parent === null || frame.key === null) return "$";
|
|
414
|
+
const parentPath = scanFramePath(frame.parent);
|
|
415
|
+
return Array.isArray(frame.parent.value)
|
|
416
|
+
? `${parentPath}[${frame.key}]`
|
|
417
|
+
: /^[A-Za-z_$][\w$]*$/.test(frame.key)
|
|
418
|
+
? `${parentPath}.${frame.key}`
|
|
419
|
+
: `${parentPath}[${JSON.stringify(frame.key)}]`;
|
|
420
|
+
}
|
|
385
421
|
function explicitUndefinedIssues(value: unknown): DiagnosticValidationIssue[] {
|
|
386
422
|
const found: DiagnosticValidationIssue[] = [];
|
|
387
|
-
const stack:
|
|
423
|
+
const stack: UndefinedScanFrame[] = [{ value, parent: null, key: null }];
|
|
388
424
|
const seen = new WeakSet<object>();
|
|
389
425
|
while (stack.length > 0) {
|
|
390
426
|
const current = stack.pop()!;
|
|
391
427
|
if (current.value === null || typeof current.value !== "object" || seen.has(current.value))
|
|
392
428
|
continue;
|
|
393
429
|
seen.add(current.value);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
430
|
+
const record = current.value as Record<string, unknown>;
|
|
431
|
+
// Large inputs are ordinary: build no path text or entry pairs on the
|
|
432
|
+
// common path, only the frame links needed to render a path on demand.
|
|
433
|
+
const keys = Object.keys(record);
|
|
434
|
+
for (let index = 0; index < keys.length; index++) {
|
|
435
|
+
const key = keys[index]!;
|
|
436
|
+
const child = record[key];
|
|
437
|
+
if (child === undefined) {
|
|
438
|
+
const path = scanFramePath({ value: child, parent: current, key });
|
|
401
439
|
found.push({
|
|
402
440
|
domain: path.startsWith("$.definition")
|
|
403
441
|
? "definition"
|
|
@@ -410,21 +448,188 @@ function explicitUndefinedIssues(value: unknown): DiagnosticValidationIssue[] {
|
|
|
410
448
|
path,
|
|
411
449
|
message: "Explicit undefined is not allowed",
|
|
412
450
|
});
|
|
413
|
-
else
|
|
451
|
+
} else if (child !== null && typeof child === "object")
|
|
452
|
+
stack.push({ value: child, parent: current, key });
|
|
414
453
|
}
|
|
415
454
|
}
|
|
416
455
|
return found;
|
|
417
456
|
}
|
|
418
457
|
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// Large-input fast path for the two row arrays. Zod validates and copies every
|
|
460
|
+
// row through generic machinery, which on 100,000-row inputs allocates more
|
|
461
|
+
// than the preparation itself. Rows are accepted here only under exactly the
|
|
462
|
+
// schema's rules (plain records, strict keys, token strings, boolean flags,
|
|
463
|
+
// safe non-negative integer source rows, raw number-or-null measures) and are
|
|
464
|
+
// copied in the schema's key order, so the owned rows are indistinguishable
|
|
465
|
+
// from the schema's output. Anything the check does not accept, including any
|
|
466
|
+
// non-plain prototype, is handed to the schema unchanged, so every rejection
|
|
467
|
+
// and every error message still comes from zod.
|
|
468
|
+
type FastLossRow = z.output<typeof lossSchema>;
|
|
469
|
+
type FastExposureRow = z.output<typeof exposureSchema>;
|
|
470
|
+
const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
|
|
471
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
472
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
473
|
+
return prototype === Object.prototype || prototype === null;
|
|
474
|
+
};
|
|
475
|
+
const hasOwn = (value: object, key: string): boolean => Object.prototype.hasOwnProperty.call(value, key);
|
|
476
|
+
function fastSource(value: unknown): z.output<typeof sourceSchema> | undefined {
|
|
477
|
+
if (!isPlainRecord(value)) return undefined;
|
|
478
|
+
const artifactId = value.artifactId;
|
|
479
|
+
if (!isDiagnosticToken(artifactId)) return undefined;
|
|
480
|
+
const out: Record<string, unknown> = { artifactId };
|
|
481
|
+
let matched = 1;
|
|
482
|
+
for (const field of ["sourceFile", "sourceSheet"] as const)
|
|
483
|
+
if (hasOwn(value, field)) {
|
|
484
|
+
const item = value[field];
|
|
485
|
+
if (!isDiagnosticToken(item)) return undefined;
|
|
486
|
+
out[field] = item;
|
|
487
|
+
matched++;
|
|
488
|
+
}
|
|
489
|
+
if (hasOwn(value, "sourceRow")) {
|
|
490
|
+
const item = value.sourceRow;
|
|
491
|
+
if (typeof item !== "number" || !Number.isSafeInteger(item) || item < 0) return undefined;
|
|
492
|
+
out.sourceRow = item;
|
|
493
|
+
matched++;
|
|
494
|
+
}
|
|
495
|
+
if (hasOwn(value, "sourceCell")) {
|
|
496
|
+
const item = value.sourceCell;
|
|
497
|
+
if (!isDiagnosticToken(item)) return undefined;
|
|
498
|
+
out.sourceCell = item;
|
|
499
|
+
matched++;
|
|
500
|
+
}
|
|
501
|
+
if (Object.keys(value).length !== matched) return undefined;
|
|
502
|
+
return out as z.output<typeof sourceSchema>;
|
|
503
|
+
}
|
|
504
|
+
function fastMeasures(value: unknown): Record<string, number | null> | undefined {
|
|
505
|
+
if (!isPlainRecord(value)) return undefined;
|
|
506
|
+
const out: Record<string, number | null> = {};
|
|
507
|
+
const keys = Object.keys(value);
|
|
508
|
+
for (let index = 0; index < keys.length; index++) {
|
|
509
|
+
const key = keys[index]!;
|
|
510
|
+
const item = value[key];
|
|
511
|
+
if (item !== null && typeof item !== "number") return undefined;
|
|
512
|
+
// The schema's key round trip keeps a literal "__proto__" as own data.
|
|
513
|
+
if (key === "__proto__")
|
|
514
|
+
Object.defineProperty(out, key, { value: item, enumerable: true, writable: true, configurable: true });
|
|
515
|
+
else out[key] = item;
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
function fastLossRow(value: unknown): FastLossRow | undefined {
|
|
520
|
+
if (!isPlainRecord(value)) return undefined;
|
|
521
|
+
const { recordId, sourceGroup, origin, valuation, complete, rowType } = value;
|
|
522
|
+
if (
|
|
523
|
+
!isDiagnosticToken(recordId) ||
|
|
524
|
+
!isDiagnosticToken(sourceGroup) ||
|
|
525
|
+
!isDiagnosticToken(origin) ||
|
|
526
|
+
!isDiagnosticToken(valuation) ||
|
|
527
|
+
typeof complete !== "boolean"
|
|
528
|
+
)
|
|
529
|
+
return undefined;
|
|
530
|
+
const out: Record<string, unknown> = { recordId, sourceGroup, origin, valuation, complete };
|
|
531
|
+
let expected = 7;
|
|
532
|
+
if (hasOwn(value, "source")) {
|
|
533
|
+
const source = fastSource(value.source);
|
|
534
|
+
if (source === undefined) return undefined;
|
|
535
|
+
out.source = source;
|
|
536
|
+
expected++;
|
|
537
|
+
}
|
|
538
|
+
const measures = fastMeasures(value.measures);
|
|
539
|
+
if (measures === undefined) return undefined;
|
|
540
|
+
out.measures = measures;
|
|
541
|
+
if (rowType === "claim") {
|
|
542
|
+
const claimId = value.claimId;
|
|
543
|
+
if (!hasOwn(value, "claimId") || !isDiagnosticToken(claimId)) return undefined;
|
|
544
|
+
out.rowType = "claim";
|
|
545
|
+
out.claimId = claimId;
|
|
546
|
+
expected++;
|
|
547
|
+
} else if (rowType === "aggregate") out.rowType = "aggregate";
|
|
548
|
+
else return undefined;
|
|
549
|
+
if (Object.keys(value).length !== expected) return undefined;
|
|
550
|
+
return out as FastLossRow;
|
|
551
|
+
}
|
|
552
|
+
function fastExposureRow(value: unknown): FastExposureRow | undefined {
|
|
553
|
+
if (!isPlainRecord(value)) return undefined;
|
|
554
|
+
const { key, sourceGroup, origin, measureId, complete } = value;
|
|
555
|
+
const measure = value.value;
|
|
556
|
+
if (
|
|
557
|
+
!isDiagnosticToken(key) ||
|
|
558
|
+
!isDiagnosticToken(sourceGroup) ||
|
|
559
|
+
!isDiagnosticToken(origin) ||
|
|
560
|
+
!isDiagnosticToken(measureId) ||
|
|
561
|
+
(measure !== null && typeof measure !== "number") ||
|
|
562
|
+
typeof complete !== "boolean"
|
|
563
|
+
)
|
|
564
|
+
return undefined;
|
|
565
|
+
const out: Record<string, unknown> = { key, sourceGroup, origin };
|
|
566
|
+
let expected = 6;
|
|
567
|
+
if (hasOwn(value, "valuation")) {
|
|
568
|
+
const valuation = value.valuation;
|
|
569
|
+
if (!isDiagnosticToken(valuation)) return undefined;
|
|
570
|
+
out.valuation = valuation;
|
|
571
|
+
expected++;
|
|
572
|
+
}
|
|
573
|
+
out.measureId = measureId;
|
|
574
|
+
out.value = measure;
|
|
575
|
+
out.complete = complete;
|
|
576
|
+
if (hasOwn(value, "source")) {
|
|
577
|
+
const source = fastSource(value.source);
|
|
578
|
+
if (source === undefined) return undefined;
|
|
579
|
+
out.source = source;
|
|
580
|
+
expected++;
|
|
581
|
+
}
|
|
582
|
+
if (Object.keys(value).length !== expected) return undefined;
|
|
583
|
+
return out as FastExposureRow;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Validates and copies the row arrays directly when every row is schema-shaped.
|
|
587
|
+
* Returns the owned rows plus a shell of the input with empty row arrays for
|
|
588
|
+
* the schema to validate everything else; undefined defers the whole input.
|
|
589
|
+
*/
|
|
590
|
+
function fastSourceRows(
|
|
591
|
+
value: unknown,
|
|
592
|
+
): { shell: Record<string, unknown>; losses: FastLossRow[]; exposures: FastExposureRow[] | undefined } | undefined {
|
|
593
|
+
if (!isPlainRecord(value) || !Array.isArray(value.losses)) return undefined;
|
|
594
|
+
const losses: FastLossRow[] = [];
|
|
595
|
+
for (let index = 0; index < value.losses.length; index++) {
|
|
596
|
+
const row = fastLossRow(value.losses[index]);
|
|
597
|
+
if (row === undefined) return undefined;
|
|
598
|
+
losses.push(row);
|
|
599
|
+
}
|
|
600
|
+
let exposures: FastExposureRow[] | undefined;
|
|
601
|
+
if (hasOwn(value, "exposures")) {
|
|
602
|
+
if (!Array.isArray(value.exposures)) return undefined;
|
|
603
|
+
exposures = [];
|
|
604
|
+
for (let index = 0; index < value.exposures.length; index++) {
|
|
605
|
+
const row = fastExposureRow(value.exposures[index]);
|
|
606
|
+
if (row === undefined) return undefined;
|
|
607
|
+
exposures.push(row);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
shell: { ...value, losses: [], ...(exposures === undefined ? {} : { exposures: [] }) },
|
|
612
|
+
losses,
|
|
613
|
+
exposures,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
419
617
|
// Both public gateways share the same full validation/ownership boundary.
|
|
420
618
|
// Selecting compact storage never invokes the eager preparation first.
|
|
421
619
|
function validateRunInputContent(value: unknown): DiagnosticRunInputContent {
|
|
422
|
-
|
|
620
|
+
// A row the fast check accepts holds no explicit undefined anywhere: every
|
|
621
|
+
// field it reads must be a token, boolean, or number-or-null, and any other
|
|
622
|
+
// value (including undefined) makes it decline the whole input. So when it
|
|
623
|
+
// accepts, scanning its row-free shell reports exactly the same issues.
|
|
624
|
+
const fast = fastSourceRows(value);
|
|
625
|
+
const undefinedIssues = explicitUndefinedIssues(fast === undefined ? value : fast.shell);
|
|
423
626
|
if (undefinedIssues.length > 0) throw new DiagnosticValidationError(undefinedIssues);
|
|
424
|
-
const parsed = runSchema.safeParse(value);
|
|
627
|
+
const parsed = runSchema.safeParse(fast === undefined ? value : fast.shell);
|
|
425
628
|
if (!parsed.success) throw issues(parsed.error);
|
|
629
|
+
const losses = fast === undefined ? parsed.data.losses : fast.losses;
|
|
630
|
+
const exposures = (fast === undefined ? parsed.data.exposures : fast.exposures) ?? [];
|
|
426
631
|
const definition = compileDiagnosticDefinition(parsed.data.definition as DiagnosticDefinition);
|
|
427
|
-
const relationIssues: DiagnosticValidationIssue[] =
|
|
632
|
+
const relationIssues: DiagnosticValidationIssue[] = losses.flatMap((row, index) =>
|
|
428
633
|
row.rowType === definition.definition.lossRowGrain
|
|
429
634
|
? []
|
|
430
635
|
: [
|
|
@@ -436,7 +641,7 @@ function validateRunInputContent(value: unknown): DiagnosticRunInputContent {
|
|
|
436
641
|
},
|
|
437
642
|
],
|
|
438
643
|
);
|
|
439
|
-
for (const [index, row] of
|
|
644
|
+
for (const [index, row] of exposures.entries()) {
|
|
440
645
|
const measure = definition.definition.measures.find((item) => item.id === row.measureId);
|
|
441
646
|
if (measure?.exposureTiming === "valuation-specific" && row.valuation === undefined)
|
|
442
647
|
relationIssues.push({
|
|
@@ -485,8 +690,8 @@ function validateRunInputContent(value: unknown): DiagnosticRunInputContent {
|
|
|
485
690
|
]);
|
|
486
691
|
const result = freeze({
|
|
487
692
|
definition,
|
|
488
|
-
losses
|
|
489
|
-
exposures
|
|
693
|
+
losses,
|
|
694
|
+
exposures,
|
|
490
695
|
filter: parsed.data.filter ?? null,
|
|
491
696
|
completePeriodCutoffs: parsed.data.completePeriodCutoffs ?? [],
|
|
492
697
|
expectedCells: parsed.data.expectedCells ?? null,
|
package/src/historyMapping.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DiagnosticValidationError,
|
|
3
|
+
canonicalJson,
|
|
3
4
|
diagnosticJsonPreflight,
|
|
4
5
|
isDiagnosticToken,
|
|
5
6
|
isRealIsoDate,
|
|
@@ -13,6 +14,12 @@ export interface HistoricalColumnSelector {
|
|
|
13
14
|
readonly columns: readonly string[];
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/** Identity parts are required, ordered strings; each part may declare column aliases. */
|
|
18
|
+
export type HistoricalIdentitySelector = HistoricalColumnSelector | {
|
|
19
|
+
readonly kind: "composite";
|
|
20
|
+
readonly parts: readonly (HistoricalColumnSelector & { readonly format?: HistoricalDateSelector["format"] })[];
|
|
21
|
+
};
|
|
22
|
+
|
|
16
23
|
export interface HistoricalDateSelector extends HistoricalColumnSelector {
|
|
17
24
|
readonly format: "iso-yyyy-mm-dd" | "mdy-slash" | "dmy-slash";
|
|
18
25
|
}
|
|
@@ -40,8 +47,8 @@ export interface HistoricalSourceMapping {
|
|
|
40
47
|
readonly grain: HistoricalObservationRecord["grain"];
|
|
41
48
|
readonly completeness: HistoricalObservationRecord["completeness"];
|
|
42
49
|
readonly fields: {
|
|
43
|
-
readonly recordId:
|
|
44
|
-
readonly claimId:
|
|
50
|
+
readonly recordId: HistoricalIdentitySelector;
|
|
51
|
+
readonly claimId: HistoricalIdentitySelector;
|
|
45
52
|
readonly componentId?: HistoricalColumnSelector;
|
|
46
53
|
readonly accidentDate?: HistoricalDateSelector;
|
|
47
54
|
readonly reportDate?: HistoricalDateSelector;
|
|
@@ -58,7 +65,7 @@ export interface HistoricalSourceMapping {
|
|
|
58
65
|
readonly coverageIds?: HistoricalColumnSelector & { readonly delimiter: string };
|
|
59
66
|
};
|
|
60
67
|
readonly revision: {
|
|
61
|
-
readonly id:
|
|
68
|
+
readonly id: HistoricalIdentitySelector;
|
|
62
69
|
readonly action?: HistoricalColumnSelector;
|
|
63
70
|
readonly sequence?: HistoricalNumberSelector;
|
|
64
71
|
readonly correctedAt?: HistoricalColumnSelector;
|
|
@@ -98,6 +105,15 @@ export interface HistoricalMappingResult {
|
|
|
98
105
|
|
|
99
106
|
const token = z.string().refine(isDiagnosticToken, "Expected a valid nonempty token");
|
|
100
107
|
const columnSelector = z.object({ columns: z.array(token).min(1) }).strict();
|
|
108
|
+
const identitySelector = z.union([
|
|
109
|
+
columnSelector,
|
|
110
|
+
z.object({
|
|
111
|
+
kind: z.literal("composite"),
|
|
112
|
+
parts: z.array(columnSelector.extend({
|
|
113
|
+
format: z.enum(["iso-yyyy-mm-dd", "mdy-slash", "dmy-slash"]).optional(),
|
|
114
|
+
}).strict()).min(2).max(8),
|
|
115
|
+
}).strict(),
|
|
116
|
+
]);
|
|
101
117
|
const dateSelector = columnSelector.extend({
|
|
102
118
|
format: z.enum(["iso-yyyy-mm-dd", "mdy-slash", "dmy-slash"]),
|
|
103
119
|
}).strict();
|
|
@@ -148,8 +164,8 @@ export const historicalSourceMappingSchema = z
|
|
|
148
164
|
completeness: z.enum(["complete-snapshot", "partial-snapshot", "change-only"]),
|
|
149
165
|
fields: z
|
|
150
166
|
.object({
|
|
151
|
-
recordId:
|
|
152
|
-
claimId:
|
|
167
|
+
recordId: identitySelector,
|
|
168
|
+
claimId: identitySelector,
|
|
153
169
|
componentId: columnSelector.optional(),
|
|
154
170
|
accidentDate: dateSelector.optional(),
|
|
155
171
|
reportDate: dateSelector.optional(),
|
|
@@ -171,7 +187,7 @@ export const historicalSourceMappingSchema = z
|
|
|
171
187
|
.optional(),
|
|
172
188
|
revision: z
|
|
173
189
|
.object({
|
|
174
|
-
id:
|
|
190
|
+
id: identitySelector,
|
|
175
191
|
action: columnSelector.optional(),
|
|
176
192
|
sequence: numberSelector.optional(),
|
|
177
193
|
correctedAt: columnSelector.optional(),
|
|
@@ -317,7 +333,11 @@ function splitIds(value: HistoricalScalar | undefined, delimiter: string): strin
|
|
|
317
333
|
|
|
318
334
|
function selectorColumns(mapping: HistoricalSourceMapping): Set<string> {
|
|
319
335
|
const result = new Set<string>();
|
|
320
|
-
const add = (selector:
|
|
336
|
+
const add = (selector: HistoricalIdentitySelector | undefined) => {
|
|
337
|
+
if (selector === undefined) return;
|
|
338
|
+
for (const part of "parts" in selector ? selector.parts : [selector])
|
|
339
|
+
part.columns.forEach((column) => result.add(column));
|
|
340
|
+
};
|
|
321
341
|
Object.values(mapping.fields).forEach(add);
|
|
322
342
|
Object.values(mapping.measures).forEach(add);
|
|
323
343
|
Object.values(mapping.dimensions ?? {}).forEach(add);
|
|
@@ -355,8 +375,19 @@ export function mapHistoricalSourceRows(
|
|
|
355
375
|
issues.push({ path: "$.rowNumber", message: "rowNumber must be a positive safe integer" });
|
|
356
376
|
const field = (selector: HistoricalColumnSelector | undefined, name: string, required = false) =>
|
|
357
377
|
selector === undefined ? undefined : selectedValue(row, selector, `$.${name}`, issues, required);
|
|
358
|
-
const
|
|
359
|
-
|
|
378
|
+
const identity = (selector: HistoricalIdentitySelector, name: string) => {
|
|
379
|
+
if (!("parts" in selector)) return scalarText(field(selector, name, true) ?? "");
|
|
380
|
+
const parts = selector.parts.map((part, index) => {
|
|
381
|
+
const partName = `${name}.parts[${index}]`;
|
|
382
|
+
const value = field(part, partName, true);
|
|
383
|
+
return part.format === undefined
|
|
384
|
+
? scalarText(value ?? "")
|
|
385
|
+
: parseDateValue(value, part.format, `$.${partName}`, issues) ?? "";
|
|
386
|
+
});
|
|
387
|
+
return `tuple/1:${canonicalJson(parts)}`;
|
|
388
|
+
};
|
|
389
|
+
const recordId = identity(mapping.fields.recordId, "recordId");
|
|
390
|
+
const claimId = identity(mapping.fields.claimId, "claimId");
|
|
360
391
|
const componentIdValue = field(mapping.fields.componentId, "componentId", mapping.grain === "claim-component-snapshot");
|
|
361
392
|
const statusValue = field(mapping.fields.status, "status");
|
|
362
393
|
const mappedDate = (selector: HistoricalDateSelector | undefined, name: string, required = false) => {
|
|
@@ -423,7 +454,7 @@ export function mapHistoricalSourceRows(
|
|
|
423
454
|
const coverageIds = relationship?.coverageIds
|
|
424
455
|
? splitIds(field(relationship.coverageIds, "relationships.coverageIds"), relationship.coverageIds.delimiter)
|
|
425
456
|
: undefined;
|
|
426
|
-
const revisionId =
|
|
457
|
+
const revisionId = identity(mapping.revision.id, "revision.id");
|
|
427
458
|
const actionText = scalarText(field(mapping.revision.action, "revision.action") ?? "upsert").toLowerCase();
|
|
428
459
|
if (actionText !== "upsert" && actionText !== "delete")
|
|
429
460
|
issues.push({ path: "$.revision.action", message: "Revision action must be upsert or delete" });
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const DATA_PACKAGE_VERSION = "0.
|
|
1
|
+
export const DATA_PACKAGE_VERSION = "0.11.0";
|