@ontrails/core 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 +58 -0
- package/README.md +10 -10
- package/package.json +1 -1
- package/src/activation-provenance.ts +5 -0
- package/src/activation-source-projection.ts +6 -0
- package/src/activation-source.ts +2 -0
- package/src/compose-batch.ts +1 -1
- package/src/draft.ts +21 -21
- package/src/{contour.ts → entity.ts} +72 -72
- package/src/execute.ts +15 -15
- package/src/fire.ts +5 -2
- package/src/index.ts +19 -17
- package/src/layer.ts +1 -1
- package/src/observe.ts +1 -1
- package/src/queue.ts +163 -0
- package/src/signal-ref.ts +30 -6
- package/src/store/accessor-protocol.ts +4 -4
- package/src/surface-overlay.ts +1 -1
- package/src/topo.ts +47 -47
- package/src/tracing.ts +1 -0
- package/src/trail.ts +92 -87
- package/src/trails/derive-trail.ts +202 -209
- package/src/trails/ingest.ts +18 -5
- package/src/trails-db.ts +1 -1
- package/src/type-utils.ts +1 -1
- package/src/types.ts +3 -3
- package/src/validate-established-topo.ts +7 -3
- package/src/validate-topo.ts +31 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { AnyEntity } from '../entity.js';
|
|
4
4
|
import {
|
|
5
5
|
DerivationError,
|
|
6
6
|
InternalError,
|
|
@@ -25,96 +25,96 @@ export type DeriveTrailOperation =
|
|
|
25
25
|
| 'delete'
|
|
26
26
|
| 'list';
|
|
27
27
|
|
|
28
|
-
type
|
|
29
|
-
type
|
|
30
|
-
type
|
|
31
|
-
keyof
|
|
28
|
+
type EntityInput<TEntity extends AnyEntity> = z.input<TEntity>;
|
|
29
|
+
type EntityOutput<TEntity extends AnyEntity> = z.output<TEntity>;
|
|
30
|
+
type EntityFieldKey<TEntity extends AnyEntity> = Extract<
|
|
31
|
+
keyof EntityOutput<TEntity>,
|
|
32
32
|
string
|
|
33
33
|
>;
|
|
34
|
-
type IdentityKey<
|
|
35
|
-
|
|
36
|
-
keyof
|
|
34
|
+
type IdentityKey<TEntity extends AnyEntity> = Extract<
|
|
35
|
+
TEntity['identity'],
|
|
36
|
+
keyof EntityInput<TEntity> & string
|
|
37
37
|
>;
|
|
38
38
|
|
|
39
39
|
type GeneratedKey<
|
|
40
|
-
|
|
41
|
-
TGenerated extends readonly
|
|
42
|
-
> = TGenerated extends readonly
|
|
40
|
+
TEntity extends AnyEntity,
|
|
41
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined,
|
|
42
|
+
> = TGenerated extends readonly EntityFieldKey<TEntity>[]
|
|
43
43
|
? TGenerated[number]
|
|
44
44
|
: never;
|
|
45
45
|
|
|
46
46
|
type CreateInputOf<
|
|
47
|
-
|
|
48
|
-
TGenerated extends readonly
|
|
47
|
+
TEntity extends AnyEntity,
|
|
48
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined,
|
|
49
49
|
> = Omit<
|
|
50
|
-
|
|
51
|
-
Extract<GeneratedKey<
|
|
50
|
+
EntityInput<TEntity>,
|
|
51
|
+
Extract<GeneratedKey<TEntity, TGenerated>, keyof EntityInput<TEntity>>
|
|
52
52
|
>;
|
|
53
53
|
|
|
54
|
-
type ReadInputOf<
|
|
55
|
-
|
|
56
|
-
IdentityKey<
|
|
54
|
+
type ReadInputOf<TEntity extends AnyEntity> = Pick<
|
|
55
|
+
EntityInput<TEntity>,
|
|
56
|
+
IdentityKey<TEntity>
|
|
57
57
|
>;
|
|
58
58
|
|
|
59
59
|
type UpdateInputOf<
|
|
60
|
-
|
|
61
|
-
TGenerated extends readonly
|
|
62
|
-
> = ReadInputOf<
|
|
63
|
-
Partial<Omit<CreateInputOf<
|
|
60
|
+
TEntity extends AnyEntity,
|
|
61
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined,
|
|
62
|
+
> = ReadInputOf<TEntity> &
|
|
63
|
+
Partial<Omit<CreateInputOf<TEntity, TGenerated>, IdentityKey<TEntity>>>;
|
|
64
64
|
|
|
65
|
-
type ListInputOf<
|
|
65
|
+
type ListInputOf<TEntity extends AnyEntity> = Partial<EntityInput<TEntity>>;
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* Input shape derived for one operation against one
|
|
68
|
+
* Input shape derived for one operation against one entity.
|
|
69
69
|
*/
|
|
70
70
|
export type DeriveTrailInput<
|
|
71
|
-
|
|
71
|
+
TEntity extends AnyEntity,
|
|
72
72
|
TOperation extends DeriveTrailOperation,
|
|
73
|
-
TGenerated extends readonly
|
|
74
|
-
| readonly
|
|
73
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined =
|
|
74
|
+
| readonly EntityFieldKey<TEntity>[]
|
|
75
75
|
| undefined,
|
|
76
76
|
> = TOperation extends 'create'
|
|
77
|
-
? CreateInputOf<
|
|
77
|
+
? CreateInputOf<TEntity, TGenerated>
|
|
78
78
|
: TOperation extends 'read' | 'delete'
|
|
79
|
-
? ReadInputOf<
|
|
79
|
+
? ReadInputOf<TEntity>
|
|
80
80
|
: TOperation extends 'update'
|
|
81
|
-
? UpdateInputOf<
|
|
82
|
-
: ListInputOf<
|
|
81
|
+
? UpdateInputOf<TEntity, TGenerated>
|
|
82
|
+
: ListInputOf<TEntity>;
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* Output shape derived for one operation against one
|
|
85
|
+
* Output shape derived for one operation against one entity.
|
|
86
86
|
*/
|
|
87
87
|
export type DeriveTrailOutput<
|
|
88
|
-
|
|
88
|
+
TEntity extends AnyEntity,
|
|
89
89
|
TOperation extends DeriveTrailOperation,
|
|
90
90
|
> = TOperation extends 'delete'
|
|
91
91
|
? undefined
|
|
92
92
|
: TOperation extends 'list'
|
|
93
|
-
?
|
|
94
|
-
:
|
|
93
|
+
? EntityOutput<TEntity>[]
|
|
94
|
+
: EntityOutput<TEntity>;
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Extra authored data accepted by `deriveTrail()` in addition to the
|
|
98
98
|
* operation-derived contract pieces.
|
|
99
99
|
*
|
|
100
|
-
* `
|
|
101
|
-
* synthesizes a default
|
|
100
|
+
* `implementation` is optional for single-resource calls: when omitted, the helper
|
|
101
|
+
* synthesizes a default implementation that delegates to the resource's accessor via
|
|
102
102
|
* the structural {@link StoreAccessorProtocol}. When multiple resources are
|
|
103
|
-
* declared, an explicit `
|
|
103
|
+
* declared, an explicit `implementation` is required.
|
|
104
104
|
*/
|
|
105
105
|
export interface DeriveTrailSpec<
|
|
106
|
-
|
|
106
|
+
TEntity extends AnyEntity,
|
|
107
107
|
TOperation extends DeriveTrailOperation,
|
|
108
|
-
TGenerated extends readonly
|
|
109
|
-
| readonly
|
|
108
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined =
|
|
109
|
+
| readonly EntityFieldKey<TEntity>[]
|
|
110
110
|
| undefined,
|
|
111
111
|
> extends Omit<
|
|
112
112
|
TrailSpec<
|
|
113
|
-
DeriveTrailInput<
|
|
114
|
-
DeriveTrailOutput<
|
|
113
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>,
|
|
114
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
115
115
|
>,
|
|
116
|
-
| '
|
|
117
|
-
| '
|
|
116
|
+
| 'implementation'
|
|
117
|
+
| 'entities'
|
|
118
118
|
| 'examples'
|
|
119
119
|
| 'input'
|
|
120
120
|
| 'intent'
|
|
@@ -123,12 +123,12 @@ export interface DeriveTrailSpec<
|
|
|
123
123
|
> {
|
|
124
124
|
/**
|
|
125
125
|
* Implementation of the trail. Optional for single-resource calls: when
|
|
126
|
-
* omitted, the helper derives a default
|
|
126
|
+
* omitted, the helper derives a default implementation from the resource accessor
|
|
127
127
|
* for standard CRUD operations.
|
|
128
128
|
*/
|
|
129
|
-
readonly
|
|
130
|
-
DeriveTrailInput<
|
|
131
|
-
DeriveTrailOutput<
|
|
129
|
+
readonly implementation?: Implementation<
|
|
130
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>,
|
|
131
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
132
132
|
>;
|
|
133
133
|
/**
|
|
134
134
|
* Server-managed fields that should not be writable through derived create
|
|
@@ -137,8 +137,8 @@ export interface DeriveTrailSpec<
|
|
|
137
137
|
readonly generated?: TGenerated;
|
|
138
138
|
/**
|
|
139
139
|
* Resource dependency declared on the derived trail. Pass a single
|
|
140
|
-
* resource for default-
|
|
141
|
-
* trails that must provide an explicit `
|
|
140
|
+
* resource for default-implementation synthesis, or an array for multi-resource
|
|
141
|
+
* trails that must provide an explicit `implementation`.
|
|
142
142
|
*/
|
|
143
143
|
readonly resource: AnyResource | readonly AnyResource[];
|
|
144
144
|
}
|
|
@@ -212,75 +212,75 @@ const normalizeResources = (
|
|
|
212
212
|
): readonly AnyResource[] =>
|
|
213
213
|
Object.freeze(Array.isArray(resource) ? [...resource] : [resource]);
|
|
214
214
|
|
|
215
|
-
const identityInputSchema = <
|
|
216
|
-
|
|
217
|
-
): z.ZodType<ReadInputOf<
|
|
218
|
-
pickFields(
|
|
219
|
-
ReadInputOf<
|
|
215
|
+
const identityInputSchema = <TEntity extends AnyEntity>(
|
|
216
|
+
entity: TEntity
|
|
217
|
+
): z.ZodType<ReadInputOf<TEntity>> =>
|
|
218
|
+
pickFields(entity, [entity.identity]) as unknown as z.ZodType<
|
|
219
|
+
ReadInputOf<TEntity>
|
|
220
220
|
>;
|
|
221
221
|
|
|
222
222
|
const createInputSchema = <
|
|
223
|
-
|
|
224
|
-
TGenerated extends readonly
|
|
223
|
+
TEntity extends AnyEntity,
|
|
224
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined,
|
|
225
225
|
>(
|
|
226
|
-
|
|
226
|
+
entity: TEntity,
|
|
227
227
|
generated: readonly string[]
|
|
228
|
-
): z.ZodType<CreateInputOf<
|
|
229
|
-
omitFields(
|
|
230
|
-
CreateInputOf<
|
|
228
|
+
): z.ZodType<CreateInputOf<TEntity, TGenerated>> =>
|
|
229
|
+
omitFields(entity, generated) as unknown as z.ZodType<
|
|
230
|
+
CreateInputOf<TEntity, TGenerated>
|
|
231
231
|
>;
|
|
232
232
|
|
|
233
233
|
const updateInputSchema = <
|
|
234
|
-
|
|
235
|
-
TGenerated extends readonly
|
|
234
|
+
TEntity extends AnyEntity,
|
|
235
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined,
|
|
236
236
|
>(
|
|
237
|
-
|
|
237
|
+
entity: TEntity,
|
|
238
238
|
generated: readonly string[]
|
|
239
|
-
): z.ZodType<UpdateInputOf<
|
|
240
|
-
const mutableSchema = omitFields(
|
|
241
|
-
const identitySchema = asObjectSchema(identityInputSchema(
|
|
239
|
+
): z.ZodType<UpdateInputOf<TEntity, TGenerated>> => {
|
|
240
|
+
const mutableSchema = omitFields(entity, [...generated, entity.identity]);
|
|
241
|
+
const identitySchema = asObjectSchema(identityInputSchema(entity));
|
|
242
242
|
|
|
243
243
|
return identitySchema.extend(
|
|
244
244
|
toPartialSchema(mutableSchema).shape
|
|
245
|
-
) as unknown as z.ZodType<UpdateInputOf<
|
|
245
|
+
) as unknown as z.ZodType<UpdateInputOf<TEntity, TGenerated>>;
|
|
246
246
|
};
|
|
247
247
|
|
|
248
|
-
const listInputSchema = <
|
|
249
|
-
|
|
250
|
-
): z.ZodType<ListInputOf<
|
|
251
|
-
toPartialSchema(
|
|
248
|
+
const listInputSchema = <TEntity extends AnyEntity>(
|
|
249
|
+
entity: TEntity
|
|
250
|
+
): z.ZodType<ListInputOf<TEntity>> =>
|
|
251
|
+
toPartialSchema(entity) as unknown as z.ZodType<ListInputOf<TEntity>>;
|
|
252
252
|
|
|
253
253
|
const deriveInputSchema = <
|
|
254
|
-
|
|
254
|
+
TEntity extends AnyEntity,
|
|
255
255
|
TOperation extends DeriveTrailOperation,
|
|
256
|
-
TGenerated extends readonly
|
|
256
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined,
|
|
257
257
|
>(
|
|
258
|
-
|
|
258
|
+
entity: TEntity,
|
|
259
259
|
operation: TOperation,
|
|
260
260
|
generated: readonly string[]
|
|
261
|
-
): z.ZodType<DeriveTrailInput<
|
|
261
|
+
): z.ZodType<DeriveTrailInput<TEntity, TOperation, TGenerated>> => {
|
|
262
262
|
switch (operation) {
|
|
263
263
|
case 'create': {
|
|
264
|
-
return createInputSchema<
|
|
265
|
-
|
|
264
|
+
return createInputSchema<TEntity, TGenerated>(
|
|
265
|
+
entity,
|
|
266
266
|
generated
|
|
267
|
-
) as z.ZodType<DeriveTrailInput<
|
|
267
|
+
) as z.ZodType<DeriveTrailInput<TEntity, TOperation, TGenerated>>;
|
|
268
268
|
}
|
|
269
269
|
case 'read':
|
|
270
270
|
case 'delete': {
|
|
271
|
-
return identityInputSchema(
|
|
272
|
-
DeriveTrailInput<
|
|
271
|
+
return identityInputSchema(entity) as z.ZodType<
|
|
272
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>
|
|
273
273
|
>;
|
|
274
274
|
}
|
|
275
275
|
case 'update': {
|
|
276
|
-
return updateInputSchema<
|
|
277
|
-
|
|
276
|
+
return updateInputSchema<TEntity, TGenerated>(
|
|
277
|
+
entity,
|
|
278
278
|
generated
|
|
279
|
-
) as z.ZodType<DeriveTrailInput<
|
|
279
|
+
) as z.ZodType<DeriveTrailInput<TEntity, TOperation, TGenerated>>;
|
|
280
280
|
}
|
|
281
281
|
case 'list': {
|
|
282
|
-
return listInputSchema(
|
|
283
|
-
DeriveTrailInput<
|
|
282
|
+
return listInputSchema(entity) as z.ZodType<
|
|
283
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>
|
|
284
284
|
>;
|
|
285
285
|
}
|
|
286
286
|
default: {
|
|
@@ -290,28 +290,28 @@ const deriveInputSchema = <
|
|
|
290
290
|
};
|
|
291
291
|
|
|
292
292
|
const deriveOutputSchema = <
|
|
293
|
-
|
|
293
|
+
TEntity extends AnyEntity,
|
|
294
294
|
TOperation extends DeriveTrailOperation,
|
|
295
295
|
>(
|
|
296
|
-
|
|
296
|
+
entity: TEntity,
|
|
297
297
|
operation: TOperation
|
|
298
|
-
): z.ZodType<DeriveTrailOutput<
|
|
298
|
+
): z.ZodType<DeriveTrailOutput<TEntity, TOperation>> => {
|
|
299
299
|
switch (operation) {
|
|
300
300
|
case 'delete': {
|
|
301
301
|
return z.void() as unknown as z.ZodType<
|
|
302
|
-
DeriveTrailOutput<
|
|
302
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
303
303
|
>;
|
|
304
304
|
}
|
|
305
305
|
case 'list': {
|
|
306
|
-
return
|
|
307
|
-
DeriveTrailOutput<
|
|
306
|
+
return entity.array() as unknown as z.ZodType<
|
|
307
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
308
308
|
>;
|
|
309
309
|
}
|
|
310
310
|
case 'create':
|
|
311
311
|
case 'read':
|
|
312
312
|
case 'update': {
|
|
313
|
-
return
|
|
314
|
-
DeriveTrailOutput<
|
|
313
|
+
return entity as unknown as z.ZodType<
|
|
314
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
315
315
|
>;
|
|
316
316
|
}
|
|
317
317
|
default: {
|
|
@@ -344,37 +344,37 @@ const omitValueFields = (
|
|
|
344
344
|
};
|
|
345
345
|
|
|
346
346
|
const formatExampleName = (
|
|
347
|
-
|
|
347
|
+
entity: AnyEntity,
|
|
348
348
|
operation: DeriveTrailOperation,
|
|
349
349
|
example: ExampleRecord,
|
|
350
350
|
index: number
|
|
351
351
|
): string => {
|
|
352
|
-
const identifier = example[
|
|
352
|
+
const identifier = example[entity.identity];
|
|
353
353
|
const suffix =
|
|
354
354
|
identifier === undefined ? String(index + 1) : String(identifier);
|
|
355
|
-
return `${titleCase(operation)} ${
|
|
355
|
+
return `${titleCase(operation)} ${entity.name} ${suffix}`;
|
|
356
356
|
};
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
|
-
* Derive a single trail example from a
|
|
359
|
+
* Derive a single trail example from a entity fixture.
|
|
360
360
|
*
|
|
361
361
|
* @remarks
|
|
362
362
|
* For `list` operations, each derived example wraps a single fixture in an
|
|
363
363
|
* array (`expected: [example]`) and uses the fixture's identity as input
|
|
364
364
|
* filters. This means the expected output is always a one-element array,
|
|
365
365
|
* which may not match the real accessor behavior when multiple fixtures
|
|
366
|
-
* share the same filter. A custom `
|
|
366
|
+
* share the same filter. A custom `implementation` with hand-authored examples is
|
|
367
367
|
* required for multi-result list assertions.
|
|
368
368
|
*/
|
|
369
369
|
const deriveExample = (
|
|
370
|
-
|
|
370
|
+
entity: AnyEntity,
|
|
371
371
|
operation: DeriveTrailOperation,
|
|
372
372
|
example: ExampleRecord,
|
|
373
373
|
index: number,
|
|
374
374
|
generated: readonly string[]
|
|
375
375
|
): TrailExample<unknown, unknown> => {
|
|
376
|
-
const name = formatExampleName(
|
|
377
|
-
const identity = pickValueFields(example, [
|
|
376
|
+
const name = formatExampleName(entity, operation, example, index);
|
|
377
|
+
const identity = pickValueFields(example, [entity.identity]);
|
|
378
378
|
|
|
379
379
|
switch (operation) {
|
|
380
380
|
case 'create': {
|
|
@@ -395,7 +395,7 @@ const deriveExample = (
|
|
|
395
395
|
return {
|
|
396
396
|
expected: example,
|
|
397
397
|
input: {
|
|
398
|
-
...omitValueFields(example, [...generated,
|
|
398
|
+
...omitValueFields(example, [...generated, entity.identity]),
|
|
399
399
|
...identity,
|
|
400
400
|
},
|
|
401
401
|
name,
|
|
@@ -421,11 +421,11 @@ const deriveExample = (
|
|
|
421
421
|
};
|
|
422
422
|
|
|
423
423
|
const deriveExamples = (
|
|
424
|
-
|
|
424
|
+
entity: AnyEntity,
|
|
425
425
|
operation: DeriveTrailOperation,
|
|
426
426
|
generated: readonly string[]
|
|
427
427
|
): readonly TrailExample<unknown, unknown>[] | undefined => {
|
|
428
|
-
if (
|
|
428
|
+
if (entity.examples === undefined || entity.examples.length === 0) {
|
|
429
429
|
return undefined;
|
|
430
430
|
}
|
|
431
431
|
|
|
@@ -436,17 +436,17 @@ const deriveExamples = (
|
|
|
436
436
|
if (operation === 'list') {
|
|
437
437
|
return Object.freeze([
|
|
438
438
|
{
|
|
439
|
-
expected:
|
|
439
|
+
expected: entity.examples,
|
|
440
440
|
input: {},
|
|
441
|
-
name: `${
|
|
441
|
+
name: `${entity.name} list example`,
|
|
442
442
|
},
|
|
443
443
|
]);
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
return Object.freeze(
|
|
447
|
-
|
|
447
|
+
entity.examples.map((example, index) =>
|
|
448
448
|
deriveExample(
|
|
449
|
-
|
|
449
|
+
entity,
|
|
450
450
|
operation,
|
|
451
451
|
example as ExampleRecord,
|
|
452
452
|
index,
|
|
@@ -457,7 +457,7 @@ const deriveExamples = (
|
|
|
457
457
|
};
|
|
458
458
|
|
|
459
459
|
// ---------------------------------------------------------------------------
|
|
460
|
-
// Default-
|
|
460
|
+
// Default-implementation synthesis
|
|
461
461
|
// ---------------------------------------------------------------------------
|
|
462
462
|
|
|
463
463
|
type GenericAccessor = StoreAccessorProtocol<
|
|
@@ -468,7 +468,7 @@ type GenericAccessor = StoreAccessorProtocol<
|
|
|
468
468
|
>;
|
|
469
469
|
|
|
470
470
|
const wrapUnexpected = (
|
|
471
|
-
|
|
471
|
+
entityName: string,
|
|
472
472
|
operation: DeriveTrailOperation,
|
|
473
473
|
error: unknown
|
|
474
474
|
): Error => {
|
|
@@ -477,18 +477,18 @@ const wrapUnexpected = (
|
|
|
477
477
|
}
|
|
478
478
|
const cause = error instanceof Error ? error : new Error(String(error));
|
|
479
479
|
return new InternalError(
|
|
480
|
-
`deriveTrail("${
|
|
480
|
+
`deriveTrail("${entityName}.${operation}") synthesized implementation failed: ${cause.message}`,
|
|
481
481
|
{ cause }
|
|
482
482
|
);
|
|
483
483
|
};
|
|
484
484
|
|
|
485
|
-
const notFoundError = (
|
|
485
|
+
const notFoundError = (entityName: string, id: unknown): NotFoundError =>
|
|
486
486
|
new NotFoundError(
|
|
487
|
-
`deriveTrail("${
|
|
487
|
+
`deriveTrail("${entityName}"): entity "${String(id)}" not found`
|
|
488
488
|
);
|
|
489
489
|
|
|
490
490
|
const resolveAccessor = (
|
|
491
|
-
|
|
491
|
+
entity: AnyEntity,
|
|
492
492
|
operation: DeriveTrailOperation,
|
|
493
493
|
resource: AnyResource,
|
|
494
494
|
ctx: TrailContext
|
|
@@ -499,52 +499,52 @@ const resolveAccessor = (
|
|
|
499
499
|
| undefined;
|
|
500
500
|
if (connection === undefined || connection === null) {
|
|
501
501
|
return new InternalError(
|
|
502
|
-
`deriveTrail("${
|
|
502
|
+
`deriveTrail("${entity.name}.${operation}"): resource "${resource.id}" produced no connection`
|
|
503
503
|
);
|
|
504
504
|
}
|
|
505
|
-
const accessor = connection[
|
|
505
|
+
const accessor = connection[entity.name];
|
|
506
506
|
if (accessor === undefined) {
|
|
507
507
|
return new InternalError(
|
|
508
|
-
`deriveTrail("${
|
|
508
|
+
`deriveTrail("${entity.name}.${operation}"): resource "${resource.id}" does not expose an accessor for "${entity.name}"`
|
|
509
509
|
);
|
|
510
510
|
}
|
|
511
511
|
return accessor;
|
|
512
512
|
} catch (error) {
|
|
513
|
-
return wrapUnexpected(
|
|
513
|
+
return wrapUnexpected(entity.name, operation, error);
|
|
514
514
|
}
|
|
515
515
|
};
|
|
516
516
|
|
|
517
|
-
const extractIdentity = (
|
|
517
|
+
const extractIdentity = (entity: AnyEntity, input: unknown): unknown => {
|
|
518
518
|
const record = input as Record<string, unknown>;
|
|
519
|
-
return record[
|
|
519
|
+
return record[entity.identity];
|
|
520
520
|
};
|
|
521
521
|
|
|
522
522
|
const callRead = async (
|
|
523
|
-
|
|
523
|
+
entity: AnyEntity,
|
|
524
524
|
accessor: GenericAccessor,
|
|
525
525
|
input: unknown
|
|
526
526
|
): Promise<Result<unknown, Error>> => {
|
|
527
527
|
if (typeof accessor.get !== 'function') {
|
|
528
528
|
return Result.err(
|
|
529
529
|
new InternalError(
|
|
530
|
-
`deriveTrail("${
|
|
530
|
+
`deriveTrail("${entity.name}.read"): accessor is missing a \`get\` method`
|
|
531
531
|
)
|
|
532
532
|
);
|
|
533
533
|
}
|
|
534
534
|
try {
|
|
535
|
-
const id = extractIdentity(
|
|
536
|
-
const
|
|
537
|
-
if (
|
|
538
|
-
return Result.err(notFoundError(
|
|
535
|
+
const id = extractIdentity(entity, input);
|
|
536
|
+
const foundEntity = await accessor.get(id);
|
|
537
|
+
if (foundEntity === null || foundEntity === undefined) {
|
|
538
|
+
return Result.err(notFoundError(entity.name, id));
|
|
539
539
|
}
|
|
540
|
-
return Result.ok(
|
|
540
|
+
return Result.ok(foundEntity);
|
|
541
541
|
} catch (error) {
|
|
542
|
-
return Result.err(wrapUnexpected(
|
|
542
|
+
return Result.err(wrapUnexpected(entity.name, 'read', error));
|
|
543
543
|
}
|
|
544
544
|
};
|
|
545
545
|
|
|
546
546
|
const callCreate = async (
|
|
547
|
-
|
|
547
|
+
entity: AnyEntity,
|
|
548
548
|
accessor: GenericAccessor,
|
|
549
549
|
input: unknown,
|
|
550
550
|
ctx: TrailContext
|
|
@@ -560,17 +560,17 @@ const callCreate = async (
|
|
|
560
560
|
if (typeof accessor.upsert !== 'function') {
|
|
561
561
|
return Result.err(
|
|
562
562
|
new InternalError(
|
|
563
|
-
`deriveTrail("${
|
|
563
|
+
`deriveTrail("${entity.name}.create"): accessor is missing both \`insert\` and \`upsert\``
|
|
564
564
|
)
|
|
565
565
|
);
|
|
566
566
|
}
|
|
567
567
|
ctx.logger?.debug(
|
|
568
|
-
`deriveTrail("${
|
|
568
|
+
`deriveTrail("${entity.name}.create"): accessor has no \`insert\`; falling back to \`upsert\``
|
|
569
569
|
);
|
|
570
570
|
const created = await accessor.upsert(input);
|
|
571
571
|
return Result.ok(created);
|
|
572
572
|
} catch (error) {
|
|
573
|
-
return Result.err(wrapUnexpected(
|
|
573
|
+
return Result.err(wrapUnexpected(entity.name, 'create', error));
|
|
574
574
|
}
|
|
575
575
|
};
|
|
576
576
|
|
|
@@ -604,7 +604,7 @@ const stripGeneratedFields = (
|
|
|
604
604
|
* then `upsert`.
|
|
605
605
|
*/
|
|
606
606
|
const updateViaReadAndUpsert = async (
|
|
607
|
-
|
|
607
|
+
entity: AnyEntity,
|
|
608
608
|
accessor: GenericAccessor,
|
|
609
609
|
id: unknown,
|
|
610
610
|
patch: Record<string, unknown>,
|
|
@@ -613,40 +613,40 @@ const updateViaReadAndUpsert = async (
|
|
|
613
613
|
if (typeof accessor.get !== 'function') {
|
|
614
614
|
return Result.err(
|
|
615
615
|
new InternalError(
|
|
616
|
-
`deriveTrail("${
|
|
616
|
+
`deriveTrail("${entity.name}.update"): accessor is missing both \`update\` and \`get\``
|
|
617
617
|
)
|
|
618
618
|
);
|
|
619
619
|
}
|
|
620
620
|
if (typeof accessor.upsert !== 'function') {
|
|
621
621
|
return Result.err(
|
|
622
622
|
new InternalError(
|
|
623
|
-
`deriveTrail("${
|
|
623
|
+
`deriveTrail("${entity.name}.update"): accessor is missing both \`update\` and \`upsert\``
|
|
624
624
|
)
|
|
625
625
|
);
|
|
626
626
|
}
|
|
627
627
|
const current = await accessor.get(id);
|
|
628
628
|
if (current === null || current === undefined) {
|
|
629
|
-
return Result.err(notFoundError(
|
|
629
|
+
return Result.err(notFoundError(entity.name, id));
|
|
630
630
|
}
|
|
631
631
|
const merged = stripGeneratedFields(
|
|
632
632
|
{ ...(current as Record<string, unknown>), ...patch },
|
|
633
633
|
generated,
|
|
634
|
-
|
|
634
|
+
entity.identity
|
|
635
635
|
);
|
|
636
636
|
const updated = await accessor.upsert(merged);
|
|
637
637
|
return Result.ok(updated);
|
|
638
638
|
};
|
|
639
639
|
|
|
640
640
|
const callUpdate = async (
|
|
641
|
-
|
|
641
|
+
entity: AnyEntity,
|
|
642
642
|
accessor: GenericAccessor,
|
|
643
643
|
input: unknown,
|
|
644
644
|
generated: readonly string[]
|
|
645
645
|
): Promise<Result<unknown, Error>> => {
|
|
646
|
-
const id = extractIdentity(
|
|
646
|
+
const id = extractIdentity(entity, input);
|
|
647
647
|
const patch = Object.fromEntries(
|
|
648
648
|
Object.entries(input as Record<string, unknown>).filter(
|
|
649
|
-
([field]) => field !==
|
|
649
|
+
([field]) => field !== entity.identity
|
|
650
650
|
)
|
|
651
651
|
);
|
|
652
652
|
|
|
@@ -654,60 +654,54 @@ const callUpdate = async (
|
|
|
654
654
|
if (typeof accessor.update === 'function') {
|
|
655
655
|
const updated = await accessor.update(id, patch);
|
|
656
656
|
if (updated === null || updated === undefined) {
|
|
657
|
-
return Result.err(notFoundError(
|
|
657
|
+
return Result.err(notFoundError(entity.name, id));
|
|
658
658
|
}
|
|
659
659
|
return Result.ok(updated);
|
|
660
660
|
}
|
|
661
|
-
return await updateViaReadAndUpsert(
|
|
662
|
-
contour,
|
|
663
|
-
accessor,
|
|
664
|
-
id,
|
|
665
|
-
patch,
|
|
666
|
-
generated
|
|
667
|
-
);
|
|
661
|
+
return await updateViaReadAndUpsert(entity, accessor, id, patch, generated);
|
|
668
662
|
} catch (error) {
|
|
669
|
-
return Result.err(wrapUnexpected(
|
|
663
|
+
return Result.err(wrapUnexpected(entity.name, 'update', error));
|
|
670
664
|
}
|
|
671
665
|
};
|
|
672
666
|
|
|
673
667
|
const callDelete = async (
|
|
674
|
-
|
|
668
|
+
entity: AnyEntity,
|
|
675
669
|
accessor: GenericAccessor,
|
|
676
670
|
input: unknown
|
|
677
671
|
): Promise<Result<undefined, Error>> => {
|
|
678
672
|
if (typeof accessor.remove !== 'function') {
|
|
679
673
|
return Result.err(
|
|
680
674
|
new InternalError(
|
|
681
|
-
`deriveTrail("${
|
|
675
|
+
`deriveTrail("${entity.name}.delete"): accessor is missing a \`remove\` method`
|
|
682
676
|
)
|
|
683
677
|
);
|
|
684
678
|
}
|
|
685
679
|
try {
|
|
686
|
-
const id = extractIdentity(
|
|
680
|
+
const id = extractIdentity(entity, input);
|
|
687
681
|
await accessor.remove(id);
|
|
688
682
|
// `{ deleted: false }` is a no-op on an absent row, not an error —
|
|
689
683
|
// matches the accessor's documented semantic.
|
|
690
684
|
return Result.ok();
|
|
691
685
|
} catch (error) {
|
|
692
|
-
return Result.err(wrapUnexpected(
|
|
686
|
+
return Result.err(wrapUnexpected(entity.name, 'delete', error));
|
|
693
687
|
}
|
|
694
688
|
};
|
|
695
689
|
|
|
696
690
|
/**
|
|
697
691
|
* Default `list` synthesis passes the entire input as the filter bag. The
|
|
698
|
-
* derived input type is `Partial<
|
|
692
|
+
* derived input type is `Partial<EntityInput>` which matches the accessor's
|
|
699
693
|
* filter shape field-for-field. Pagination controls are not derived — callers
|
|
700
|
-
* that need pagination must provide an explicit
|
|
694
|
+
* that need pagination must provide an explicit implementation.
|
|
701
695
|
*/
|
|
702
696
|
const callList = async (
|
|
703
|
-
|
|
697
|
+
entity: AnyEntity,
|
|
704
698
|
accessor: GenericAccessor,
|
|
705
699
|
input: unknown
|
|
706
700
|
): Promise<Result<unknown[], Error>> => {
|
|
707
701
|
if (typeof accessor.list !== 'function') {
|
|
708
702
|
return Result.err(
|
|
709
703
|
new InternalError(
|
|
710
|
-
`deriveTrail("${
|
|
704
|
+
`deriveTrail("${entity.name}.list"): accessor is missing a \`list\` method`
|
|
711
705
|
)
|
|
712
706
|
);
|
|
713
707
|
}
|
|
@@ -715,43 +709,43 @@ const callList = async (
|
|
|
715
709
|
const listed = await accessor.list(input);
|
|
716
710
|
return Result.ok([...listed]);
|
|
717
711
|
} catch (error) {
|
|
718
|
-
return Result.err(wrapUnexpected(
|
|
712
|
+
return Result.err(wrapUnexpected(entity.name, 'list', error));
|
|
719
713
|
}
|
|
720
714
|
};
|
|
721
715
|
|
|
722
|
-
const
|
|
723
|
-
|
|
716
|
+
const synthesizeDefaultImplementation = <
|
|
717
|
+
TEntity extends AnyEntity,
|
|
724
718
|
TOperation extends DeriveTrailOperation,
|
|
725
|
-
TGenerated extends readonly
|
|
719
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined,
|
|
726
720
|
>(
|
|
727
|
-
|
|
721
|
+
entity: TEntity,
|
|
728
722
|
operation: TOperation,
|
|
729
723
|
resource: AnyResource,
|
|
730
724
|
generated: readonly string[]
|
|
731
725
|
): Implementation<
|
|
732
|
-
DeriveTrailInput<
|
|
733
|
-
DeriveTrailOutput<
|
|
726
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>,
|
|
727
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
734
728
|
> => {
|
|
735
729
|
const impl: Implementation<unknown, unknown> = (input, ctx) => {
|
|
736
|
-
const accessor = resolveAccessor(
|
|
730
|
+
const accessor = resolveAccessor(entity, operation, resource, ctx);
|
|
737
731
|
if (accessor instanceof Error) {
|
|
738
732
|
return Promise.resolve(Result.err(accessor));
|
|
739
733
|
}
|
|
740
734
|
switch (operation) {
|
|
741
735
|
case 'create': {
|
|
742
|
-
return callCreate(
|
|
736
|
+
return callCreate(entity, accessor, input, ctx);
|
|
743
737
|
}
|
|
744
738
|
case 'read': {
|
|
745
|
-
return callRead(
|
|
739
|
+
return callRead(entity, accessor, input);
|
|
746
740
|
}
|
|
747
741
|
case 'update': {
|
|
748
|
-
return callUpdate(
|
|
742
|
+
return callUpdate(entity, accessor, input, generated);
|
|
749
743
|
}
|
|
750
744
|
case 'delete': {
|
|
751
|
-
return callDelete(
|
|
745
|
+
return callDelete(entity, accessor, input);
|
|
752
746
|
}
|
|
753
747
|
case 'list': {
|
|
754
|
-
return callList(
|
|
748
|
+
return callList(entity, accessor, input);
|
|
755
749
|
}
|
|
756
750
|
default: {
|
|
757
751
|
return unsupportedOperation(operation);
|
|
@@ -760,83 +754,82 @@ const synthesizeDefaultBlaze = <
|
|
|
760
754
|
};
|
|
761
755
|
|
|
762
756
|
return impl as Implementation<
|
|
763
|
-
DeriveTrailInput<
|
|
764
|
-
DeriveTrailOutput<
|
|
757
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>,
|
|
758
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
765
759
|
>;
|
|
766
760
|
};
|
|
767
761
|
|
|
768
762
|
/**
|
|
769
|
-
* Mechanically project one CRUD-shaped trail from a
|
|
763
|
+
* Mechanically project one CRUD-shaped trail from a entity declaration.
|
|
770
764
|
*
|
|
771
|
-
* When `spec.
|
|
772
|
-
* helper derives a default
|
|
765
|
+
* When `spec.implementation` is omitted and the call declares a single resource, the
|
|
766
|
+
* helper derives a default implementation that dispatches to the resource accessor
|
|
773
767
|
* through the structural {@link StoreAccessorProtocol}. Multi-resource calls
|
|
774
|
-
* must supply an explicit
|
|
768
|
+
* must supply an explicit implementation and are rejected with {@link DerivationError}
|
|
775
769
|
* at construction time when they do not.
|
|
776
770
|
*/
|
|
777
771
|
export const deriveTrail = <
|
|
778
|
-
|
|
772
|
+
TEntity extends AnyEntity,
|
|
779
773
|
TOperation extends DeriveTrailOperation,
|
|
780
|
-
TGenerated extends readonly
|
|
781
|
-
| readonly
|
|
774
|
+
TGenerated extends readonly EntityFieldKey<TEntity>[] | undefined =
|
|
775
|
+
| readonly EntityFieldKey<TEntity>[]
|
|
782
776
|
| undefined,
|
|
783
777
|
>(
|
|
784
|
-
|
|
778
|
+
entity: TEntity,
|
|
785
779
|
operation: TOperation,
|
|
786
|
-
spec: DeriveTrailSpec<
|
|
780
|
+
spec: DeriveTrailSpec<TEntity, TOperation, TGenerated>
|
|
787
781
|
): Trail<
|
|
788
|
-
DeriveTrailInput<
|
|
789
|
-
DeriveTrailOutput<
|
|
782
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>,
|
|
783
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
790
784
|
> => {
|
|
791
785
|
const resources = normalizeResources(spec.resource);
|
|
792
786
|
const generated = uniqueStrings(
|
|
793
787
|
spec.generated as readonly string[] | undefined
|
|
794
788
|
);
|
|
795
789
|
|
|
796
|
-
let
|
|
797
|
-
DeriveTrailInput<
|
|
798
|
-
DeriveTrailOutput<
|
|
790
|
+
let implementation: Implementation<
|
|
791
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>,
|
|
792
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
799
793
|
>;
|
|
800
|
-
if (typeof spec.
|
|
801
|
-
({
|
|
794
|
+
if (typeof spec.implementation === 'function') {
|
|
795
|
+
({ implementation } = spec);
|
|
802
796
|
} else if (resources.length === 1) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
);
|
|
797
|
+
implementation = synthesizeDefaultImplementation<
|
|
798
|
+
TEntity,
|
|
799
|
+
TOperation,
|
|
800
|
+
TGenerated
|
|
801
|
+
>(entity, operation, resources[0] as AnyResource, generated);
|
|
809
802
|
} else {
|
|
810
803
|
throw new DerivationError(
|
|
811
|
-
`deriveTrail("${
|
|
804
|
+
`deriveTrail("${entity.name}.${operation}") requires an explicit \`implementation\` when ${describeDeriveTrailResourceDeclaration(resources.length)} — default synthesis is single-resource only`
|
|
812
805
|
);
|
|
813
806
|
}
|
|
814
807
|
const {
|
|
815
|
-
|
|
808
|
+
implementation: _implementation,
|
|
816
809
|
resource: _resource,
|
|
817
810
|
generated: _generated,
|
|
818
811
|
...trailSpec
|
|
819
812
|
} = spec;
|
|
820
813
|
const derivedSpec = {
|
|
821
814
|
...trailSpec,
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
input: deriveInputSchema<
|
|
826
|
-
|
|
815
|
+
entities: [entity],
|
|
816
|
+
examples: deriveExamples(entity, operation, generated),
|
|
817
|
+
implementation,
|
|
818
|
+
input: deriveInputSchema<TEntity, TOperation, TGenerated>(
|
|
819
|
+
entity,
|
|
827
820
|
operation,
|
|
828
821
|
generated
|
|
829
822
|
),
|
|
830
823
|
intent: operationIntent[operation],
|
|
831
|
-
output: deriveOutputSchema(
|
|
824
|
+
output: deriveOutputSchema(entity, operation),
|
|
832
825
|
resources,
|
|
833
826
|
} as unknown as TrailSpec<
|
|
834
|
-
DeriveTrailInput<
|
|
835
|
-
DeriveTrailOutput<
|
|
827
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>,
|
|
828
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
836
829
|
>;
|
|
837
830
|
|
|
838
|
-
return trail(`${
|
|
839
|
-
DeriveTrailInput<
|
|
840
|
-
DeriveTrailOutput<
|
|
831
|
+
return trail(`${entity.name}.${operation}`, derivedSpec) as unknown as Trail<
|
|
832
|
+
DeriveTrailInput<TEntity, TOperation, TGenerated>,
|
|
833
|
+
DeriveTrailOutput<TEntity, TOperation>
|
|
841
834
|
>;
|
|
842
835
|
};
|