@ontrails/store 1.0.0-beta.39 → 1.0.0-beta.41

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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @ontrails/store
2
2
 
3
+ ## 1.0.0-beta.41
4
+
5
+ ## 1.0.0-beta.40
6
+
7
+ ### Minor Changes
8
+
9
+ - [`5adb995`](https://github.com/outfitter-dev/trails/commit/5adb99551c2dda6190d46cce7f60bb08d63c99aa): Complete the v1 hard cutover from the authored `blaze` field to
10
+ `implementation` across trail contracts, surface projections, tests, examples,
11
+ and public source-analysis helpers. Existing applications must rename authored
12
+ trail behavior fields and direct trail-object access before upgrading.
13
+ - [`6712075`](https://github.com/outfitter-dev/trails/commit/67120754df3f614c7f4dd98be1fa0ba9d69b7765): Complete the v1 hard cutover from the `contour` domain-object declaration
14
+ vocabulary to `entity` across contracts, topo facts, store helpers, Warden,
15
+ Wayfinder, operator surfaces, examples, and generated locks. Existing
16
+ applications must rename contour APIs, run `trails dev reset --yes` to discard
17
+ pre-cutover local Topographer snapshots, and then recompile committed
18
+ `trails.lock` artifacts before upgrading. Those derived snapshots are
19
+ intentionally not read through a compatibility layer.
20
+ The entity-shaped wire contract advances `TopoGraph` and split lock manifests
21
+ from schema version 3 to 4; old split artifacts fail with regeneration guidance,
22
+ while the canonical root `trails.lock` remains schema version 5.
23
+ Wayfinder reports those stale rows as topo-store drift while keeping current
24
+ committed lock facts available for inspection.
25
+
26
+ ### Patch Changes
27
+
28
+ - [`9874e0b`](https://github.com/outfitter-dev/trails/commit/9874e0bb034c0f98edeb19833d9d3519c2a07a4c): Add `@ontrails/cloudflare/d1`, an env-bound Cloudflare D1 store resource for `@ontrails/store` definitions. The new subpath exports `cloudflareD1` and `connectD1`, supports the backend-agnostic store accessor contract (`get`, `list`, `upsert`, `remove`), versioned-table optimistic concurrency, fixture/mock seeding, store-derived write signals, Miniflare-backed conformance tests, and Worker env-bridge integration.
29
+
30
+ `@ontrails/core` and `@ontrails/store` no longer require the Bun global for signal fire ids or late-bound store signal tokens, so store definitions and store-derived signal emission work inside Worker modules. `@ontrails/warden` now treats `cloudflareD1` as a required Cloudflare public export with `@example` coverage.
31
+
32
+ - [`9bf592d`](https://github.com/outfitter-dev/trails/commit/9bf592ddba46aa12e3f4e6ffc0f772f7a41ed3df): Declare verified first-party adapter metadata for Drizzle, HTTP/Bun, and Store/Jsonfile so shared adapter checks can dogfood real owner targets.
33
+
3
34
  ## 1.0.0-beta.39
4
35
 
5
36
  ### Patch Changes
package/README.md CHANGED
@@ -69,7 +69,7 @@ The bound store is a resource. Use it directly in trails:
69
69
  export const list = trail('gist.list', {
70
70
  resources: [db],
71
71
  intent: 'read',
72
- blaze: async (_input, ctx) => {
72
+ implementation: async (_input, ctx) => {
73
73
  const conn = db.from(ctx);
74
74
  const gists = await conn.gists.list();
75
75
  return Result.ok(gists);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/store",
3
- "version": "1.0.0-beta.39",
3
+ "version": "1.0.0-beta.41",
4
4
  "files": [
5
5
  "src/**/*.ts",
6
6
  "!src/**/__tests__/**",
@@ -26,12 +26,17 @@
26
26
  "clean": "rm -rf dist *.tsbuildinfo"
27
27
  },
28
28
  "dependencies": {
29
- "@ontrails/core": "^1.0.0-beta.39"
29
+ "@ontrails/core": "^1.0.0-beta.41"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "zod": "^4.3.5"
33
33
  },
34
34
  "trails": {
35
+ "adapters": {
36
+ "./jsonfile": {
37
+ "target": "store"
38
+ }
39
+ },
35
40
  "adapterTargets": {
36
41
  "store": {
37
42
  "placements": [
@@ -19,6 +19,21 @@ type MutableTables<TStore extends AnyStoreDefinition> = {
19
19
 
20
20
  type StoreSignalChange = 'created' | 'removed' | 'updated';
21
21
 
22
+ const storeSignalTokenCounter = Symbol.for(
23
+ '@ontrails/store.late-bound-signal-counter'
24
+ );
25
+
26
+ const takeStoreSignalTokenCounter = (): number => {
27
+ const globals = globalThis as Record<PropertyKey, unknown>;
28
+ const current = globals[storeSignalTokenCounter];
29
+ const next = typeof current === 'number' ? current : 0;
30
+ globals[storeSignalTokenCounter] = next + 1;
31
+ return next;
32
+ };
33
+
34
+ const createStoreSignalToken = (change: StoreSignalChange): string =>
35
+ `store-${change}-${takeStoreSignalTokenCounter()}`;
36
+
22
37
  const createStoreSignalDescription = (
23
38
  tableName: string,
24
39
  change: StoreSignalChange
@@ -51,7 +66,7 @@ const createStoreSignal = <TPayload>(
51
66
  }),
52
67
  {
53
68
  kind: 'store-derived',
54
- token: Bun.randomUUIDv7(),
69
+ token: createStoreSignalToken(change),
55
70
  }
56
71
  );
57
72
 
@@ -21,8 +21,8 @@ import type {
21
21
  UpdateOf,
22
22
  } from '../types.js';
23
23
  import type { CrudOperation } from '../crud-doctrine.js';
24
- import { createTableContour } from './utils.js';
25
- import type { TableContour } from './utils.js';
24
+ import { assertCurrentEntityOption, createTableEntity } from './utils.js';
25
+ import type { TableEntity } from './utils.js';
26
26
 
27
27
  type IdentityInputOf<TTable extends AnyStoreTable> = Readonly<
28
28
  Record<Extract<TTable['identity'], string>, StoreIdentifierOf<TTable>>
@@ -32,27 +32,27 @@ type CrudConnection<TTable extends AnyStoreTable> = Readonly<
32
32
  Record<TTable['name'], StoreAccessor<TTable>>
33
33
  >;
34
34
 
35
- type TableContourFieldKey<TTable extends AnyStoreTable> = Extract<
36
- keyof z.output<TableContour<TTable>>,
35
+ type TableEntityFieldKey<TTable extends AnyStoreTable> = Extract<
36
+ keyof z.output<TableEntity<TTable>>,
37
37
  string
38
38
  >;
39
39
 
40
40
  type GeneratedFieldsOf<TTable extends AnyStoreTable> =
41
- TTable['generated'] extends readonly TableContourFieldKey<TTable>[]
41
+ TTable['generated'] extends readonly TableEntityFieldKey<TTable>[]
42
42
  ? TTable['generated']
43
43
  : readonly [];
44
44
 
45
45
  /**
46
46
  * Input type `deriveTrail` projects for a given CRUD operation against a
47
- * store table. Uses `TableContour<TTable>` so the projected input
48
- * structurally matches the contour-backed derivation path in
47
+ * store table. Uses `TableEntity<TTable>` so the projected input
48
+ * structurally matches the entity-backed derivation path in
49
49
  * `@ontrails/core`'s `deriveTrail`.
50
50
  */
51
51
  type DerivedInput<
52
52
  TTable extends AnyStoreTable,
53
53
  TOperation extends CrudOperation,
54
54
  > = DeriveTrailInput<
55
- TableContour<TTable>,
55
+ TableEntity<TTable>,
56
56
  TOperation,
57
57
  GeneratedFieldsOf<TTable>
58
58
  >;
@@ -64,7 +64,7 @@ type DerivedInput<
64
64
  type DerivedOutput<
65
65
  TTable extends AnyStoreTable,
66
66
  TOperation extends CrudOperation,
67
- > = DeriveTrailOutput<TableContour<TTable>, TOperation>;
67
+ > = DeriveTrailOutput<TableEntity<TTable>, TOperation>;
68
68
 
69
69
  type InternalCreateTrailOf<TTable extends AnyStoreTable> = Trail<
70
70
  DerivedInput<TTable, 'create'>,
@@ -144,15 +144,15 @@ export type CrudTrails<TTable extends AnyStoreTable> = readonly [
144
144
  list: ListTrailOf<TTable>,
145
145
  ] & {
146
146
  /**
147
- * The table contour the factory registered on its trails. Pass it to
148
- * `reconcile({ contour })` (or other factories over the same table) so
149
- * the topo sees one shared contour instance instead of rejecting two
147
+ * The table entity the factory registered on its trails. Pass it to
148
+ * `reconcile({ entity })` (or other factories over the same table) so
149
+ * the topo sees one shared entity instance instead of rejecting two
150
150
  * same-named rebuilds as duplicates.
151
151
  */
152
- readonly contour: TableContour<TTable>;
152
+ readonly entity: TableEntity<TTable>;
153
153
  };
154
154
 
155
- export interface CrudBlazeOverrides<TTable extends AnyStoreTable> {
155
+ export interface CrudImplementationOverrides<TTable extends AnyStoreTable> {
156
156
  readonly create?: Implementation<InsertOf<TTable>, EntityOf<TTable>>;
157
157
  readonly read?: Implementation<IdentityInputOf<TTable>, EntityOf<TTable>>;
158
158
  readonly update?: Implementation<
@@ -164,14 +164,14 @@ export interface CrudBlazeOverrides<TTable extends AnyStoreTable> {
164
164
  }
165
165
 
166
166
  export interface CrudOptions<TTable extends AnyStoreTable> {
167
- readonly blaze?: CrudBlazeOverrides<TTable>;
167
+ readonly implementation?: CrudImplementationOverrides<TTable>;
168
168
  /**
169
- * Existing table contour to register on the produced trails. When
169
+ * Existing table entity to register on the produced trails. When
170
170
  * omitted, the factory builds one from the table. Pass a shared
171
171
  * instance when another factory (e.g. `reconcile()`) covers the same
172
- * table so `topo()` sees a single contour registration.
172
+ * table so `topo()` sees a single entity registration.
173
173
  */
174
- readonly contour?: TableContour<TTable>;
174
+ readonly entity?: TableEntity<TTable>;
175
175
  /**
176
176
  * Permit requirement declared on every produced trail. Factory trails
177
177
  * carry authored defaults like any hand-written trail; per-operation
@@ -185,7 +185,7 @@ export interface CrudOptions<TTable extends AnyStoreTable> {
185
185
  readonly permits?: Partial<Record<CrudOperation, PermitRequirement>>;
186
186
  }
187
187
 
188
- interface InternalCrudBlazeOverrides<TTable extends AnyStoreTable> {
188
+ interface InternalCrudImplementationOverrides<TTable extends AnyStoreTable> {
189
189
  readonly create?: Implementation<
190
190
  DerivedInput<TTable, 'create'>,
191
191
  DerivedOutput<TTable, 'create'>
@@ -209,8 +209,8 @@ interface InternalCrudBlazeOverrides<TTable extends AnyStoreTable> {
209
209
  }
210
210
 
211
211
  interface InternalCrudOptions<TTable extends AnyStoreTable> {
212
- readonly blaze?: InternalCrudBlazeOverrides<TTable>;
213
- readonly contour?: TableContour<TTable>;
212
+ readonly implementation?: InternalCrudImplementationOverrides<TTable>;
213
+ readonly entity?: TableEntity<TTable>;
214
214
  readonly permit?: PermitRequirement;
215
215
  readonly permits?: Partial<Record<CrudOperation, PermitRequirement>>;
216
216
  }
@@ -256,7 +256,7 @@ const normalizeExamplesForOutput = <TInput, TOutput>(
256
256
  const finalizeTrail = <TInput, TOutput>(
257
257
  base: Trail<TInput, TOutput>,
258
258
  options: {
259
- readonly blaze?: Implementation<TInput, TOutput> | undefined;
259
+ readonly implementation?: Implementation<TInput, TOutput> | undefined;
260
260
  readonly output?: z.ZodType<TOutput> | undefined;
261
261
  readonly pattern?: string | undefined;
262
262
  readonly permit?: PermitRequirement | undefined;
@@ -264,7 +264,9 @@ const finalizeTrail = <TInput, TOutput>(
264
264
  ): Trail<TInput, TOutput> =>
265
265
  Object.freeze({
266
266
  ...base,
267
- ...(options.blaze === undefined ? {} : { blaze: options.blaze }),
267
+ ...(options.implementation === undefined
268
+ ? {}
269
+ : { implementation: options.implementation }),
268
270
  ...(options.output === undefined
269
271
  ? {}
270
272
  : {
@@ -281,35 +283,35 @@ const deriveCrudBaseTrails = <
281
283
  >(
282
284
  table: TTable,
283
285
  resource: Resource<TConnection>,
284
- entityContour: TableContour<TTable>
286
+ tableEntity: TableEntity<TTable>
285
287
  ): InternalCrudBaseTrails<TTable> => {
286
- // Narrow the store's `readonly string[]` to the contour's typed field-key
288
+ // Narrow the store's `readonly string[]` to the entity's typed field-key
287
289
  // array so `deriveTrail`'s `TGenerated` generic picks up the precise
288
- // key-of shape that `CreateInputOf<Contour, TGenerated>` expects. The
290
+ // key-of shape that `CreateInputOf<Entity, TGenerated>` expects. The
289
291
  // runtime value is unchanged — the names in `table.generated` are already
290
292
  // keys of `table.schema.shape` by construction in `store()`.
291
293
  const generated = table.generated as GeneratedFieldsOf<TTable>;
292
294
 
293
295
  return {
294
- createBase: deriveTrail(entityContour, 'create', {
296
+ createBase: deriveTrail(tableEntity, 'create', {
295
297
  generated,
296
298
  resource,
297
299
  }),
298
- deleteBase: deriveTrail(entityContour, 'delete', {
300
+ deleteBase: deriveTrail(tableEntity, 'delete', {
299
301
  resource,
300
302
  }),
301
- listBase: deriveTrail(entityContour, 'list', {
303
+ listBase: deriveTrail(tableEntity, 'list', {
302
304
  resource,
303
305
  }),
304
- readBase: deriveTrail(entityContour, 'read', {
306
+ readBase: deriveTrail(tableEntity, 'read', {
305
307
  resource,
306
308
  }),
307
- // The `update` blaze synthesized by `deriveTrail` handles the partial-patch
309
+ // The `update` implementation synthesized by `deriveTrail` handles the partial-patch
308
310
  // concern: when the accessor lacks a native `update`, the fallback path in
309
311
  // `derive-trail.ts` (`updateViaReadAndUpsert`) reads the current entity,
310
312
  // merges the patch, strips the `version` field, then calls `upsert` with
311
313
  // the full merged payload — so no fields are silently lost.
312
- updateBase: deriveTrail(entityContour, 'update', {
314
+ updateBase: deriveTrail(tableEntity, 'update', {
313
315
  generated,
314
316
  resource,
315
317
  }),
@@ -322,25 +324,31 @@ const buildCrudTrails = <TTable extends AnyStoreTable>(
322
324
  entityOutput: z.ZodType<DerivedOutput<TTable, 'create'>>,
323
325
  listOutput: z.ZodType<DerivedOutput<TTable, 'list'>>
324
326
  ): InternalCrudTrails<TTable> => {
325
- const overrides = options.blaze ?? {};
327
+ const overrides = options.implementation ?? {};
326
328
  const permitFor = (operation: CrudOperation): PermitRequirement | undefined =>
327
329
  options.permits?.[operation] ?? options.permit;
328
330
 
329
331
  return Object.freeze([
330
332
  finalizeTrail(baseTrails.createBase, {
331
- ...(overrides.create === undefined ? {} : { blaze: overrides.create }),
333
+ ...(overrides.create === undefined
334
+ ? {}
335
+ : { implementation: overrides.create }),
332
336
  output: entityOutput,
333
337
  pattern: 'crud',
334
338
  permit: permitFor('create'),
335
339
  }),
336
340
  finalizeTrail(baseTrails.readBase, {
337
- ...(overrides.read === undefined ? {} : { blaze: overrides.read }),
341
+ ...(overrides.read === undefined
342
+ ? {}
343
+ : { implementation: overrides.read }),
338
344
  output: entityOutput,
339
345
  pattern: 'crud',
340
346
  permit: permitFor('read'),
341
347
  }),
342
348
  finalizeTrail(baseTrails.updateBase, {
343
- ...(overrides.update === undefined ? {} : { blaze: overrides.update }),
349
+ ...(overrides.update === undefined
350
+ ? {}
351
+ : { implementation: overrides.update }),
344
352
  output: entityOutput,
345
353
  pattern: 'crud',
346
354
  permit: permitFor('update'),
@@ -351,12 +359,14 @@ const buildCrudTrails = <TTable extends AnyStoreTable>(
351
359
  permit: permitFor('delete'),
352
360
  })
353
361
  : finalizeTrail(baseTrails.deleteBase, {
354
- blaze: overrides.delete,
362
+ implementation: overrides.delete,
355
363
  pattern: 'crud',
356
364
  permit: permitFor('delete'),
357
365
  }),
358
366
  finalizeTrail(baseTrails.listBase, {
359
- ...(overrides.list === undefined ? {} : { blaze: overrides.list }),
367
+ ...(overrides.list === undefined
368
+ ? {}
369
+ : { implementation: overrides.list }),
360
370
  output: listOutput,
361
371
  pattern: 'crud',
362
372
  permit: permitFor('list'),
@@ -367,10 +377,10 @@ const buildCrudTrails = <TTable extends AnyStoreTable>(
367
377
  /**
368
378
  * Produce the standard CRUD trail tuple for one normalized store table.
369
379
  *
370
- * The factory derives schemas, examples, resources, and contour linkage from
371
- * the table metadata. Blazes default to the backend-agnostic store accessor
380
+ * The factory derives schemas, examples, resources, and entity linkage from
381
+ * the table metadata. Implementations default to the backend-agnostic store accessor
372
382
  * contract via `deriveTrail()`'s single-resource synthesis path. Per-operation
373
- * blaze overrides stay available for callers that need custom persistence
383
+ * implementation overrides stay available for callers that need custom persistence
374
384
  * behavior and are layered onto the derived trails in a single pass.
375
385
  */
376
386
  export function crud<
@@ -389,11 +399,12 @@ export function crud<
389
399
  resource: Resource<TConnection>,
390
400
  options: InternalCrudOptions<TTable> = {}
391
401
  ) {
392
- const entityContour = options.contour ?? createTableContour(table);
393
- const baseTrails = deriveCrudBaseTrails(table, resource, entityContour);
402
+ assertCurrentEntityOption(options, 'crud() options');
403
+ const tableEntity = options.entity ?? createTableEntity(table);
404
+ const baseTrails = deriveCrudBaseTrails(table, resource, tableEntity);
394
405
  // Narrow `table.schema` (typed `StoreObjectSchema`, which is
395
406
  // `z.ZodObject<Record<string, z.ZodType>>`) to a ZodObject keyed by the
396
- // concrete shape so its `z.output` unifies with the contour-derived
407
+ // concrete shape so its `z.output` unifies with the entity-derived
397
408
  // output. Structurally `table.schema` already has `shape:
398
409
  // TTable['schema']['shape']` — this only refines the generic parameter.
399
410
  const entitySchema = table.schema as z.ZodObject<TTable['schema']['shape']>;
@@ -402,11 +413,11 @@ export function crud<
402
413
  entitySchema.array();
403
414
 
404
415
  const trails = buildCrudTrails(baseTrails, options, entityOutput, listOutput);
405
- // Expose the registered contour so other factories over the same table
416
+ // Expose the registered entity so other factories over the same table
406
417
  // (reconcile, sync) can share the instance instead of rebuilding it.
407
418
  return Object.freeze(
408
- Object.assign([...trails], { contour: entityContour })
419
+ Object.assign([...trails], { entity: tableEntity })
409
420
  ) as unknown as InternalCrudTrails<TTable> & {
410
- readonly contour: TableContour<TTable>;
421
+ readonly entity: TableEntity<TTable>;
411
422
  };
412
423
  }
@@ -4,7 +4,11 @@ export type {
4
4
  CrudOperation,
5
5
  } from '../crud-doctrine.js';
6
6
  export { crud } from './crud.js';
7
- export type { CrudBlazeOverrides, CrudOptions, CrudTrails } from './crud.js';
7
+ export type {
8
+ CrudImplementationOverrides,
9
+ CrudOptions,
10
+ CrudTrails,
11
+ } from './crud.js';
8
12
  export { reconcile } from './reconcile.js';
9
13
  export type {
10
14
  ReconcileConflict,
@@ -13,4 +17,4 @@ export type {
13
17
  } from './reconcile.js';
14
18
  export { sync } from './sync.js';
15
19
  export type { SyncEndpoint, SyncOptions, SyncTransform } from './sync.js';
16
- export type { TableContour } from './utils.js';
20
+ export type { TableEntity } from './utils.js';
@@ -18,8 +18,12 @@ import type {
18
18
  UpsertOf,
19
19
  } from '../types.js';
20
20
  import { versionFieldName } from '../store.js';
21
- import { createTableContour, mapStoreTrailError } from './utils.js';
22
- import type { TableContour } from './utils.js';
21
+ import {
22
+ assertCurrentEntityOption,
23
+ createTableEntity,
24
+ mapStoreTrailError,
25
+ } from './utils.js';
26
+ import type { TableEntity } from './utils.js';
23
27
 
24
28
  type ReconcileConnection<TTable extends AnyStoreTable> = Readonly<
25
29
  Record<TTable['name'], StoreAccessor<TTable>>
@@ -42,13 +46,13 @@ export interface ReconcileOptions<
42
46
  TConnection extends ReconcileConnection<TTable>,
43
47
  > {
44
48
  /**
45
- * Existing table contour to register on the reconcile trail. Pass the
46
- * contour a `crud()` call over the same table exposes (its `contour`
49
+ * Existing table entity to register on the reconcile trail. Pass the
50
+ * entity a `crud()` call over the same table exposes (its `entity`
47
51
  * property) so `topo()` sees one shared instance instead of rejecting
48
52
  * two same-named rebuilds as duplicates. When omitted, the factory
49
53
  * builds its own.
50
54
  */
51
- readonly contour?: TableContour<TTable>;
55
+ readonly entity?: TableEntity<TTable>;
52
56
  readonly description?: string;
53
57
  readonly id?: string;
54
58
  readonly on?: readonly (AnySignal | string)[];
@@ -189,8 +193,8 @@ const buildReconcileInputSchema = <TTable extends AnyStoreTable>(
189
193
  [versionFieldName]: z.number().int(),
190
194
  }) as unknown as z.ZodType<UpsertOf<TTable>>;
191
195
 
192
- /** The blaze performs only the initial upsert; conflict recovery is handled by the detour. */
193
- const createReconcileBlaze =
196
+ /** The implementation performs only the initial upsert; conflict recovery is handled by the detour. */
197
+ const createReconcileImplementation =
194
198
  <
195
199
  TTable extends AnyStoreTable,
196
200
  TConnection extends ReconcileConnection<TTable>,
@@ -265,6 +269,7 @@ export const reconcile = <
265
269
  >(
266
270
  options: ReconcileOptions<TTable, TConnection>
267
271
  ): Trail<UpsertOf<TTable>, EntityOf<TTable>> => {
272
+ assertCurrentEntityOption(options, 'reconcile() options');
268
273
  if (!options.table.versioned) {
269
274
  throw new ValidationError(
270
275
  `reconcile("${options.table.name}") requires a versioned store table.`
@@ -272,17 +277,17 @@ export const reconcile = <
272
277
  }
273
278
 
274
279
  const id = options.id ?? `${options.table.name}.reconcile`;
275
- const entityContour = options.contour ?? createTableContour(options.table);
280
+ const tableEntity = options.entity ?? createTableEntity(options.table);
276
281
  const strategy = options.strategy ?? 'last-write-wins';
277
282
 
278
283
  return trail(id, {
279
- blaze: createReconcileBlaze(options, id),
280
- contours: [entityContour],
281
284
  description:
282
285
  options.description ??
283
286
  `Reconcile version conflicts for "${options.table.name}" entities.`,
284
287
  detours: [createReconcileDetour(options, id, strategy)],
288
+ entities: [tableEntity],
285
289
  examples: deriveExamples(options.table),
290
+ implementation: createReconcileImplementation(options, id),
286
291
  input: buildReconcileInputSchema(options.table),
287
292
  intent: 'write',
288
293
  on: options.on,
@@ -17,8 +17,12 @@ import type {
17
17
  StoreIdentifierOf,
18
18
  UpsertOf,
19
19
  } from '../types.js';
20
- import { createTableContour, mapStoreTrailError } from './utils.js';
21
- import type { TableContour } from './utils.js';
20
+ import {
21
+ assertCurrentEntityOption,
22
+ createTableEntity,
23
+ mapStoreTrailError,
24
+ } from './utils.js';
25
+ import type { TableEntity } from './utils.js';
22
26
 
23
27
  type IdentityInputOf<TTable extends AnyStoreTable> = Readonly<
24
28
  Record<Extract<TTable['identity'], string>, StoreIdentifierOf<TTable>>
@@ -37,13 +41,13 @@ export interface SyncEndpoint<
37
41
  TConnection extends SourceConnection<TTable> | TargetConnection<TTable>,
38
42
  > {
39
43
  /**
40
- * Existing table contour to register on the produced trail for this
41
- * endpoint. Pass the contour a `crud()` bundle over the same table
42
- * exposes (its `contour` property) so `topo()` sees one shared
44
+ * Existing table entity to register on the produced trail for this
45
+ * endpoint. Pass the entity a `crud()` bundle over the same table
46
+ * exposes (its `entity` property) so `topo()` sees one shared
43
47
  * instance instead of rejecting two same-named rebuilds as
44
48
  * duplicates. When omitted, the factory builds one from the table.
45
49
  */
46
- readonly contour?: TableContour<TTable>;
50
+ readonly entity?: TableEntity<TTable>;
47
51
  readonly resource: Resource<TConnection>;
48
52
  readonly table: TTable;
49
53
  }
@@ -189,24 +193,39 @@ export const sync = <
189
193
  TTargetConnection
190
194
  >
191
195
  ): Trail<IdentityInputOf<TSourceTable>, EntityOf<TTargetTable>> => {
196
+ assertCurrentEntityOption(options.from, 'sync() from options');
197
+ assertCurrentEntityOption(options.to, 'sync() to options');
192
198
  const id = options.id ?? `${options.to.table.name}.sync`;
193
- const sourceContour =
194
- options.from.contour ?? createTableContour(options.from.table);
195
- const targetContour =
196
- options.to.contour ?? createTableContour(options.to.table);
199
+ const sourceEntity =
200
+ options.from.entity ?? createTableEntity(options.from.table);
201
+ const targetEntity = options.to.entity ?? createTableEntity(options.to.table);
197
202
 
198
203
  return trail(id, {
199
- // oxlint-disable-next-line max-statements -- sync blaze reads more clearly as one try/catch with schema validation, transform, and accessor call inline
200
- blaze: async (input, ctx) => {
204
+ description:
205
+ options.description ??
206
+ `Sync one "${options.from.table.name}" entity into "${options.to.table.name}".`,
207
+ entities: [sourceEntity, targetEntity],
208
+ examples: deriveExamples(
209
+ options.from.table,
210
+ options.to.table,
211
+ options.transform
212
+ ) as
213
+ | readonly TrailExample<
214
+ IdentityInputOf<TSourceTable>,
215
+ EntityOf<TTargetTable>
216
+ >[]
217
+ | undefined,
218
+ // oxlint-disable-next-line max-statements -- sync implementation reads more clearly as one try/catch with schema validation, transform, and accessor call inline
219
+ implementation: async (input, ctx) => {
201
220
  try {
202
221
  const identifier = input[
203
222
  options.from.table.identity as keyof typeof input
204
223
  ] as StoreIdentifierOf<TSourceTable>;
205
- const sourceEntity = await resolveSourceAccessor(options.from, ctx).get(
224
+ const sourceRecord = await resolveSourceAccessor(options.from, ctx).get(
206
225
  identifier
207
226
  );
208
227
 
209
- if (sourceEntity === null) {
228
+ if (sourceRecord === null) {
210
229
  return Result.err(sourceMissingError(options.from.table, identifier));
211
230
  }
212
231
 
@@ -218,7 +237,7 @@ export const sync = <
218
237
  // a mismatched payload.
219
238
  const next =
220
239
  options.transform === undefined
221
- ? options.to.table.fixtureSchema.safeParse(sourceEntity)
240
+ ? options.to.table.fixtureSchema.safeParse(sourceRecord)
222
241
  : undefined;
223
242
 
224
243
  if (next !== undefined && !next.success) {
@@ -232,7 +251,7 @@ export const sync = <
232
251
  const payload =
233
252
  options.transform === undefined
234
253
  ? (next?.data as unknown as UpsertOf<TTargetTable>)
235
- : await options.transform(sourceEntity, ctx);
254
+ : await options.transform(sourceRecord, ctx);
236
255
 
237
256
  const synced = await resolveTargetAccessor(options.to, ctx).upsert(
238
257
  payload
@@ -242,20 +261,6 @@ export const sync = <
242
261
  return Result.err(mapStoreTrailError(id, error));
243
262
  }
244
263
  },
245
- contours: [sourceContour, targetContour],
246
- description:
247
- options.description ??
248
- `Sync one "${options.from.table.name}" entity into "${options.to.table.name}".`,
249
- examples: deriveExamples(
250
- options.from.table,
251
- options.to.table,
252
- options.transform
253
- ) as
254
- | readonly TrailExample<
255
- IdentityInputOf<TSourceTable>,
256
- EntityOf<TTargetTable>
257
- >[]
258
- | undefined,
259
264
  input: identityInputSchema(options.from.table),
260
265
  intent: 'write',
261
266
  on: options.on,
@@ -1,26 +1,31 @@
1
- import { InternalError, contour, isTrailsError } from '@ontrails/core';
2
- import type { Contour } from '@ontrails/core';
1
+ import {
2
+ InternalError,
3
+ ValidationError,
4
+ entity,
5
+ isTrailsError,
6
+ } from '@ontrails/core';
7
+ import type { Entity } from '@ontrails/core';
3
8
  import type { z } from 'zod';
4
9
 
5
10
  import type { AnyStoreTable } from '../types.js';
6
11
 
7
12
  /**
8
- * The contour type produced by {@link createTableContour} for a given store
13
+ * The entity type produced by {@link createTableEntity} for a given store
9
14
  * table. Threads the table's name, schema shape, and identity through the
10
- * contour generics so downstream `deriveTrail()` calls project concrete
15
+ * entity generics so downstream `deriveTrail()` calls project concrete
11
16
  * input/output types instead of widening back to
12
- * `Contour<string, z.ZodRawShape, string>` (the `AnyContour` alias).
17
+ * `Entity<string, z.ZodRawShape, string>` (the `AnyEntity` alias).
13
18
  */
14
- export type TableContour<TTable extends AnyStoreTable> = Contour<
19
+ export type TableEntity<TTable extends AnyStoreTable> = Entity<
15
20
  TTable['name'],
16
21
  TTable['schema']['shape'],
17
22
  Extract<TTable['identity'], keyof TTable['schema']['shape'] & string>
18
23
  >;
19
24
 
20
25
  /**
21
- * Build the shape used when deriving a contour view of a store table.
26
+ * Build the shape used when deriving an entity view of a store table.
22
27
  *
23
- * Contour validates every example against the shape passed in, so the shape
28
+ * Entity validates every example against the shape passed in, so the shape
24
29
  * must match how fixtures are actually shaped. Store fixtures may omit
25
30
  * framework-generated fields (`createdAt`, `version`, ...) because the
26
31
  * adapter populates them, so we mirror `fixtureSchema`'s treatment of
@@ -30,7 +35,7 @@ export type TableContour<TTable extends AnyStoreTable> = Contour<
30
35
  * `table.schema.shape` directly and crashed when a fixture omitted
31
36
  * `createdAt` or another generated field.
32
37
  */
33
- export const buildContourShape = (
38
+ export const buildEntityShape = (
34
39
  table: AnyStoreTable
35
40
  ): Record<string, z.ZodType> => {
36
41
  const shape = table.schema.shape as unknown as Record<string, z.ZodType>;
@@ -52,25 +57,41 @@ export const buildContourShape = (
52
57
  };
53
58
 
54
59
  /**
55
- * Derive a contour view of a store table.
60
+ * Derive an entity view of a store table.
56
61
  *
57
62
  * Both `sync` and `reconcile` use this helper so they pick up the
58
63
  * fixture-shape treatment (generated fields optional).
59
64
  *
60
65
  * @remarks
61
- * Intentionally not cached. `contour()` brands the identity schema via
66
+ * Intentionally not cached. `entity()` brands the identity schema via
62
67
  * `Object.defineProperty(..., { writable: false })`, and re-invoking on a
63
68
  * schema that's already been branded throws TypeError. Factory call sites
64
- * already build the contour once per trail instance, so rebuilding on a
69
+ * already build the entity once per trail instance, so rebuilding on a
65
70
  * warm call is cheap and side-effect-free.
66
71
  */
67
- export const createTableContour = <TTable extends AnyStoreTable>(
72
+ export const createTableEntity = <TTable extends AnyStoreTable>(
68
73
  table: TTable
69
- ): TableContour<TTable> =>
70
- contour(table.name, buildContourShape(table), {
74
+ ): TableEntity<TTable> =>
75
+ entity(table.name, buildEntityShape(table), {
71
76
  examples: table.fixtures as readonly Record<string, unknown>[],
72
77
  identity: table.identity,
73
- }) as TableContour<TTable>;
78
+ }) as TableEntity<TTable>;
79
+
80
+ /** Reject the retired store-factory option instead of silently ignoring it. */
81
+ export const assertCurrentEntityOption = (
82
+ value: unknown,
83
+ owner: string
84
+ ): void => {
85
+ if (
86
+ typeof value === 'object' &&
87
+ value !== null &&
88
+ Object.hasOwn(value, 'contour')
89
+ ) {
90
+ throw new ValidationError(
91
+ `${owner} uses retired "contour"; use "entity" instead`
92
+ );
93
+ }
94
+ };
74
95
 
75
96
  /**
76
97
  * Coerce an unknown thrown value into an Error instance, preserving the
package/src/types.ts CHANGED
@@ -70,7 +70,7 @@ type ObjectOutputOf<TShape extends z.ZodRawShape> = z.core.$InferObjectOutput<
70
70
  * create/upsert inputs they meet at store/core trail boundaries.
71
71
  *
72
72
  * This is not the same type-level path core's `deriveTrail()` uses — core
73
- * still goes through `z.input<TContour>` / `z.output<TContour>` — but at
73
+ * still goes through `z.input<TEntity>` / `z.output<TEntity>` — but at
74
74
  * concrete instantiations it collapses to the same structural fixture shape
75
75
  * while preserving generic equality across the store/core seam.
76
76
  */
@@ -103,7 +103,7 @@ export type StoreFixtureInput<
103
103
  *
104
104
  * Mirror of {@link StoreFixtureInput} on the output side — computed through
105
105
  * `$InferObjectOutput<TShape, Record<never, never>>` so row types stay
106
- * structurally aligned with the contour output shapes they compose against.
106
+ * structurally aligned with the entity output shapes they compose against.
107
107
  *
108
108
  * As with {@link StoreFixtureInput}, this is a shape-based equivalent rather
109
109
  * than the identical `z.output<TSchema>` inference path core uses directly.
@@ -433,7 +433,7 @@ export type GeneratedKeysOf<TTable extends AnyStoreTable> = Extract<
433
433
  * this collapses to the same structural shape as
434
434
  * `Omit<z.input<TTable['schema']>, GeneratedKeysOf<TTable>>`, but the
435
435
  * shape-based form lets TypeScript prove structural equality with
436
- * `CreateInputOf<Contour, ...>` at trail boundaries without widening
436
+ * `CreateInputOf<Entity, ...>` at trail boundaries without widening
437
437
  * generic call sites to `Record<string, unknown>`.
438
438
  *
439
439
  * Defaulted fields remain optional because `$InferObjectInput` honors
@@ -532,7 +532,7 @@ export interface StoreAccessor<
532
532
  // Compile-time assertion: StoreAccessor satisfies the core accessor protocol.
533
533
  //
534
534
  // `@ontrails/core/store` declares a structural protocol that `deriveTrail()`
535
- // uses to synthesize default blazes without importing `@ontrails/store`. We
535
+ // uses to synthesize default implementations without importing `@ontrails/store`. We
536
536
  // pin the relationship here rather than in core so that any drift between
537
537
  // the two shapes fails the store build immediately. If this check fails, the
538
538
  // protocol in core has diverged from the store accessor contract — fix the