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

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,24 @@
1
1
  # @ontrails/store
2
2
 
3
+ ## 1.0.0-beta.39
4
+
5
+ ### Patch Changes
6
+
7
+ - [`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()`.
8
+ - [`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.
9
+
10
+ ## 1.0.0-beta.38
11
+
12
+ ## 1.0.0-beta.37
13
+
14
+ ## 1.0.0-beta.36
15
+
16
+ ## 1.0.0-beta.35
17
+
18
+ ## 1.0.0-beta.34
19
+
20
+ ## 1.0.0-beta.33
21
+
3
22
  ## 1.0.0-beta.32
4
23
 
5
24
  ### Patch Changes
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.39",
4
4
  "files": [
5
5
  "src/**/*.ts",
6
6
  "!src/**/__tests__/**",
@@ -26,7 +26,7 @@
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.39"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "zod": "^4.3.5"
@@ -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,
@@ -137,7 +142,15 @@ 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 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
150
+ * same-named rebuilds as duplicates.
151
+ */
152
+ readonly contour: TableContour<TTable>;
153
+ };
141
154
 
142
155
  export interface CrudBlazeOverrides<TTable extends AnyStoreTable> {
143
156
  readonly create?: Implementation<InsertOf<TTable>, EntityOf<TTable>>;
@@ -152,6 +165,24 @@ export interface CrudBlazeOverrides<TTable extends AnyStoreTable> {
152
165
 
153
166
  export interface CrudOptions<TTable extends AnyStoreTable> {
154
167
  readonly blaze?: CrudBlazeOverrides<TTable>;
168
+ /**
169
+ * Existing table contour 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 contour registration.
173
+ */
174
+ readonly contour?: TableContour<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
188
  interface InternalCrudBlazeOverrides<TTable extends AnyStoreTable> {
@@ -179,6 +210,9 @@ interface InternalCrudBlazeOverrides<TTable extends AnyStoreTable> {
179
210
 
180
211
  interface InternalCrudOptions<TTable extends AnyStoreTable> {
181
212
  readonly blaze?: InternalCrudBlazeOverrides<TTable>;
213
+ readonly contour?: TableContour<TTable>;
214
+ readonly permit?: PermitRequirement;
215
+ readonly permits?: Partial<Record<CrudOperation, PermitRequirement>>;
182
216
  }
183
217
 
184
218
  const normalizeExampleForOutput = <TInput, TOutput>(
@@ -225,6 +259,7 @@ const finalizeTrail = <TInput, TOutput>(
225
259
  readonly blaze?: 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({
@@ -237,6 +272,7 @@ const finalizeTrail = <TInput, TOutput>(
237
272
  output: options.output,
238
273
  }),
239
274
  ...(options.pattern === undefined ? {} : { pattern: options.pattern }),
275
+ ...(options.permit === undefined ? {} : { permit: options.permit }),
240
276
  }) as Trail<TInput, TOutput>;
241
277
 
242
278
  const deriveCrudBaseTrails = <
@@ -244,9 +280,9 @@ const deriveCrudBaseTrails = <
244
280
  TConnection extends CrudConnection<TTable>,
245
281
  >(
246
282
  table: TTable,
247
- resource: Resource<TConnection>
283
+ resource: Resource<TConnection>,
284
+ entityContour: TableContour<TTable>
248
285
  ): InternalCrudBaseTrails<TTable> => {
249
- const entityContour = createTableContour(table);
250
286
  // Narrow the store's `readonly string[]` to the contour's typed field-key
251
287
  // array so `deriveTrail`'s `TGenerated` generic picks up the precise
252
288
  // key-of shape that `CreateInputOf<Contour, TGenerated>` expects. The
@@ -282,38 +318,51 @@ const deriveCrudBaseTrails = <
282
318
 
283
319
  const buildCrudTrails = <TTable extends AnyStoreTable>(
284
320
  baseTrails: InternalCrudBaseTrails<TTable>,
285
- overrides: InternalCrudBlazeOverrides<TTable>,
321
+ options: InternalCrudOptions<TTable>,
286
322
  entityOutput: z.ZodType<DerivedOutput<TTable, 'create'>>,
287
323
  listOutput: z.ZodType<DerivedOutput<TTable, 'list'>>
288
- ): InternalCrudTrails<TTable> =>
289
- Object.freeze([
324
+ ): InternalCrudTrails<TTable> => {
325
+ const overrides = options.blaze ?? {};
326
+ const permitFor = (operation: CrudOperation): PermitRequirement | undefined =>
327
+ options.permits?.[operation] ?? options.permit;
328
+
329
+ return Object.freeze([
290
330
  finalizeTrail(baseTrails.createBase, {
291
331
  ...(overrides.create === undefined ? {} : { blaze: overrides.create }),
292
332
  output: entityOutput,
293
333
  pattern: 'crud',
334
+ permit: permitFor('create'),
294
335
  }),
295
336
  finalizeTrail(baseTrails.readBase, {
296
337
  ...(overrides.read === undefined ? {} : { blaze: overrides.read }),
297
338
  output: entityOutput,
298
339
  pattern: 'crud',
340
+ permit: permitFor('read'),
299
341
  }),
300
342
  finalizeTrail(baseTrails.updateBase, {
301
343
  ...(overrides.update === undefined ? {} : { blaze: overrides.update }),
302
344
  output: entityOutput,
303
345
  pattern: 'crud',
346
+ permit: permitFor('update'),
304
347
  }),
305
348
  overrides.delete === undefined
306
- ? finalizeTrail(baseTrails.deleteBase, { pattern: 'crud' })
349
+ ? finalizeTrail(baseTrails.deleteBase, {
350
+ pattern: 'crud',
351
+ permit: permitFor('delete'),
352
+ })
307
353
  : finalizeTrail(baseTrails.deleteBase, {
308
354
  blaze: overrides.delete,
309
355
  pattern: 'crud',
356
+ permit: permitFor('delete'),
310
357
  }),
311
358
  finalizeTrail(baseTrails.listBase, {
312
359
  ...(overrides.list === undefined ? {} : { blaze: overrides.list }),
313
360
  output: listOutput,
314
361
  pattern: 'crud',
362
+ permit: permitFor('list'),
315
363
  }),
316
364
  ]) as InternalCrudTrails<TTable>;
365
+ };
317
366
 
318
367
  /**
319
368
  * Produce the standard CRUD trail tuple for one normalized store table.
@@ -340,8 +389,8 @@ export function crud<
340
389
  resource: Resource<TConnection>,
341
390
  options: InternalCrudOptions<TTable> = {}
342
391
  ) {
343
- const overrides = options.blaze ?? {};
344
- const baseTrails = deriveCrudBaseTrails(table, resource);
392
+ const entityContour = options.contour ?? createTableContour(table);
393
+ const baseTrails = deriveCrudBaseTrails(table, resource, entityContour);
345
394
  // Narrow `table.schema` (typed `StoreObjectSchema`, which is
346
395
  // `z.ZodObject<Record<string, z.ZodType>>`) to a ZodObject keyed by the
347
396
  // concrete shape so its `z.output` unifies with the contour-derived
@@ -352,5 +401,12 @@ export function crud<
352
401
  const listOutput: z.ZodType<DerivedOutput<TTable, 'list'>> =
353
402
  entitySchema.array();
354
403
 
355
- return buildCrudTrails(baseTrails, overrides, entityOutput, listOutput);
404
+ const trails = buildCrudTrails(baseTrails, options, entityOutput, listOutput);
405
+ // Expose the registered contour so other factories over the same table
406
+ // (reconcile, sync) can share the instance instead of rebuilding it.
407
+ return Object.freeze(
408
+ Object.assign([...trails], { contour: entityContour })
409
+ ) as unknown as InternalCrudTrails<TTable> & {
410
+ readonly contour: TableContour<TTable>;
411
+ };
356
412
  }
@@ -13,3 +13,4 @@ export type {
13
13
  } from './reconcile.js';
14
14
  export { sync } from './sync.js';
15
15
  export type { SyncEndpoint, SyncOptions, SyncTransform } from './sync.js';
16
+ export type { TableContour } 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,
@@ -18,6 +19,7 @@ import type {
18
19
  } from '../types.js';
19
20
  import { versionFieldName } from '../store.js';
20
21
  import { createTableContour, mapStoreTrailError } from './utils.js';
22
+ import type { TableContour } from './utils.js';
21
23
 
22
24
  type ReconcileConnection<TTable extends AnyStoreTable> = Readonly<
23
25
  Record<TTable['name'], StoreAccessor<TTable>>
@@ -39,9 +41,19 @@ export interface ReconcileOptions<
39
41
  TTable extends AnyStoreTable,
40
42
  TConnection extends ReconcileConnection<TTable>,
41
43
  > {
44
+ /**
45
+ * Existing table contour to register on the reconcile trail. Pass the
46
+ * contour a `crud()` call over the same table exposes (its `contour`
47
+ * property) so `topo()` sees one shared instance instead of rejecting
48
+ * two same-named rebuilds as duplicates. When omitted, the factory
49
+ * builds its own.
50
+ */
51
+ readonly contour?: TableContour<TTable>;
42
52
  readonly description?: string;
43
53
  readonly id?: string;
44
54
  readonly on?: readonly (AnySignal | string)[];
55
+ /** Permit requirement declared on the reconcile trail. */
56
+ readonly permit?: PermitRequirement;
45
57
  readonly resource: Resource<TConnection>;
46
58
  readonly strategy?: ReconcileStrategy<TTable>;
47
59
  readonly table: TTable;
@@ -260,7 +272,7 @@ export const reconcile = <
260
272
  }
261
273
 
262
274
  const id = options.id ?? `${options.table.name}.reconcile`;
263
- const entityContour = createTableContour(options.table);
275
+ const entityContour = options.contour ?? createTableContour(options.table);
264
276
  const strategy = options.strategy ?? 'last-write-wins';
265
277
 
266
278
  return trail(id, {
@@ -276,6 +288,7 @@ export const reconcile = <
276
288
  on: options.on,
277
289
  output: options.table.schema as unknown as z.ZodType<EntityOf<TTable>>,
278
290
  pattern: 'reconcile',
291
+ ...(options.permit === undefined ? {} : { permit: options.permit }),
279
292
  resources: [options.resource],
280
293
  });
281
294
  };
@@ -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,
@@ -17,6 +18,7 @@ import type {
17
18
  UpsertOf,
18
19
  } from '../types.js';
19
20
  import { createTableContour, mapStoreTrailError } from './utils.js';
21
+ import type { TableContour } from './utils.js';
20
22
 
21
23
  type IdentityInputOf<TTable extends AnyStoreTable> = Readonly<
22
24
  Record<Extract<TTable['identity'], string>, StoreIdentifierOf<TTable>>
@@ -34,6 +36,14 @@ export interface SyncEndpoint<
34
36
  TTable extends AnyStoreTable,
35
37
  TConnection extends SourceConnection<TTable> | TargetConnection<TTable>,
36
38
  > {
39
+ /**
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
43
+ * instance instead of rejecting two same-named rebuilds as
44
+ * duplicates. When omitted, the factory builds one from the table.
45
+ */
46
+ readonly contour?: TableContour<TTable>;
37
47
  readonly resource: Resource<TConnection>;
38
48
  readonly table: TTable;
39
49
  }
@@ -56,6 +66,11 @@ export interface SyncOptions<
56
66
  readonly from: SyncEndpoint<TSourceTable, TSourceConnection>;
57
67
  readonly id?: string;
58
68
  readonly on?: readonly (AnySignal | string)[];
69
+ /**
70
+ * Permit requirement declared on the produced trail. Factory trails
71
+ * carry authored defaults like any hand-written trail.
72
+ */
73
+ readonly permit?: PermitRequirement;
59
74
  readonly to: SyncEndpoint<TTargetTable, TTargetConnection>;
60
75
  readonly transform?: SyncTransform<TSourceTable, TTargetTable>;
61
76
  }
@@ -175,8 +190,10 @@ export const sync = <
175
190
  >
176
191
  ): Trail<IdentityInputOf<TSourceTable>, EntityOf<TTargetTable>> => {
177
192
  const id = options.id ?? `${options.to.table.name}.sync`;
178
- const sourceContour = createTableContour(options.from.table);
179
- const targetContour = createTableContour(options.to.table);
193
+ const sourceContour =
194
+ options.from.contour ?? createTableContour(options.from.table);
195
+ const targetContour =
196
+ options.to.contour ?? createTableContour(options.to.table);
180
197
 
181
198
  return trail(id, {
182
199
  // oxlint-disable-next-line max-statements -- sync blaze reads more clearly as one try/catch with schema validation, transform, and accessor call inline
@@ -246,6 +263,7 @@ export const sync = <
246
263
  EntityOf<TTargetTable>
247
264
  >,
248
265
  pattern: 'sync',
266
+ ...(options.permit === undefined ? {} : { permit: options.permit }),
249
267
  resources: [options.from.resource, options.to.resource],
250
268
  });
251
269
  };