@ontrails/store 1.0.0-beta.14 → 1.0.0-beta.15

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.
Files changed (92) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +126 -61
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal/signal-identity.d.ts +17 -0
  9. package/dist/internal/signal-identity.d.ts.map +1 -0
  10. package/dist/internal/signal-identity.js +81 -0
  11. package/dist/internal/signal-identity.js.map +1 -0
  12. package/dist/jsonfile/index.d.ts +3 -0
  13. package/dist/jsonfile/index.d.ts.map +1 -0
  14. package/dist/jsonfile/index.js +2 -0
  15. package/dist/jsonfile/index.js.map +1 -0
  16. package/dist/jsonfile/runtime.d.ts +19 -0
  17. package/dist/jsonfile/runtime.d.ts.map +1 -0
  18. package/dist/jsonfile/runtime.js +403 -0
  19. package/dist/jsonfile/runtime.js.map +1 -0
  20. package/dist/jsonfile/types.d.ts +40 -0
  21. package/dist/jsonfile/types.d.ts.map +1 -0
  22. package/dist/{drizzle → jsonfile}/types.js.map +1 -1
  23. package/dist/store.d.ts +8 -18
  24. package/dist/store.d.ts.map +1 -1
  25. package/dist/store.js +152 -94
  26. package/dist/store.js.map +1 -1
  27. package/dist/testing.d.ts +28 -0
  28. package/dist/testing.d.ts.map +1 -0
  29. package/dist/testing.js +90 -0
  30. package/dist/testing.js.map +1 -0
  31. package/dist/trails/crud.d.ts +38 -0
  32. package/dist/trails/crud.d.ts.map +1 -0
  33. package/dist/trails/crud.js +112 -0
  34. package/dist/trails/crud.js.map +1 -0
  35. package/dist/trails/index.d.ts +7 -0
  36. package/dist/trails/index.d.ts.map +1 -0
  37. package/dist/trails/index.js +4 -0
  38. package/dist/trails/index.js.map +1 -0
  39. package/dist/trails/reconcile.d.ts +36 -0
  40. package/dist/trails/reconcile.d.ts.map +1 -0
  41. package/dist/trails/reconcile.js +142 -0
  42. package/dist/trails/reconcile.js.map +1 -0
  43. package/dist/trails/sync.d.ts +25 -0
  44. package/dist/trails/sync.d.ts.map +1 -0
  45. package/dist/trails/sync.js +96 -0
  46. package/dist/trails/sync.js.map +1 -0
  47. package/dist/trails/utils.d.ts +51 -0
  48. package/dist/trails/utils.d.ts.map +1 -0
  49. package/dist/trails/utils.js +65 -0
  50. package/dist/trails/utils.js.map +1 -0
  51. package/dist/types.d.ts +205 -35
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +6 -9
  54. package/src/__tests__/crud.test.ts +346 -0
  55. package/src/__tests__/jsonfile.test.ts +502 -0
  56. package/src/__tests__/store.test.ts +239 -33
  57. package/src/__tests__/sync-reconcile.test.ts +646 -0
  58. package/src/index.ts +12 -8
  59. package/src/internal/signal-identity.ts +162 -0
  60. package/src/jsonfile/index.ts +6 -0
  61. package/src/jsonfile/runtime.ts +702 -0
  62. package/src/jsonfile/types.ts +50 -0
  63. package/src/store.ts +296 -134
  64. package/src/testing.ts +175 -0
  65. package/src/trails/crud.ts +355 -0
  66. package/src/trails/index.ts +10 -0
  67. package/src/trails/reconcile.ts +281 -0
  68. package/src/trails/sync.ts +251 -0
  69. package/src/trails/utils.ts +96 -0
  70. package/src/types.ts +355 -63
  71. package/tsconfig.tests.json +10 -0
  72. package/tsconfig.tsbuildinfo +1 -1
  73. package/dist/drizzle/index.d.ts +0 -3
  74. package/dist/drizzle/index.d.ts.map +0 -1
  75. package/dist/drizzle/index.js +0 -2
  76. package/dist/drizzle/index.js.map +0 -1
  77. package/dist/drizzle/runtime.d.ts +0 -21
  78. package/dist/drizzle/runtime.d.ts.map +0 -1
  79. package/dist/drizzle/runtime.js +0 -458
  80. package/dist/drizzle/runtime.js.map +0 -1
  81. package/dist/drizzle/schema.d.ts +0 -15
  82. package/dist/drizzle/schema.d.ts.map +0 -1
  83. package/dist/drizzle/schema.js +0 -322
  84. package/dist/drizzle/schema.js.map +0 -1
  85. package/dist/drizzle/types.d.ts +0 -40
  86. package/dist/drizzle/types.d.ts.map +0 -1
  87. package/src/drizzle/__tests__/drizzle.test.ts +0 -469
  88. package/src/drizzle/index.ts +0 -17
  89. package/src/drizzle/runtime.ts +0 -853
  90. package/src/drizzle/schema.ts +0 -577
  91. package/src/drizzle/types.ts +0 -70
  92. /package/dist/{drizzle → jsonfile}/types.js +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crud.js","sourceRoot":"","sources":["../../src/trails/crud.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { crud } from './crud.js';
2
+ export type { CrudBlazeOverrides, CrudOptions, CrudTrails } from './crud.js';
3
+ export { reconcile } from './reconcile.js';
4
+ export type { ReconcileConflict, ReconcileOptions, ReconcileStrategy, } from './reconcile.js';
5
+ export { sync } from './sync.js';
6
+ export type { SyncEndpoint, SyncOptions, SyncTransform } from './sync.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/trails/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { crud } from './crud.js';
2
+ export { reconcile } from './reconcile.js';
3
+ export { sync } from './sync.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/trails/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAM3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { AnySignal, Resource, Trail, TrailContext } from '@ontrails/core';
2
+ import type { AnyStoreTable, EntityOf, StoreAccessor, UpsertOf } from '../types.js';
3
+ type ReconcileConnection<TTable extends AnyStoreTable> = Readonly<Record<TTable['name'], StoreAccessor<TTable>>>;
4
+ export interface ReconcileConflict<TTable extends AnyStoreTable> {
5
+ readonly current: EntityOf<TTable>;
6
+ readonly incoming: UpsertOf<TTable>;
7
+ }
8
+ export type ReconcileStrategy<TTable extends AnyStoreTable> = 'last-write-wins' | ((conflict: ReconcileConflict<TTable>, ctx: TrailContext) => Promise<UpsertOf<TTable>> | UpsertOf<TTable>);
9
+ export interface ReconcileOptions<TTable extends AnyStoreTable, TConnection extends ReconcileConnection<TTable>> {
10
+ readonly description?: string;
11
+ readonly id?: string;
12
+ readonly on?: readonly (AnySignal | string)[];
13
+ readonly resource: Resource<TConnection>;
14
+ readonly strategy?: ReconcileStrategy<TTable>;
15
+ readonly table: TTable;
16
+ }
17
+ /**
18
+ * Produce one trail that retries a versioned upsert with a conflict strategy
19
+ * when the incoming entity is stale.
20
+ *
21
+ * Reconcile is bounded to a single retry via a declarative `detour`. If a
22
+ * concurrent writer races the retry and produces a second `ConflictError`,
23
+ * the detour loop wraps it in `RetryExhaustedError<ConflictError>` so
24
+ * callers can distinguish "retry reconcile at a higher level" from
25
+ * "reconcile tried and lost the race".
26
+ *
27
+ * @remarks
28
+ * For versioned tables, the derived input schema requires an explicit
29
+ * `version` field so callers cannot sidestep optimistic concurrency at the
30
+ * input boundary. `fixtureSchema` alone makes `version` optional because
31
+ * connectors populate it for writes; reconcile must reject that relaxed
32
+ * shape.
33
+ */
34
+ export declare const reconcile: <TTable extends AnyStoreTable, TConnection extends ReconcileConnection<TTable>>(options: ReconcileOptions<TTable, TConnection>) => Trail<UpsertOf<TTable>, EntityOf<TTable>>;
35
+ export {};
36
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/trails/reconcile.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EAET,QAAQ,EACR,KAAK,EACL,YAAY,EAGb,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EACV,aAAa,EACb,QAAQ,EACR,aAAa,EACb,QAAQ,EACT,MAAM,aAAa,CAAC;AAIrB,KAAK,mBAAmB,CAAC,MAAM,SAAS,aAAa,IAAI,QAAQ,CAC/D,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAC9C,CAAC;AAEF,MAAM,WAAW,iBAAiB,CAAC,MAAM,SAAS,aAAa;IAC7D,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACrC;AAED,MAAM,MAAM,iBAAiB,CAAC,MAAM,SAAS,aAAa,IACtD,iBAAiB,GACjB,CAAC,CACC,QAAQ,EAAE,iBAAiB,CAAC,MAAM,CAAC,EACnC,GAAG,EAAE,YAAY,KACd,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AAEvD,MAAM,WAAW,gBAAgB,CAC/B,MAAM,SAAS,aAAa,EAC5B,WAAW,SAAS,mBAAmB,CAAC,MAAM,CAAC;IAE/C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAyLD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,SAAS,GACpB,MAAM,SAAS,aAAa,EAC5B,WAAW,SAAS,mBAAmB,CAAC,MAAM,CAAC,EAE/C,SAAS,gBAAgB,CAAC,MAAM,EAAE,WAAW,CAAC,KAC7C,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CA0B1C,CAAC"}
@@ -0,0 +1,142 @@
1
+ import { ConflictError, Result, ValidationError, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+ import { versionFieldName } from '../store.js';
4
+ import { createTableContour, mapStoreTrailError } from './utils.js';
5
+ const resolveAccessor = (table, resource, ctx) => {
6
+ const connection = resource.from(ctx);
7
+ return connection[table.name];
8
+ };
9
+ const omitUndefined = (value) => Object.fromEntries(Object.entries(value).filter(([, candidate]) => candidate !== undefined));
10
+ const currentVersion = (current) => current[versionFieldName];
11
+ const lastWriteWins = (conflict) => ({
12
+ ...conflict.current,
13
+ ...omitUndefined(conflict.incoming),
14
+ [versionFieldName]: currentVersion(conflict.current),
15
+ });
16
+ const normalizeResolvedInput = (table, current, resolved) => ({
17
+ ...current,
18
+ ...omitUndefined(resolved),
19
+ [table.identity]: current[table.identity],
20
+ [versionFieldName]: currentVersion(current),
21
+ });
22
+ const deriveExamples = (table) => {
23
+ const examples = table.fixtures.flatMap((fixture) => {
24
+ const parsed = table.schema.safeParse(fixture);
25
+ if (!parsed.success) {
26
+ return [];
27
+ }
28
+ return [
29
+ {
30
+ expected: parsed.data,
31
+ input: fixture,
32
+ name: `Reconcile ${table.name} ${String(fixture[table.identity])}`,
33
+ },
34
+ ];
35
+ });
36
+ return examples.length === 0 ? undefined : Object.freeze(examples);
37
+ };
38
+ const buildConflict = async (table, input, accessor, error) => {
39
+ const identifier = input[table.identity];
40
+ if (identifier === undefined) {
41
+ return error;
42
+ }
43
+ const current = await accessor.get(identifier);
44
+ return current === null ? error : { current, incoming: input };
45
+ };
46
+ const resolveStrategy = async (strategy, conflict, ctx) => strategy === 'last-write-wins'
47
+ ? lastWriteWins(conflict)
48
+ : await strategy(conflict, ctx);
49
+ /** Resolve a version conflict through the configured strategy and retry the upsert. */
50
+ const recoverConflict = async (table, input, accessor, error, strategy, ctx) => {
51
+ const conflict = await buildConflict(table, input, accessor, error);
52
+ if (conflict instanceof ConflictError) {
53
+ return Result.err(conflict);
54
+ }
55
+ const resolved = await resolveStrategy(strategy, conflict, ctx);
56
+ const normalized = normalizeResolvedInput(table, conflict.current, resolved);
57
+ return Result.ok(await accessor.upsert(normalized));
58
+ };
59
+ /**
60
+ * Build the input schema for a reconcile trail.
61
+ *
62
+ * `fixtureSchema` makes generated fields (including `version` on versioned
63
+ * tables) optional because connectors populate them. Reconcile, however,
64
+ * relies on optimistic concurrency: the caller must pass the expected
65
+ * `version` so `assertExpectedVersionMatch` can detect stale payloads. We
66
+ * therefore extend `fixtureSchema` with a required `version` field so
67
+ * callers cannot sidestep optimistic concurrency at the input boundary.
68
+ */
69
+ const buildReconcileInputSchema = (table) => table.fixtureSchema.extend({
70
+ [versionFieldName]: z.number().int(),
71
+ });
72
+ /** The blaze performs only the initial upsert; conflict recovery is handled by the detour. */
73
+ const createReconcileBlaze = (options, id) => async (input, ctx) => {
74
+ try {
75
+ const accessor = resolveAccessor(options.table, options.resource, ctx);
76
+ return Result.ok(await accessor.upsert(input));
77
+ }
78
+ catch (error) {
79
+ if (error instanceof ConflictError) {
80
+ return Result.err(error);
81
+ }
82
+ return Result.err(mapStoreTrailError(id, error));
83
+ }
84
+ };
85
+ /** Build the detour that handles ConflictError recovery via the configured strategy. */
86
+ const createReconcileDetour = (options, id, strategy) => ({
87
+ maxAttempts: 1,
88
+ on: ConflictError,
89
+ recover: async (attempt, ctx) => {
90
+ const conflictError = attempt.error;
91
+ try {
92
+ const accessor = resolveAccessor(options.table, options.resource, ctx);
93
+ return await recoverConflict(options.table, attempt.input, accessor, conflictError, strategy, ctx);
94
+ }
95
+ catch (error) {
96
+ if (error instanceof ConflictError) {
97
+ return Result.err(error);
98
+ }
99
+ return Result.err(mapStoreTrailError(id, error));
100
+ }
101
+ },
102
+ });
103
+ /**
104
+ * Produce one trail that retries a versioned upsert with a conflict strategy
105
+ * when the incoming entity is stale.
106
+ *
107
+ * Reconcile is bounded to a single retry via a declarative `detour`. If a
108
+ * concurrent writer races the retry and produces a second `ConflictError`,
109
+ * the detour loop wraps it in `RetryExhaustedError<ConflictError>` so
110
+ * callers can distinguish "retry reconcile at a higher level" from
111
+ * "reconcile tried and lost the race".
112
+ *
113
+ * @remarks
114
+ * For versioned tables, the derived input schema requires an explicit
115
+ * `version` field so callers cannot sidestep optimistic concurrency at the
116
+ * input boundary. `fixtureSchema` alone makes `version` optional because
117
+ * connectors populate it for writes; reconcile must reject that relaxed
118
+ * shape.
119
+ */
120
+ export const reconcile = (options) => {
121
+ if (!options.table.versioned) {
122
+ throw new ValidationError(`reconcile("${options.table.name}") requires a versioned store table.`);
123
+ }
124
+ const id = options.id ?? `${options.table.name}.reconcile`;
125
+ const entityContour = createTableContour(options.table);
126
+ const strategy = options.strategy ?? 'last-write-wins';
127
+ return trail(id, {
128
+ blaze: createReconcileBlaze(options, id),
129
+ contours: [entityContour],
130
+ description: options.description ??
131
+ `Reconcile version conflicts for "${options.table.name}" entities.`,
132
+ detours: [createReconcileDetour(options, id, strategy)],
133
+ examples: deriveExamples(options.table),
134
+ input: buildReconcileInputSchema(options.table),
135
+ intent: 'write',
136
+ on: options.on,
137
+ output: options.table.schema,
138
+ pattern: 'reconcile',
139
+ resources: [options.resource],
140
+ });
141
+ };
142
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../src/trails/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAU/E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA8BpE,MAAM,eAAe,GAAG,CAItB,KAAa,EACb,QAA+B,EAC/B,GAAiB,EACM,EAAE;IACzB,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtC,OAAO,UAAU,CAAC,KAAK,CAAC,IAAyB,CAA0B,CAAC;AAC9E,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAoC,KAAQ,EAAK,EAAE,CACvE,MAAM,CAAC,WAAW,CAChB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,EAAE,EAAE,CAAC,SAAS,KAAK,SAAS,CAAC,CACpE,CAAC;AAET,MAAM,cAAc,GAAG,CACrB,OAAyB,EACjB,EAAE,CAAC,OAAO,CAAC,gBAA0C,CAAW,CAAC;AAE3E,MAAM,aAAa,GAAG,CACpB,QAAmC,EACjB,EAAE,CACpB,CAAC;IACC,GAAG,QAAQ,CAAC,OAAO;IACnB,GAAG,aAAa,CAAC,QAAQ,CAAC,QAAmC,CAAC;IAC9D,CAAC,gBAAgB,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC;CACrD,CAAqB,CAAC;AAEzB,MAAM,sBAAsB,GAAG,CAC7B,KAAa,EACb,OAAyB,EACzB,QAA0B,EACR,EAAE,CACpB,CAAC;IACC,GAAG,OAAO;IACV,GAAG,aAAa,CAAC,QAAmC,CAAC;IACrD,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,OAAO,CACvB,KAAK,CAAC,QAAkC,CACG;IAC7C,CAAC,gBAAgB,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC;CAC5C,CAAqB,CAAC;AAEzB,MAAM,cAAc,GAAG,CACrB,KAAa,EAC4D,EAAE;IAC3E,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAClD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;YACL;gBACE,QAAQ,EAAE,MAAM,CAAC,IAAwB;gBACzC,KAAK,EAAE,OAA2B;gBAClC,IAAI,EAAE,aAAa,KAAK,CAAC,IAAI,IAAI,MAAM,CACrC,OAAO,CAAC,KAAK,CAAC,QAAgC,CAAC,CAChD,EAAE;aACJ;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AACrE,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,KAAK,EACzB,KAAa,EACb,KAAuB,EACvB,QAA+B,EAC/B,KAAoB,EACgC,EAAE;IACtD,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,QAA8B,CAEhD,CAAC;IAEd,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,UAAmB,CAAC,CAAC;IACxD,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AACjE,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,KAAK,EAC3B,QAAmC,EACnC,QAAmC,EACnC,GAAiB,EACU,EAAE,CAC7B,QAAQ,KAAK,iBAAiB;IAC5B,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC;IACzB,CAAC,CAAC,MAAM,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AAEpC,uFAAuF;AACvF,MAAM,eAAe,GAAG,KAAK,EAC3B,KAAa,EACb,KAAuB,EACvB,QAA+B,EAC/B,KAAoB,EACpB,QAAmC,EACnC,GAAiB,EACjB,EAAE;IACF,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IACpE,IAAI,QAAQ,YAAY,aAAa,EAAE,CAAC;QACtC,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;IAChE,MAAM,UAAU,GAAG,sBAAsB,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC7E,OAAO,MAAM,CAAC,EAAE,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;AACtD,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,yBAAyB,GAAG,CAChC,KAAa,EACgB,EAAE,CAC/B,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC;IACzB,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;CACrC,CAA2C,CAAC;AAE/C,8FAA8F;AAC9F,MAAM,oBAAoB,GACxB,CAIE,OAA8C,EAC9C,EAAU,EACV,EAAE,CACJ,KAAK,EAAE,KAAuB,EAAE,GAAiB,EAAE,EAAE;IACnD,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACvE,OAAO,MAAM,CAAC,EAAE,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;YACnC,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;QACD,OAAO,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC;IACnD,CAAC;AACH,CAAC,CAAC;AAEJ,wFAAwF;AACxF,MAAM,qBAAqB,GAAG,CAI5B,OAA8C,EAC9C,EAAU,EACV,QAAmC,EACsB,EAAE,CAAC,CAAC;IAC7D,WAAW,EAAE,CAAC;IACd,EAAE,EAAE,aAAa;IACjB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE;QAC9B,MAAM,aAAa,GAAG,OAAO,CAAC,KAAsB,CAAC;QACrD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YACvE,OAAO,MAAM,eAAe,CAC1B,OAAO,CAAC,KAAK,EACb,OAAO,CAAC,KAAK,EACb,QAAQ,EACR,aAAa,EACb,QAAQ,EACR,GAAG,CACJ,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;gBACnC,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC3B,CAAC;YACD,OAAO,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,EAAE,EAAE,KAAK,CAAgB,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAIvB,OAA8C,EACH,EAAE;IAC7C,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,eAAe,CACvB,cAAc,OAAO,CAAC,KAAK,CAAC,IAAI,sCAAsC,CACvE,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC;IAC3D,MAAM,aAAa,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IAEvD,OAAO,KAAK,CAAqC,EAAE,EAAE;QACnD,KAAK,EAAE,oBAAoB,CAAC,OAAO,EAAE,EAAE,CAAC;QACxC,QAAQ,EAAE,CAAC,aAAa,CAAC;QACzB,WAAW,EACT,OAAO,CAAC,WAAW;YACnB,oCAAoC,OAAO,CAAC,KAAK,CAAC,IAAI,aAAa;QACrE,OAAO,EAAE,CAAC,qBAAqB,CAAC,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;QACvD,QAAQ,EAAE,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC;QACvC,KAAK,EAAE,yBAAyB,CAAC,OAAO,CAAC,KAAK,CAAC;QAC/C,MAAM,EAAE,OAAO;QACf,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,MAAgD;QACtE,OAAO,EAAE,WAAW;QACpB,SAAS,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;KAC9B,CAAC,CAAC;AACL,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { AnySignal, Resource, Trail, TrailContext } from '@ontrails/core';
2
+ import type { AnyStoreTable, EntityOf, ReadOnlyStoreTableAccessor, StoreAccessor, StoreIdentifierOf, UpsertOf } from '../types.js';
3
+ type IdentityInputOf<TTable extends AnyStoreTable> = Readonly<Record<Extract<TTable['identity'], string>, StoreIdentifierOf<TTable>>>;
4
+ type SourceConnection<TTable extends AnyStoreTable> = Readonly<Record<TTable['name'], ReadOnlyStoreTableAccessor<TTable>>>;
5
+ type TargetConnection<TTable extends AnyStoreTable> = Readonly<Record<TTable['name'], StoreAccessor<TTable>>>;
6
+ export interface SyncEndpoint<TTable extends AnyStoreTable, TConnection extends SourceConnection<TTable> | TargetConnection<TTable>> {
7
+ readonly resource: Resource<TConnection>;
8
+ readonly table: TTable;
9
+ }
10
+ export type SyncTransform<TSourceTable extends AnyStoreTable, TTargetTable extends AnyStoreTable> = (entity: EntityOf<TSourceTable>, ctx: TrailContext) => Promise<UpsertOf<TTargetTable>> | UpsertOf<TTargetTable>;
11
+ export interface SyncOptions<TSourceTable extends AnyStoreTable, TTargetTable extends AnyStoreTable, TSourceConnection extends SourceConnection<TSourceTable>, TTargetConnection extends TargetConnection<TTargetTable>> {
12
+ readonly description?: string;
13
+ readonly from: SyncEndpoint<TSourceTable, TSourceConnection>;
14
+ readonly id?: string;
15
+ readonly on?: readonly (AnySignal | string)[];
16
+ readonly to: SyncEndpoint<TTargetTable, TTargetConnection>;
17
+ readonly transform?: SyncTransform<TSourceTable, TTargetTable>;
18
+ }
19
+ /**
20
+ * Produce one trail that reads one source entity and writes the transformed
21
+ * result into a target store resource.
22
+ */
23
+ export declare const sync: <TSourceTable extends AnyStoreTable, TTargetTable extends AnyStoreTable, TSourceConnection extends SourceConnection<TSourceTable>, TTargetConnection extends TargetConnection<TTargetTable>>(options: SyncOptions<TSourceTable, TTargetTable, TSourceConnection, TTargetConnection>) => Trail<IdentityInputOf<TSourceTable>, EntityOf<TTargetTable>>;
24
+ export {};
25
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/trails/sync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,QAAQ,EACR,KAAK,EACL,YAAY,EAEb,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EACV,aAAa,EACb,QAAQ,EACR,0BAA0B,EAC1B,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACT,MAAM,aAAa,CAAC;AAGrB,KAAK,eAAe,CAAC,MAAM,SAAS,aAAa,IAAI,QAAQ,CAC3D,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC,CACvE,CAAC;AAEF,KAAK,gBAAgB,CAAC,MAAM,SAAS,aAAa,IAAI,QAAQ,CAC5D,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,0BAA0B,CAAC,MAAM,CAAC,CAAC,CAC3D,CAAC;AAEF,KAAK,gBAAgB,CAAC,MAAM,SAAS,aAAa,IAAI,QAAQ,CAC5D,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAC9C,CAAC;AAEF,MAAM,WAAW,YAAY,CAC3B,MAAM,SAAS,aAAa,EAC5B,WAAW,SAAS,gBAAgB,CAAC,MAAM,CAAC,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAEvE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,aAAa,CACvB,YAAY,SAAS,aAAa,EAClC,YAAY,SAAS,aAAa,IAChC,CACF,MAAM,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC9B,GAAG,EAAE,YAAY,KACd,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;AAE9D,MAAM,WAAW,WAAW,CAC1B,YAAY,SAAS,aAAa,EAClC,YAAY,SAAS,aAAa,EAClC,iBAAiB,SAAS,gBAAgB,CAAC,YAAY,CAAC,EACxD,iBAAiB,SAAS,gBAAgB,CAAC,YAAY,CAAC;IAExD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAAC;IAC7D,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC;IAC9C,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAAC;IAC3D,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;CAChE;AAmGD;;;GAGG;AACH,eAAO,MAAM,IAAI,GACf,YAAY,SAAS,aAAa,EAClC,YAAY,SAAS,aAAa,EAClC,iBAAiB,SAAS,gBAAgB,CAAC,YAAY,CAAC,EACxD,iBAAiB,SAAS,gBAAgB,CAAC,YAAY,CAAC,EAExD,SAAS,WAAW,CAClB,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,CAClB,KACA,KAAK,CAAC,eAAe,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CA2E7D,CAAC"}
@@ -0,0 +1,96 @@
1
+ import { InternalError, NotFoundError, Result, trail } from '@ontrails/core';
2
+ import { createTableContour, mapStoreTrailError } from './utils.js';
3
+ const resolveSourceAccessor = (endpoint, ctx) => {
4
+ const connection = endpoint.resource.from(ctx);
5
+ return connection[endpoint.table.name];
6
+ };
7
+ const resolveTargetAccessor = (endpoint, ctx) => {
8
+ const connection = endpoint.resource.from(ctx);
9
+ return connection[endpoint.table.name];
10
+ };
11
+ const sourceMissingError = (table, id) => new NotFoundError(`Store table "${table.name}" could not find source entity "${String(id)}"`);
12
+ const identityInputSchema = (table) => table.schema.pick({
13
+ [table.identity]: true,
14
+ });
15
+ const deriveExamples = (sourceTable, targetTable, transform) => {
16
+ const targetById = new Map();
17
+ for (const fixture of targetTable.fixtures) {
18
+ const id = fixture[targetTable.identity];
19
+ if (id !== undefined) {
20
+ targetById.set(id, fixture);
21
+ }
22
+ }
23
+ const examples = sourceTable.fixtures.flatMap((fixture) => {
24
+ const id = fixture[sourceTable.identity];
25
+ if (id === undefined) {
26
+ return [];
27
+ }
28
+ const targetFixture = targetById.get(id) ??
29
+ (transform === undefined && targetTable.schema.safeParse(fixture).success
30
+ ? fixture
31
+ : undefined);
32
+ if (targetFixture === undefined) {
33
+ return [];
34
+ }
35
+ return [
36
+ {
37
+ expected: targetFixture,
38
+ input: { [sourceTable.identity]: id },
39
+ name: `Sync ${targetTable.name} ${String(id)}`,
40
+ },
41
+ ];
42
+ });
43
+ return examples.length === 0 ? undefined : Object.freeze(examples);
44
+ };
45
+ /**
46
+ * Produce one trail that reads one source entity and writes the transformed
47
+ * result into a target store resource.
48
+ */
49
+ export const sync = (options) => {
50
+ const id = options.id ?? `${options.to.table.name}.sync`;
51
+ const sourceContour = createTableContour(options.from.table);
52
+ const targetContour = createTableContour(options.to.table);
53
+ return trail(id, {
54
+ // oxlint-disable-next-line max-statements -- sync blaze reads more clearly as one try/catch with schema validation, transform, and accessor call inline
55
+ blaze: async (input, ctx) => {
56
+ try {
57
+ const identifier = input[options.from.table.identity];
58
+ const sourceEntity = await resolveSourceAccessor(options.from, ctx).get(identifier);
59
+ if (sourceEntity === null) {
60
+ return Result.err(sourceMissingError(options.from.table, identifier));
61
+ }
62
+ // No-transform path: the source entity is upserted directly into
63
+ // the target table. The generic signature does not require the two
64
+ // tables to be structurally compatible, so validate `next` against
65
+ // the target table's fixture schema at runtime. This catches
66
+ // accidentally omitted transforms before the underlying store sees
67
+ // a mismatched payload.
68
+ const next = options.transform === undefined
69
+ ? options.to.table.fixtureSchema.safeParse(sourceEntity)
70
+ : undefined;
71
+ if (next !== undefined && !next.success) {
72
+ return Result.err(new InternalError(`${id} produced an invalid target entity: ${next.error.message}`));
73
+ }
74
+ const payload = options.transform === undefined
75
+ ? next?.data
76
+ : await options.transform(sourceEntity, ctx);
77
+ const synced = await resolveTargetAccessor(options.to, ctx).upsert(payload);
78
+ return Result.ok(synced);
79
+ }
80
+ catch (error) {
81
+ return Result.err(mapStoreTrailError(id, error));
82
+ }
83
+ },
84
+ contours: [sourceContour, targetContour],
85
+ description: options.description ??
86
+ `Sync one "${options.from.table.name}" entity into "${options.to.table.name}".`,
87
+ examples: deriveExamples(options.from.table, options.to.table, options.transform),
88
+ input: identityInputSchema(options.from.table),
89
+ intent: 'write',
90
+ on: options.on,
91
+ output: options.to.table.schema,
92
+ pattern: 'sync',
93
+ resources: [options.from.resource, options.to.resource],
94
+ });
95
+ };
96
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/trails/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAkB7E,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA4CpE,MAAM,qBAAqB,GAAG,CAI5B,QAA2C,EAC3C,GAAiB,EACmB,EAAE;IACtC,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/C,OAAO,UAAU,CACf,QAAQ,CAAC,KAAK,CAAC,IAAyB,CACH,CAAC;AAC1C,CAAC,CAAC;AAEF,MAAM,qBAAqB,GAAG,CAI5B,QAA2C,EAC3C,GAAiB,EACM,EAAE;IACzB,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/C,OAAO,UAAU,CACf,QAAQ,CAAC,KAAK,CAAC,IAAyB,CAChB,CAAC;AAC7B,CAAC,CAAC;AAEF,MAAM,kBAAkB,GAAG,CACzB,KAAa,EACb,EAA6B,EACd,EAAE,CACjB,IAAI,aAAa,CACf,gBAAgB,KAAK,CAAC,IAAI,mCAAmC,MAAM,CAAC,EAAE,CAAC,GAAG,CAC3E,CAAC;AAEJ,MAAM,mBAAmB,GAAG,CAC1B,KAAa,EACuB,EAAE,CACtC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI;CACd,CAAkD,CAAC;AAE/D,MAAM,cAAc,GAAG,CAIrB,WAAyB,EACzB,WAAyB,EACzB,SAAgE,EAMpD,EAAE;IACd,MAAM,UAAU,GAAG,IAAI,GAAG,EAGvB,CAAC;IACJ,KAAK,MAAM,OAAO,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAC;QAC3C,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,QAAgC,CAElD,CAAC;QACd,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,OAAiC,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QACxD,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,QAAgC,CAElD,CAAC;QACd,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,aAAa,GACjB,UAAU,CAAC,GAAG,CAAC,EAAgD,CAAC;YAChE,CAAC,SAAS,KAAK,SAAS,IAAI,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO;gBACvE,CAAC,CAAE,OAAkC;gBACrC,CAAC,CAAC,SAAS,CAAC,CAAC;QAEjB,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YAChC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;YACL;gBACE,QAAQ,EAAE,aAAa;gBACvB,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAmC;gBACtE,IAAI,EAAE,QAAQ,WAAW,CAAC,IAAI,IAAI,MAAM,CAAC,EAAE,CAAC,EAAE;aAC/C;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AACrE,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAMlB,OAKC,EAC6D,EAAE;IAChE,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC;IACzD,MAAM,aAAa,GAAG,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7D,MAAM,aAAa,GAAG,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IAE3D,OAAO,KAAK,CAAwD,EAAE,EAAE;QACtE,wJAAwJ;QACxJ,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAC1B,IAAI,CAAC;gBACH,MAAM,UAAU,GAAG,KAAK,CACtB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAA8B,CACf,CAAC;gBACrC,MAAM,YAAY,GAAG,MAAM,qBAAqB,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,CACrE,UAAU,CACX,CAAC;gBAEF,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;oBAC1B,OAAO,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC;gBACxE,CAAC;gBAED,iEAAiE;gBACjE,mEAAmE;gBACnE,mEAAmE;gBACnE,6DAA6D;gBAC7D,mEAAmE;gBACnE,wBAAwB;gBACxB,MAAM,IAAI,GACR,OAAO,CAAC,SAAS,KAAK,SAAS;oBAC7B,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,SAAS,CAAC,YAAY,CAAC;oBACxD,CAAC,CAAC,SAAS,CAAC;gBAEhB,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;oBACxC,OAAO,MAAM,CAAC,GAAG,CACf,IAAI,aAAa,CACf,GAAG,EAAE,uCAAuC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CACjE,CACF,CAAC;gBACJ,CAAC;gBAED,MAAM,OAAO,GACX,OAAO,CAAC,SAAS,KAAK,SAAS;oBAC7B,CAAC,CAAE,IAAI,EAAE,IAA0C;oBACnD,CAAC,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;gBAEjD,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,MAAM,CAChE,OAAO,CACR,CAAC;gBACF,OAAO,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;YAC3B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC;YACnD,CAAC;QACH,CAAC;QACD,QAAQ,EAAE,CAAC,aAAa,EAAE,aAAa,CAAC;QACxC,WAAW,EACT,OAAO,CAAC,WAAW;YACnB,aAAa,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,kBAAkB,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,IAAI;QACjF,QAAQ,EAAE,cAAc,CACtB,OAAO,CAAC,IAAI,CAAC,KAAK,EAClB,OAAO,CAAC,EAAE,CAAC,KAAK,EAChB,OAAO,CAAC,SAAS,CAMN;QACb,KAAK,EAAE,mBAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9C,MAAM,EAAE,OAAO;QACf,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,MAExB;QACD,OAAO,EAAE,MAAM;QACf,SAAS,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC;KACxD,CAAC,CAAC;AACL,CAAC,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { Contour } from '@ontrails/core';
2
+ import type { z } from 'zod';
3
+ import type { AnyStoreTable } from '../types.js';
4
+ /**
5
+ * The contour type produced by {@link createTableContour} for a given store
6
+ * table. Threads the table's name, schema shape, and identity through the
7
+ * contour generics so downstream `deriveTrail()` calls project concrete
8
+ * input/output types instead of widening back to
9
+ * `Contour<string, z.ZodRawShape, string>` (the `AnyContour` alias).
10
+ */
11
+ export type TableContour<TTable extends AnyStoreTable> = Contour<TTable['name'], TTable['schema']['shape'], Extract<TTable['identity'], keyof TTable['schema']['shape'] & string>>;
12
+ /**
13
+ * Build the shape used when deriving a contour view of a store table.
14
+ *
15
+ * Contour validates every example against the shape passed in, so the shape
16
+ * must match how fixtures are actually shaped. Store fixtures may omit
17
+ * framework-generated fields (`createdAt`, `version`, ...) because the
18
+ * connector populates them, so we mirror `fixtureSchema`'s treatment of
19
+ * generated fields: generated, non-identity fields are made optional; the
20
+ * identity field stays required because read/delete/update all derive their
21
+ * input from it. Previously reconcile.ts and sync.ts passed
22
+ * `table.schema.shape` directly and crashed when a fixture omitted
23
+ * `createdAt` or another generated field.
24
+ */
25
+ export declare const buildContourShape: (table: AnyStoreTable) => Record<string, z.ZodType>;
26
+ /**
27
+ * Derive a contour view of a store table.
28
+ *
29
+ * Both `sync` and `reconcile` use this helper so they pick up the
30
+ * fixture-shape treatment (generated fields optional).
31
+ *
32
+ * @remarks
33
+ * Intentionally not cached. `contour()` brands the identity schema via
34
+ * `Object.defineProperty(..., { writable: false })`, and re-invoking on a
35
+ * schema that's already been branded throws TypeError. Factory call sites
36
+ * already build the contour once per trail instance, so rebuilding on a
37
+ * warm call is cheap and side-effect-free.
38
+ */
39
+ export declare const createTableContour: <TTable extends AnyStoreTable>(table: TTable) => TableContour<TTable>;
40
+ /**
41
+ * Coerce an unknown thrown value into an Error instance, preserving the
42
+ * original when possible.
43
+ */
44
+ export declare const asError: (error: unknown) => Error;
45
+ /**
46
+ * Map a caught error into a `TrailsError` suitable for surfacing from a store
47
+ * trail factory. Pass-through for errors already in the taxonomy; otherwise
48
+ * wrap in an `InternalError` keyed by the trail id.
49
+ */
50
+ export declare const mapStoreTrailError: (trailId: string, error: unknown) => Error;
51
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/trails/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,CAAC,MAAM,SAAS,aAAa,IAAI,OAAO,CAC9D,MAAM,CAAC,MAAM,CAAC,EACd,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EACzB,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,CACtE,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,GAC5B,OAAO,aAAa,KACnB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAiB1B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,SAAS,aAAa,EAC7D,OAAO,MAAM,KACZ,YAAY,CAAC,MAAM,CAIM,CAAC;AAE7B;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,OAAO,KAAG,KACkB,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,EAAE,OAAO,OAAO,KAAG,KASpE,CAAC"}
@@ -0,0 +1,65 @@
1
+ import { InternalError, contour, isTrailsError } from '@ontrails/core';
2
+ /**
3
+ * Build the shape used when deriving a contour view of a store table.
4
+ *
5
+ * Contour validates every example against the shape passed in, so the shape
6
+ * must match how fixtures are actually shaped. Store fixtures may omit
7
+ * framework-generated fields (`createdAt`, `version`, ...) because the
8
+ * connector populates them, so we mirror `fixtureSchema`'s treatment of
9
+ * generated fields: generated, non-identity fields are made optional; the
10
+ * identity field stays required because read/delete/update all derive their
11
+ * input from it. Previously reconcile.ts and sync.ts passed
12
+ * `table.schema.shape` directly and crashed when a fixture omitted
13
+ * `createdAt` or another generated field.
14
+ */
15
+ export const buildContourShape = (table) => {
16
+ const shape = table.schema.shape;
17
+ const generatedNonIdentity = new Set(table.generated.filter((field) => field !== table.identity));
18
+ if (generatedNonIdentity.size === 0) {
19
+ return shape;
20
+ }
21
+ const next = {};
22
+ for (const [field, fieldSchema] of Object.entries(shape)) {
23
+ next[field] = generatedNonIdentity.has(field)
24
+ ? fieldSchema.optional()
25
+ : fieldSchema;
26
+ }
27
+ return next;
28
+ };
29
+ /**
30
+ * Derive a contour view of a store table.
31
+ *
32
+ * Both `sync` and `reconcile` use this helper so they pick up the
33
+ * fixture-shape treatment (generated fields optional).
34
+ *
35
+ * @remarks
36
+ * Intentionally not cached. `contour()` brands the identity schema via
37
+ * `Object.defineProperty(..., { writable: false })`, and re-invoking on a
38
+ * schema that's already been branded throws TypeError. Factory call sites
39
+ * already build the contour once per trail instance, so rebuilding on a
40
+ * warm call is cheap and side-effect-free.
41
+ */
42
+ export const createTableContour = (table) => contour(table.name, buildContourShape(table), {
43
+ examples: table.fixtures,
44
+ identity: table.identity,
45
+ });
46
+ /**
47
+ * Coerce an unknown thrown value into an Error instance, preserving the
48
+ * original when possible.
49
+ */
50
+ export const asError = (error) => error instanceof Error ? error : new Error(String(error));
51
+ /**
52
+ * Map a caught error into a `TrailsError` suitable for surfacing from a store
53
+ * trail factory. Pass-through for errors already in the taxonomy; otherwise
54
+ * wrap in an `InternalError` keyed by the trail id.
55
+ */
56
+ export const mapStoreTrailError = (trailId, error) => {
57
+ if (isTrailsError(error)) {
58
+ return error;
59
+ }
60
+ const resolved = asError(error);
61
+ return new InternalError(`${trailId} failed: ${resolved.message}`, {
62
+ cause: resolved,
63
+ });
64
+ };
65
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/trails/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAmBvE;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,KAAoB,EACO,EAAE;IAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAA6C,CAAC;IACzE,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAClC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,KAAK,CAAC,QAAQ,CAAC,CAC5D,CAAC;IAEF,IAAI,oBAAoB,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACpC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,IAAI,GAA8B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzD,IAAI,CAAC,KAAK,CAAC,GAAG,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC;YAC3C,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE;YACxB,CAAC,CAAC,WAAW,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,KAAa,EACS,EAAE,CACxB,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAAE;IAC5C,QAAQ,EAAE,KAAK,CAAC,QAA8C;IAC9D,QAAQ,EAAE,KAAK,CAAC,QAAQ;CACzB,CAAyB,CAAC;AAE7B;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,KAAc,EAAS,EAAE,CAC/C,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAE5D;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAe,EAAE,KAAc,EAAS,EAAE;IAC3E,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAChC,OAAO,IAAI,aAAa,CAAC,GAAG,OAAO,YAAY,QAAQ,CAAC,OAAO,EAAE,EAAE;QACjE,KAAK,EAAE,QAAQ;KAChB,CAAC,CAAC;AACL,CAAC,CAAC"}