@ontrails/store 1.0.0-beta.32 → 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,55 @@
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
+
34
+ ## 1.0.0-beta.39
35
+
36
+ ### Patch Changes
37
+
38
+ - [`f7ec225`](https://github.com/outfitter-dev/trails/commit/f7ec225c01482f8fb55afd174add3d961a63171b): `sync()` gains the factory-contract options `crud()` and `reconcile()` received in TRL-1195: a `permit` option declared on the produced trail, and per-endpoint `contour` options on `SyncEndpoint` so a `crud()` bundle's table contour can be shared instead of colliding as a duplicate registration at `topo()`.
39
+ - [`5a38c73`](https://github.com/outfitter-dev/trails/commit/5a38c73092f81612769be4b44944d828c3436e07): Complete the store factory trail contracts (TRL-1195, absorbing TRL-1177 and TRL-1178). `crud()` gains `permit` (applied to every produced trail) and `permits` (per-operation overrides, so destroy trails satisfy permit governance) plus a `contour` option, and the returned tuple now exposes the table contour it registered as a `contour` property. `reconcile()` gains `permit` and accepts a shared `contour` instance, so crud + reconcile on one table register cleanly in a single `topo()` instead of colliding on a duplicate contour name. `TableContour` is exported from `@ontrails/store/trails`. Consuming apps no longer need to post-process factory trails to attach permits or strip contours.
40
+
41
+ ## 1.0.0-beta.38
42
+
43
+ ## 1.0.0-beta.37
44
+
45
+ ## 1.0.0-beta.36
46
+
47
+ ## 1.0.0-beta.35
48
+
49
+ ## 1.0.0-beta.34
50
+
51
+ ## 1.0.0-beta.33
52
+
3
53
  ## 1.0.0-beta.32
4
54
 
5
55
  ### 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.32",
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.32"
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
 
@@ -1,4 +1,9 @@
1
- import type { Implementation, Resource, Trail } from '@ontrails/core';
1
+ import type {
2
+ Implementation,
3
+ PermitRequirement,
4
+ Resource,
5
+ Trail,
6
+ } from '@ontrails/core';
2
7
  import { deriveTrail } from '@ontrails/core/trails';
3
8
  import type {
4
9
  DeriveTrailInput,
@@ -16,8 +21,8 @@ import type {
16
21
  UpdateOf,
17
22
  } from '../types.js';
18
23
  import type { CrudOperation } from '../crud-doctrine.js';
19
- import { createTableContour } from './utils.js';
20
- import type { TableContour } from './utils.js';
24
+ import { assertCurrentEntityOption, createTableEntity } from './utils.js';
25
+ import type { TableEntity } from './utils.js';
21
26
 
22
27
  type IdentityInputOf<TTable extends AnyStoreTable> = Readonly<
23
28
  Record<Extract<TTable['identity'], string>, StoreIdentifierOf<TTable>>
@@ -27,27 +32,27 @@ type CrudConnection<TTable extends AnyStoreTable> = Readonly<
27
32
  Record<TTable['name'], StoreAccessor<TTable>>
28
33
  >;
29
34
 
30
- type TableContourFieldKey<TTable extends AnyStoreTable> = Extract<
31
- keyof z.output<TableContour<TTable>>,
35
+ type TableEntityFieldKey<TTable extends AnyStoreTable> = Extract<
36
+ keyof z.output<TableEntity<TTable>>,
32
37
  string
33
38
  >;
34
39
 
35
40
  type GeneratedFieldsOf<TTable extends AnyStoreTable> =
36
- TTable['generated'] extends readonly TableContourFieldKey<TTable>[]
41
+ TTable['generated'] extends readonly TableEntityFieldKey<TTable>[]
37
42
  ? TTable['generated']
38
43
  : readonly [];
39
44
 
40
45
  /**
41
46
  * Input type `deriveTrail` projects for a given CRUD operation against a
42
- * store table. Uses `TableContour<TTable>` so the projected input
43
- * 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
44
49
  * `@ontrails/core`'s `deriveTrail`.
45
50
  */
46
51
  type DerivedInput<
47
52
  TTable extends AnyStoreTable,
48
53
  TOperation extends CrudOperation,
49
54
  > = DeriveTrailInput<
50
- TableContour<TTable>,
55
+ TableEntity<TTable>,
51
56
  TOperation,
52
57
  GeneratedFieldsOf<TTable>
53
58
  >;
@@ -59,7 +64,7 @@ type DerivedInput<
59
64
  type DerivedOutput<
60
65
  TTable extends AnyStoreTable,
61
66
  TOperation extends CrudOperation,
62
- > = DeriveTrailOutput<TableContour<TTable>, TOperation>;
67
+ > = DeriveTrailOutput<TableEntity<TTable>, TOperation>;
63
68
 
64
69
  type InternalCreateTrailOf<TTable extends AnyStoreTable> = Trail<
65
70
  DerivedInput<TTable, 'create'>,
@@ -137,9 +142,17 @@ export type CrudTrails<TTable extends AnyStoreTable> = readonly [
137
142
  update: UpdateTrailOf<TTable>,
138
143
  remove: DeleteTrailOf<TTable>,
139
144
  list: ListTrailOf<TTable>,
140
- ];
145
+ ] & {
146
+ /**
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
+ * same-named rebuilds as duplicates.
151
+ */
152
+ readonly entity: TableEntity<TTable>;
153
+ };
141
154
 
142
- export interface CrudBlazeOverrides<TTable extends AnyStoreTable> {
155
+ export interface CrudImplementationOverrides<TTable extends AnyStoreTable> {
143
156
  readonly create?: Implementation<InsertOf<TTable>, EntityOf<TTable>>;
144
157
  readonly read?: Implementation<IdentityInputOf<TTable>, EntityOf<TTable>>;
145
158
  readonly update?: Implementation<
@@ -151,10 +164,28 @@ export interface CrudBlazeOverrides<TTable extends AnyStoreTable> {
151
164
  }
152
165
 
153
166
  export interface CrudOptions<TTable extends AnyStoreTable> {
154
- readonly blaze?: CrudBlazeOverrides<TTable>;
167
+ readonly implementation?: CrudImplementationOverrides<TTable>;
168
+ /**
169
+ * Existing table entity to register on the produced trails. When
170
+ * omitted, the factory builds one from the table. Pass a shared
171
+ * instance when another factory (e.g. `reconcile()`) covers the same
172
+ * table so `topo()` sees a single entity registration.
173
+ */
174
+ readonly entity?: TableEntity<TTable>;
175
+ /**
176
+ * Permit requirement declared on every produced trail. Factory trails
177
+ * carry authored defaults like any hand-written trail; per-operation
178
+ * entries in `permits` override this baseline.
179
+ */
180
+ readonly permit?: PermitRequirement;
181
+ /**
182
+ * Per-operation permit overrides. At minimum, destroy-intent trails
183
+ * (`delete`) need a declaration to satisfy permit governance.
184
+ */
185
+ readonly permits?: Partial<Record<CrudOperation, PermitRequirement>>;
155
186
  }
156
187
 
157
- interface InternalCrudBlazeOverrides<TTable extends AnyStoreTable> {
188
+ interface InternalCrudImplementationOverrides<TTable extends AnyStoreTable> {
158
189
  readonly create?: Implementation<
159
190
  DerivedInput<TTable, 'create'>,
160
191
  DerivedOutput<TTable, 'create'>
@@ -178,7 +209,10 @@ interface InternalCrudBlazeOverrides<TTable extends AnyStoreTable> {
178
209
  }
179
210
 
180
211
  interface InternalCrudOptions<TTable extends AnyStoreTable> {
181
- readonly blaze?: InternalCrudBlazeOverrides<TTable>;
212
+ readonly implementation?: InternalCrudImplementationOverrides<TTable>;
213
+ readonly entity?: TableEntity<TTable>;
214
+ readonly permit?: PermitRequirement;
215
+ readonly permits?: Partial<Record<CrudOperation, PermitRequirement>>;
182
216
  }
183
217
 
184
218
  const normalizeExampleForOutput = <TInput, TOutput>(
@@ -222,14 +256,17 @@ const normalizeExamplesForOutput = <TInput, TOutput>(
222
256
  const finalizeTrail = <TInput, TOutput>(
223
257
  base: Trail<TInput, TOutput>,
224
258
  options: {
225
- readonly blaze?: Implementation<TInput, TOutput> | undefined;
259
+ readonly implementation?: Implementation<TInput, TOutput> | undefined;
226
260
  readonly output?: z.ZodType<TOutput> | undefined;
227
261
  readonly pattern?: string | undefined;
262
+ readonly permit?: PermitRequirement | undefined;
228
263
  } = {}
229
264
  ): Trail<TInput, TOutput> =>
230
265
  Object.freeze({
231
266
  ...base,
232
- ...(options.blaze === undefined ? {} : { blaze: options.blaze }),
267
+ ...(options.implementation === undefined
268
+ ? {}
269
+ : { implementation: options.implementation }),
233
270
  ...(options.output === undefined
234
271
  ? {}
235
272
  : {
@@ -237,6 +274,7 @@ const finalizeTrail = <TInput, TOutput>(
237
274
  output: options.output,
238
275
  }),
239
276
  ...(options.pattern === undefined ? {} : { pattern: options.pattern }),
277
+ ...(options.permit === undefined ? {} : { permit: options.permit }),
240
278
  }) as Trail<TInput, TOutput>;
241
279
 
242
280
  const deriveCrudBaseTrails = <
@@ -244,36 +282,36 @@ const deriveCrudBaseTrails = <
244
282
  TConnection extends CrudConnection<TTable>,
245
283
  >(
246
284
  table: TTable,
247
- resource: Resource<TConnection>
285
+ resource: Resource<TConnection>,
286
+ tableEntity: TableEntity<TTable>
248
287
  ): InternalCrudBaseTrails<TTable> => {
249
- const entityContour = createTableContour(table);
250
- // 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
251
289
  // array so `deriveTrail`'s `TGenerated` generic picks up the precise
252
- // key-of shape that `CreateInputOf<Contour, TGenerated>` expects. The
290
+ // key-of shape that `CreateInputOf<Entity, TGenerated>` expects. The
253
291
  // runtime value is unchanged — the names in `table.generated` are already
254
292
  // keys of `table.schema.shape` by construction in `store()`.
255
293
  const generated = table.generated as GeneratedFieldsOf<TTable>;
256
294
 
257
295
  return {
258
- createBase: deriveTrail(entityContour, 'create', {
296
+ createBase: deriveTrail(tableEntity, 'create', {
259
297
  generated,
260
298
  resource,
261
299
  }),
262
- deleteBase: deriveTrail(entityContour, 'delete', {
300
+ deleteBase: deriveTrail(tableEntity, 'delete', {
263
301
  resource,
264
302
  }),
265
- listBase: deriveTrail(entityContour, 'list', {
303
+ listBase: deriveTrail(tableEntity, 'list', {
266
304
  resource,
267
305
  }),
268
- readBase: deriveTrail(entityContour, 'read', {
306
+ readBase: deriveTrail(tableEntity, 'read', {
269
307
  resource,
270
308
  }),
271
- // The `update` blaze synthesized by `deriveTrail` handles the partial-patch
309
+ // The `update` implementation synthesized by `deriveTrail` handles the partial-patch
272
310
  // concern: when the accessor lacks a native `update`, the fallback path in
273
311
  // `derive-trail.ts` (`updateViaReadAndUpsert`) reads the current entity,
274
312
  // merges the patch, strips the `version` field, then calls `upsert` with
275
313
  // the full merged payload — so no fields are silently lost.
276
- updateBase: deriveTrail(entityContour, 'update', {
314
+ updateBase: deriveTrail(tableEntity, 'update', {
277
315
  generated,
278
316
  resource,
279
317
  }),
@@ -282,46 +320,67 @@ const deriveCrudBaseTrails = <
282
320
 
283
321
  const buildCrudTrails = <TTable extends AnyStoreTable>(
284
322
  baseTrails: InternalCrudBaseTrails<TTable>,
285
- overrides: InternalCrudBlazeOverrides<TTable>,
323
+ options: InternalCrudOptions<TTable>,
286
324
  entityOutput: z.ZodType<DerivedOutput<TTable, 'create'>>,
287
325
  listOutput: z.ZodType<DerivedOutput<TTable, 'list'>>
288
- ): InternalCrudTrails<TTable> =>
289
- Object.freeze([
326
+ ): InternalCrudTrails<TTable> => {
327
+ const overrides = options.implementation ?? {};
328
+ const permitFor = (operation: CrudOperation): PermitRequirement | undefined =>
329
+ options.permits?.[operation] ?? options.permit;
330
+
331
+ return Object.freeze([
290
332
  finalizeTrail(baseTrails.createBase, {
291
- ...(overrides.create === undefined ? {} : { blaze: overrides.create }),
333
+ ...(overrides.create === undefined
334
+ ? {}
335
+ : { implementation: overrides.create }),
292
336
  output: entityOutput,
293
337
  pattern: 'crud',
338
+ permit: permitFor('create'),
294
339
  }),
295
340
  finalizeTrail(baseTrails.readBase, {
296
- ...(overrides.read === undefined ? {} : { blaze: overrides.read }),
341
+ ...(overrides.read === undefined
342
+ ? {}
343
+ : { implementation: overrides.read }),
297
344
  output: entityOutput,
298
345
  pattern: 'crud',
346
+ permit: permitFor('read'),
299
347
  }),
300
348
  finalizeTrail(baseTrails.updateBase, {
301
- ...(overrides.update === undefined ? {} : { blaze: overrides.update }),
349
+ ...(overrides.update === undefined
350
+ ? {}
351
+ : { implementation: overrides.update }),
302
352
  output: entityOutput,
303
353
  pattern: 'crud',
354
+ permit: permitFor('update'),
304
355
  }),
305
356
  overrides.delete === undefined
306
- ? finalizeTrail(baseTrails.deleteBase, { pattern: 'crud' })
357
+ ? finalizeTrail(baseTrails.deleteBase, {
358
+ pattern: 'crud',
359
+ permit: permitFor('delete'),
360
+ })
307
361
  : finalizeTrail(baseTrails.deleteBase, {
308
- blaze: overrides.delete,
362
+ implementation: overrides.delete,
309
363
  pattern: 'crud',
364
+ permit: permitFor('delete'),
310
365
  }),
311
366
  finalizeTrail(baseTrails.listBase, {
312
- ...(overrides.list === undefined ? {} : { blaze: overrides.list }),
367
+ ...(overrides.list === undefined
368
+ ? {}
369
+ : { implementation: overrides.list }),
313
370
  output: listOutput,
314
371
  pattern: 'crud',
372
+ permit: permitFor('list'),
315
373
  }),
316
374
  ]) as InternalCrudTrails<TTable>;
375
+ };
317
376
 
318
377
  /**
319
378
  * Produce the standard CRUD trail tuple for one normalized store table.
320
379
  *
321
- * The factory derives schemas, examples, resources, and contour linkage from
322
- * 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
323
382
  * contract via `deriveTrail()`'s single-resource synthesis path. Per-operation
324
- * blaze overrides stay available for callers that need custom persistence
383
+ * implementation overrides stay available for callers that need custom persistence
325
384
  * behavior and are layered onto the derived trails in a single pass.
326
385
  */
327
386
  export function crud<
@@ -340,11 +399,12 @@ export function crud<
340
399
  resource: Resource<TConnection>,
341
400
  options: InternalCrudOptions<TTable> = {}
342
401
  ) {
343
- const overrides = options.blaze ?? {};
344
- const baseTrails = deriveCrudBaseTrails(table, resource);
402
+ assertCurrentEntityOption(options, 'crud() options');
403
+ const tableEntity = options.entity ?? createTableEntity(table);
404
+ const baseTrails = deriveCrudBaseTrails(table, resource, tableEntity);
345
405
  // Narrow `table.schema` (typed `StoreObjectSchema`, which is
346
406
  // `z.ZodObject<Record<string, z.ZodType>>`) to a ZodObject keyed by the
347
- // concrete shape so its `z.output` unifies with the contour-derived
407
+ // concrete shape so its `z.output` unifies with the entity-derived
348
408
  // output. Structurally `table.schema` already has `shape:
349
409
  // TTable['schema']['shape']` — this only refines the generic parameter.
350
410
  const entitySchema = table.schema as z.ZodObject<TTable['schema']['shape']>;
@@ -352,5 +412,12 @@ export function crud<
352
412
  const listOutput: z.ZodType<DerivedOutput<TTable, 'list'>> =
353
413
  entitySchema.array();
354
414
 
355
- return buildCrudTrails(baseTrails, overrides, entityOutput, listOutput);
415
+ const trails = buildCrudTrails(baseTrails, options, entityOutput, listOutput);
416
+ // Expose the registered entity so other factories over the same table
417
+ // (reconcile, sync) can share the instance instead of rebuilding it.
418
+ return Object.freeze(
419
+ Object.assign([...trails], { entity: tableEntity })
420
+ ) as unknown as InternalCrudTrails<TTable> & {
421
+ readonly entity: TableEntity<TTable>;
422
+ };
356
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,3 +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';
20
+ export type { TableEntity } from './utils.js';
@@ -2,6 +2,7 @@ import { ConflictError, Result, ValidationError, trail } from '@ontrails/core';
2
2
  import type {
3
3
  AnySignal,
4
4
  Detour,
5
+ PermitRequirement,
5
6
  Resource,
6
7
  Trail,
7
8
  TrailContext,
@@ -17,7 +18,12 @@ import type {
17
18
  UpsertOf,
18
19
  } from '../types.js';
19
20
  import { versionFieldName } from '../store.js';
20
- import { createTableContour, mapStoreTrailError } from './utils.js';
21
+ import {
22
+ assertCurrentEntityOption,
23
+ createTableEntity,
24
+ mapStoreTrailError,
25
+ } from './utils.js';
26
+ import type { TableEntity } from './utils.js';
21
27
 
22
28
  type ReconcileConnection<TTable extends AnyStoreTable> = Readonly<
23
29
  Record<TTable['name'], StoreAccessor<TTable>>
@@ -39,9 +45,19 @@ export interface ReconcileOptions<
39
45
  TTable extends AnyStoreTable,
40
46
  TConnection extends ReconcileConnection<TTable>,
41
47
  > {
48
+ /**
49
+ * Existing table entity to register on the reconcile trail. Pass the
50
+ * entity a `crud()` call over the same table exposes (its `entity`
51
+ * property) so `topo()` sees one shared instance instead of rejecting
52
+ * two same-named rebuilds as duplicates. When omitted, the factory
53
+ * builds its own.
54
+ */
55
+ readonly entity?: TableEntity<TTable>;
42
56
  readonly description?: string;
43
57
  readonly id?: string;
44
58
  readonly on?: readonly (AnySignal | string)[];
59
+ /** Permit requirement declared on the reconcile trail. */
60
+ readonly permit?: PermitRequirement;
45
61
  readonly resource: Resource<TConnection>;
46
62
  readonly strategy?: ReconcileStrategy<TTable>;
47
63
  readonly table: TTable;
@@ -177,8 +193,8 @@ const buildReconcileInputSchema = <TTable extends AnyStoreTable>(
177
193
  [versionFieldName]: z.number().int(),
178
194
  }) as unknown as z.ZodType<UpsertOf<TTable>>;
179
195
 
180
- /** The blaze performs only the initial upsert; conflict recovery is handled by the detour. */
181
- const createReconcileBlaze =
196
+ /** The implementation performs only the initial upsert; conflict recovery is handled by the detour. */
197
+ const createReconcileImplementation =
182
198
  <
183
199
  TTable extends AnyStoreTable,
184
200
  TConnection extends ReconcileConnection<TTable>,
@@ -253,6 +269,7 @@ export const reconcile = <
253
269
  >(
254
270
  options: ReconcileOptions<TTable, TConnection>
255
271
  ): Trail<UpsertOf<TTable>, EntityOf<TTable>> => {
272
+ assertCurrentEntityOption(options, 'reconcile() options');
256
273
  if (!options.table.versioned) {
257
274
  throw new ValidationError(
258
275
  `reconcile("${options.table.name}") requires a versioned store table.`
@@ -260,22 +277,23 @@ export const reconcile = <
260
277
  }
261
278
 
262
279
  const id = options.id ?? `${options.table.name}.reconcile`;
263
- const entityContour = createTableContour(options.table);
280
+ const tableEntity = options.entity ?? createTableEntity(options.table);
264
281
  const strategy = options.strategy ?? 'last-write-wins';
265
282
 
266
283
  return trail(id, {
267
- blaze: createReconcileBlaze(options, id),
268
- contours: [entityContour],
269
284
  description:
270
285
  options.description ??
271
286
  `Reconcile version conflicts for "${options.table.name}" entities.`,
272
287
  detours: [createReconcileDetour(options, id, strategy)],
288
+ entities: [tableEntity],
273
289
  examples: deriveExamples(options.table),
290
+ implementation: createReconcileImplementation(options, id),
274
291
  input: buildReconcileInputSchema(options.table),
275
292
  intent: 'write',
276
293
  on: options.on,
277
294
  output: options.table.schema as unknown as z.ZodType<EntityOf<TTable>>,
278
295
  pattern: 'reconcile',
296
+ ...(options.permit === undefined ? {} : { permit: options.permit }),
279
297
  resources: [options.resource],
280
298
  });
281
299
  };
@@ -1,6 +1,7 @@
1
1
  import { InternalError, NotFoundError, Result, trail } from '@ontrails/core';
2
2
  import type {
3
3
  AnySignal,
4
+ PermitRequirement,
4
5
  Resource,
5
6
  Trail,
6
7
  TrailContext,
@@ -16,7 +17,12 @@ import type {
16
17
  StoreIdentifierOf,
17
18
  UpsertOf,
18
19
  } from '../types.js';
19
- import { createTableContour, mapStoreTrailError } from './utils.js';
20
+ import {
21
+ assertCurrentEntityOption,
22
+ createTableEntity,
23
+ mapStoreTrailError,
24
+ } from './utils.js';
25
+ import type { TableEntity } from './utils.js';
20
26
 
21
27
  type IdentityInputOf<TTable extends AnyStoreTable> = Readonly<
22
28
  Record<Extract<TTable['identity'], string>, StoreIdentifierOf<TTable>>
@@ -34,6 +40,14 @@ export interface SyncEndpoint<
34
40
  TTable extends AnyStoreTable,
35
41
  TConnection extends SourceConnection<TTable> | TargetConnection<TTable>,
36
42
  > {
43
+ /**
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
47
+ * instance instead of rejecting two same-named rebuilds as
48
+ * duplicates. When omitted, the factory builds one from the table.
49
+ */
50
+ readonly entity?: TableEntity<TTable>;
37
51
  readonly resource: Resource<TConnection>;
38
52
  readonly table: TTable;
39
53
  }
@@ -56,6 +70,11 @@ export interface SyncOptions<
56
70
  readonly from: SyncEndpoint<TSourceTable, TSourceConnection>;
57
71
  readonly id?: string;
58
72
  readonly on?: readonly (AnySignal | string)[];
73
+ /**
74
+ * Permit requirement declared on the produced trail. Factory trails
75
+ * carry authored defaults like any hand-written trail.
76
+ */
77
+ readonly permit?: PermitRequirement;
59
78
  readonly to: SyncEndpoint<TTargetTable, TTargetConnection>;
60
79
  readonly transform?: SyncTransform<TSourceTable, TTargetTable>;
61
80
  }
@@ -174,22 +193,39 @@ export const sync = <
174
193
  TTargetConnection
175
194
  >
176
195
  ): Trail<IdentityInputOf<TSourceTable>, EntityOf<TTargetTable>> => {
196
+ assertCurrentEntityOption(options.from, 'sync() from options');
197
+ assertCurrentEntityOption(options.to, 'sync() to options');
177
198
  const id = options.id ?? `${options.to.table.name}.sync`;
178
- const sourceContour = createTableContour(options.from.table);
179
- const targetContour = 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);
180
202
 
181
203
  return trail(id, {
182
- // oxlint-disable-next-line max-statements -- sync blaze reads more clearly as one try/catch with schema validation, transform, and accessor call inline
183
- 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) => {
184
220
  try {
185
221
  const identifier = input[
186
222
  options.from.table.identity as keyof typeof input
187
223
  ] as StoreIdentifierOf<TSourceTable>;
188
- const sourceEntity = await resolveSourceAccessor(options.from, ctx).get(
224
+ const sourceRecord = await resolveSourceAccessor(options.from, ctx).get(
189
225
  identifier
190
226
  );
191
227
 
192
- if (sourceEntity === null) {
228
+ if (sourceRecord === null) {
193
229
  return Result.err(sourceMissingError(options.from.table, identifier));
194
230
  }
195
231
 
@@ -201,7 +237,7 @@ export const sync = <
201
237
  // a mismatched payload.
202
238
  const next =
203
239
  options.transform === undefined
204
- ? options.to.table.fixtureSchema.safeParse(sourceEntity)
240
+ ? options.to.table.fixtureSchema.safeParse(sourceRecord)
205
241
  : undefined;
206
242
 
207
243
  if (next !== undefined && !next.success) {
@@ -215,7 +251,7 @@ export const sync = <
215
251
  const payload =
216
252
  options.transform === undefined
217
253
  ? (next?.data as unknown as UpsertOf<TTargetTable>)
218
- : await options.transform(sourceEntity, ctx);
254
+ : await options.transform(sourceRecord, ctx);
219
255
 
220
256
  const synced = await resolveTargetAccessor(options.to, ctx).upsert(
221
257
  payload
@@ -225,20 +261,6 @@ export const sync = <
225
261
  return Result.err(mapStoreTrailError(id, error));
226
262
  }
227
263
  },
228
- contours: [sourceContour, targetContour],
229
- description:
230
- options.description ??
231
- `Sync one "${options.from.table.name}" entity into "${options.to.table.name}".`,
232
- examples: deriveExamples(
233
- options.from.table,
234
- options.to.table,
235
- options.transform
236
- ) as
237
- | readonly TrailExample<
238
- IdentityInputOf<TSourceTable>,
239
- EntityOf<TTargetTable>
240
- >[]
241
- | undefined,
242
264
  input: identityInputSchema(options.from.table),
243
265
  intent: 'write',
244
266
  on: options.on,
@@ -246,6 +268,7 @@ export const sync = <
246
268
  EntityOf<TTargetTable>
247
269
  >,
248
270
  pattern: 'sync',
271
+ ...(options.permit === undefined ? {} : { permit: options.permit }),
249
272
  resources: [options.from.resource, options.to.resource],
250
273
  });
251
274
  };
@@ -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