@ontrails/core 1.0.0-beta.2 → 1.0.0-beta.21
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 +283 -5
- package/README.md +113 -33
- package/package.json +11 -1
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/branded.ts +1 -1
- package/src/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +44 -10
- package/src/contour.ts +346 -0
- package/src/derive.ts +59 -18
- package/src/detours.ts +8 -0
- package/src/draft.ts +342 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +309 -48
- package/src/execute.ts +1577 -0
- package/src/fire.ts +1169 -0
- package/src/index.ts +477 -32
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-projection.ts +193 -0
- package/src/layer.ts +43 -6
- package/src/observe.ts +361 -0
- package/src/path-security.ts +1 -1
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +792 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +36 -4
- package/src/run.ts +76 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +119 -39
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +87 -0
- package/src/signal.ts +104 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +248 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +176 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +769 -57
- package/src/tracing.ts +345 -0
- package/src/trail.ts +1161 -29
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +139 -0
- package/src/trails-db.ts +208 -0
- package/src/transport-error-map.ts +174 -0
- package/src/type-utils.ts +87 -0
- package/src/types.ts +254 -12
- package/src/validate-established-topo.ts +69 -0
- package/src/validate-topo.ts +442 -35
- package/src/validation.ts +152 -4
- package/src/version-marker.ts +716 -0
- package/src/version-resolution.ts +308 -0
- package/src/version-runtime.ts +120 -0
- package/src/webhook.ts +278 -0
- package/src/workspace.ts +1 -1
- package/src/zod-wrappers.ts +72 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -10
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -15
- package/dist/context.js.map +0 -1
- package/dist/derive.d.ts +0 -33
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -122
- package/dist/derive.js.map +0 -1
- package/dist/errors.d.ts +0 -83
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -142
- package/dist/errors.js.map +0 -1
- package/dist/event.d.ts +0 -45
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -17
- package/dist/event.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/hike.d.ts +0 -36
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -20
- package/dist/hike.js.map +0 -1
- package/dist/index.d.ts +0 -34
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -38
- package/dist/index.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/layer.d.ts +0 -17
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -21
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -145
- package/dist/result.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -115
- package/dist/serialization.js.map +0 -1
- package/dist/topo.d.ts +0 -18
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -74
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -83
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -16
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -46
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -108
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -134
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -66
- package/src/__tests__/derive.test.ts +0 -159
- package/src/__tests__/errors.test.ts +0 -309
- package/src/__tests__/event.test.ts +0 -82
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/hike.test.ts +0 -117
- package/src/__tests__/job.test.ts +0 -98
- package/src/__tests__/layer.test.ts +0 -224
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/serialization.test.ts +0 -236
- package/src/__tests__/topo.test.ts +0 -184
- package/src/__tests__/trail.test.ts +0 -179
- package/src/__tests__/validate-topo.test.ts +0 -201
- package/src/__tests__/validation.test.ts +0 -283
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/adapters.ts +0 -68
- package/src/event.ts +0 -77
- package/src/health.ts +0 -23
- package/src/hike.ts +0 -77
- package/src/job.ts +0 -20
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import type { AnyContour } from '../contour.js';
|
|
4
|
+
import {
|
|
5
|
+
DerivationError,
|
|
6
|
+
InternalError,
|
|
7
|
+
isTrailsError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
} from '../errors.js';
|
|
10
|
+
import { stripDefaultsFromShape } from '../zod-wrappers.js';
|
|
11
|
+
import type { AnyResource } from '../resource.js';
|
|
12
|
+
import { Result } from '../result.js';
|
|
13
|
+
import type { StoreAccessorProtocol } from '../store/accessor-protocol.js';
|
|
14
|
+
import { trail } from '../trail.js';
|
|
15
|
+
import type { Trail, TrailExample, TrailSpec } from '../trail.js';
|
|
16
|
+
import type { Implementation, TrailContext } from '../types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* CRUD-shaped operations the base trail derivation helper understands.
|
|
20
|
+
*/
|
|
21
|
+
export type DeriveTrailOperation =
|
|
22
|
+
| 'create'
|
|
23
|
+
| 'read'
|
|
24
|
+
| 'update'
|
|
25
|
+
| 'delete'
|
|
26
|
+
| 'list';
|
|
27
|
+
|
|
28
|
+
type ContourInput<TContour extends AnyContour> = z.input<TContour>;
|
|
29
|
+
type ContourOutput<TContour extends AnyContour> = z.output<TContour>;
|
|
30
|
+
type ContourFieldKey<TContour extends AnyContour> = Extract<
|
|
31
|
+
keyof ContourOutput<TContour>,
|
|
32
|
+
string
|
|
33
|
+
>;
|
|
34
|
+
type IdentityKey<TContour extends AnyContour> = Extract<
|
|
35
|
+
TContour['identity'],
|
|
36
|
+
keyof ContourInput<TContour> & string
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
type GeneratedKey<
|
|
40
|
+
TContour extends AnyContour,
|
|
41
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
|
|
42
|
+
> = TGenerated extends readonly ContourFieldKey<TContour>[]
|
|
43
|
+
? TGenerated[number]
|
|
44
|
+
: never;
|
|
45
|
+
|
|
46
|
+
type CreateInputOf<
|
|
47
|
+
TContour extends AnyContour,
|
|
48
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
|
|
49
|
+
> = Omit<
|
|
50
|
+
ContourInput<TContour>,
|
|
51
|
+
Extract<GeneratedKey<TContour, TGenerated>, keyof ContourInput<TContour>>
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
type ReadInputOf<TContour extends AnyContour> = Pick<
|
|
55
|
+
ContourInput<TContour>,
|
|
56
|
+
IdentityKey<TContour>
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
type UpdateInputOf<
|
|
60
|
+
TContour extends AnyContour,
|
|
61
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
|
|
62
|
+
> = ReadInputOf<TContour> &
|
|
63
|
+
Partial<Omit<CreateInputOf<TContour, TGenerated>, IdentityKey<TContour>>>;
|
|
64
|
+
|
|
65
|
+
type ListInputOf<TContour extends AnyContour> = Partial<ContourInput<TContour>>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Input shape derived for one operation against one contour.
|
|
69
|
+
*/
|
|
70
|
+
export type DeriveTrailInput<
|
|
71
|
+
TContour extends AnyContour,
|
|
72
|
+
TOperation extends DeriveTrailOperation,
|
|
73
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined =
|
|
74
|
+
| readonly ContourFieldKey<TContour>[]
|
|
75
|
+
| undefined,
|
|
76
|
+
> = TOperation extends 'create'
|
|
77
|
+
? CreateInputOf<TContour, TGenerated>
|
|
78
|
+
: TOperation extends 'read' | 'delete'
|
|
79
|
+
? ReadInputOf<TContour>
|
|
80
|
+
: TOperation extends 'update'
|
|
81
|
+
? UpdateInputOf<TContour, TGenerated>
|
|
82
|
+
: ListInputOf<TContour>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Output shape derived for one operation against one contour.
|
|
86
|
+
*/
|
|
87
|
+
export type DeriveTrailOutput<
|
|
88
|
+
TContour extends AnyContour,
|
|
89
|
+
TOperation extends DeriveTrailOperation,
|
|
90
|
+
> = TOperation extends 'delete'
|
|
91
|
+
? undefined
|
|
92
|
+
: TOperation extends 'list'
|
|
93
|
+
? ContourOutput<TContour>[]
|
|
94
|
+
: ContourOutput<TContour>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extra authored data accepted by `deriveTrail()` in addition to the
|
|
98
|
+
* operation-derived contract pieces.
|
|
99
|
+
*
|
|
100
|
+
* `blaze` is optional for single-resource calls: when omitted, the helper
|
|
101
|
+
* synthesizes a default blaze that delegates to the resource's accessor via
|
|
102
|
+
* the structural {@link StoreAccessorProtocol}. When multiple resources are
|
|
103
|
+
* declared, an explicit `blaze` is required.
|
|
104
|
+
*/
|
|
105
|
+
export interface DeriveTrailSpec<
|
|
106
|
+
TContour extends AnyContour,
|
|
107
|
+
TOperation extends DeriveTrailOperation,
|
|
108
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined =
|
|
109
|
+
| readonly ContourFieldKey<TContour>[]
|
|
110
|
+
| undefined,
|
|
111
|
+
> extends Omit<
|
|
112
|
+
TrailSpec<
|
|
113
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>,
|
|
114
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
115
|
+
>,
|
|
116
|
+
| 'blaze'
|
|
117
|
+
| 'contours'
|
|
118
|
+
| 'examples'
|
|
119
|
+
| 'input'
|
|
120
|
+
| 'intent'
|
|
121
|
+
| 'output'
|
|
122
|
+
| 'resources'
|
|
123
|
+
> {
|
|
124
|
+
/**
|
|
125
|
+
* Implementation of the trail. Optional for single-resource calls: when
|
|
126
|
+
* omitted, the helper derives a default blaze from the resource accessor
|
|
127
|
+
* for standard CRUD operations.
|
|
128
|
+
*/
|
|
129
|
+
readonly blaze?: Implementation<
|
|
130
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>,
|
|
131
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
132
|
+
>;
|
|
133
|
+
/**
|
|
134
|
+
* Server-managed fields that should not be writable through derived create
|
|
135
|
+
* and update inputs.
|
|
136
|
+
*/
|
|
137
|
+
readonly generated?: TGenerated;
|
|
138
|
+
/**
|
|
139
|
+
* Resource dependency declared on the derived trail. Pass a single
|
|
140
|
+
* resource for default-blaze synthesis, or an array for multi-resource
|
|
141
|
+
* trails that must provide an explicit `blaze`.
|
|
142
|
+
*/
|
|
143
|
+
readonly resource: AnyResource | readonly AnyResource[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const operationIntent = {
|
|
147
|
+
create: 'write',
|
|
148
|
+
delete: 'destroy',
|
|
149
|
+
list: 'read',
|
|
150
|
+
read: 'read',
|
|
151
|
+
update: 'write',
|
|
152
|
+
} as const;
|
|
153
|
+
|
|
154
|
+
const describeDeriveTrailResourceDeclaration = (
|
|
155
|
+
resourceCount: number
|
|
156
|
+
): string =>
|
|
157
|
+
resourceCount === 0
|
|
158
|
+
? 'no resources are declared'
|
|
159
|
+
: 'multiple resources are declared';
|
|
160
|
+
|
|
161
|
+
const titleCase = (value: string): string =>
|
|
162
|
+
value.length === 0 ? value : value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
163
|
+
|
|
164
|
+
const uniqueStrings = (
|
|
165
|
+
values: readonly string[] | undefined
|
|
166
|
+
): readonly string[] =>
|
|
167
|
+
Object.freeze([...(values === undefined ? [] : new Set(values))]);
|
|
168
|
+
|
|
169
|
+
const buildFieldMask = (fields: readonly string[]): Record<string, true> =>
|
|
170
|
+
Object.fromEntries(fields.map((field) => [field, true] as const)) as Record<
|
|
171
|
+
string,
|
|
172
|
+
true
|
|
173
|
+
>;
|
|
174
|
+
|
|
175
|
+
type AnyObjectSchema = z.ZodObject<Record<string, z.ZodType>>;
|
|
176
|
+
|
|
177
|
+
const asObjectSchema = (schema: z.ZodType): AnyObjectSchema =>
|
|
178
|
+
schema as unknown as AnyObjectSchema;
|
|
179
|
+
|
|
180
|
+
const unsupportedOperation = (operation: never): never => {
|
|
181
|
+
throw new DerivationError(
|
|
182
|
+
`Unsupported deriveTrail() operation: ${String(operation)}`
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const omitFields = (
|
|
187
|
+
schema: z.ZodType,
|
|
188
|
+
fields: readonly string[]
|
|
189
|
+
): AnyObjectSchema => {
|
|
190
|
+
const objectSchema = asObjectSchema(schema);
|
|
191
|
+
|
|
192
|
+
return fields.length === 0
|
|
193
|
+
? objectSchema
|
|
194
|
+
: (objectSchema.omit(buildFieldMask(fields)) as unknown as AnyObjectSchema);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const pickFields = (
|
|
198
|
+
schema: z.ZodType,
|
|
199
|
+
fields: readonly string[]
|
|
200
|
+
): AnyObjectSchema =>
|
|
201
|
+
asObjectSchema(schema).pick(
|
|
202
|
+
buildFieldMask(fields)
|
|
203
|
+
) as unknown as AnyObjectSchema;
|
|
204
|
+
|
|
205
|
+
const toPartialSchema = (schema: z.ZodType): AnyObjectSchema =>
|
|
206
|
+
asObjectSchema(schema)
|
|
207
|
+
.extend(stripDefaultsFromShape(schema))
|
|
208
|
+
.partial() as unknown as AnyObjectSchema;
|
|
209
|
+
|
|
210
|
+
const normalizeResources = (
|
|
211
|
+
resource: AnyResource | readonly AnyResource[]
|
|
212
|
+
): readonly AnyResource[] =>
|
|
213
|
+
Object.freeze(Array.isArray(resource) ? [...resource] : [resource]);
|
|
214
|
+
|
|
215
|
+
const identityInputSchema = <TContour extends AnyContour>(
|
|
216
|
+
contour: TContour
|
|
217
|
+
): z.ZodType<ReadInputOf<TContour>> =>
|
|
218
|
+
pickFields(contour, [contour.identity]) as unknown as z.ZodType<
|
|
219
|
+
ReadInputOf<TContour>
|
|
220
|
+
>;
|
|
221
|
+
|
|
222
|
+
const createInputSchema = <
|
|
223
|
+
TContour extends AnyContour,
|
|
224
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
|
|
225
|
+
>(
|
|
226
|
+
contour: TContour,
|
|
227
|
+
generated: readonly string[]
|
|
228
|
+
): z.ZodType<CreateInputOf<TContour, TGenerated>> =>
|
|
229
|
+
omitFields(contour, generated) as unknown as z.ZodType<
|
|
230
|
+
CreateInputOf<TContour, TGenerated>
|
|
231
|
+
>;
|
|
232
|
+
|
|
233
|
+
const updateInputSchema = <
|
|
234
|
+
TContour extends AnyContour,
|
|
235
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
|
|
236
|
+
>(
|
|
237
|
+
contour: TContour,
|
|
238
|
+
generated: readonly string[]
|
|
239
|
+
): z.ZodType<UpdateInputOf<TContour, TGenerated>> => {
|
|
240
|
+
const mutableSchema = omitFields(contour, [...generated, contour.identity]);
|
|
241
|
+
const identitySchema = asObjectSchema(identityInputSchema(contour));
|
|
242
|
+
|
|
243
|
+
return identitySchema.extend(
|
|
244
|
+
toPartialSchema(mutableSchema).shape
|
|
245
|
+
) as unknown as z.ZodType<UpdateInputOf<TContour, TGenerated>>;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const listInputSchema = <TContour extends AnyContour>(
|
|
249
|
+
contour: TContour
|
|
250
|
+
): z.ZodType<ListInputOf<TContour>> =>
|
|
251
|
+
toPartialSchema(contour) as unknown as z.ZodType<ListInputOf<TContour>>;
|
|
252
|
+
|
|
253
|
+
const deriveInputSchema = <
|
|
254
|
+
TContour extends AnyContour,
|
|
255
|
+
TOperation extends DeriveTrailOperation,
|
|
256
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
|
|
257
|
+
>(
|
|
258
|
+
contour: TContour,
|
|
259
|
+
operation: TOperation,
|
|
260
|
+
generated: readonly string[]
|
|
261
|
+
): z.ZodType<DeriveTrailInput<TContour, TOperation, TGenerated>> => {
|
|
262
|
+
switch (operation) {
|
|
263
|
+
case 'create': {
|
|
264
|
+
return createInputSchema<TContour, TGenerated>(
|
|
265
|
+
contour,
|
|
266
|
+
generated
|
|
267
|
+
) as z.ZodType<DeriveTrailInput<TContour, TOperation, TGenerated>>;
|
|
268
|
+
}
|
|
269
|
+
case 'read':
|
|
270
|
+
case 'delete': {
|
|
271
|
+
return identityInputSchema(contour) as z.ZodType<
|
|
272
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>
|
|
273
|
+
>;
|
|
274
|
+
}
|
|
275
|
+
case 'update': {
|
|
276
|
+
return updateInputSchema<TContour, TGenerated>(
|
|
277
|
+
contour,
|
|
278
|
+
generated
|
|
279
|
+
) as z.ZodType<DeriveTrailInput<TContour, TOperation, TGenerated>>;
|
|
280
|
+
}
|
|
281
|
+
case 'list': {
|
|
282
|
+
return listInputSchema(contour) as z.ZodType<
|
|
283
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>
|
|
284
|
+
>;
|
|
285
|
+
}
|
|
286
|
+
default: {
|
|
287
|
+
return unsupportedOperation(operation);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const deriveOutputSchema = <
|
|
293
|
+
TContour extends AnyContour,
|
|
294
|
+
TOperation extends DeriveTrailOperation,
|
|
295
|
+
>(
|
|
296
|
+
contour: TContour,
|
|
297
|
+
operation: TOperation
|
|
298
|
+
): z.ZodType<DeriveTrailOutput<TContour, TOperation>> => {
|
|
299
|
+
switch (operation) {
|
|
300
|
+
case 'delete': {
|
|
301
|
+
return z.void() as unknown as z.ZodType<
|
|
302
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
303
|
+
>;
|
|
304
|
+
}
|
|
305
|
+
case 'list': {
|
|
306
|
+
return contour.array() as unknown as z.ZodType<
|
|
307
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
308
|
+
>;
|
|
309
|
+
}
|
|
310
|
+
case 'create':
|
|
311
|
+
case 'read':
|
|
312
|
+
case 'update': {
|
|
313
|
+
return contour as unknown as z.ZodType<
|
|
314
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
315
|
+
>;
|
|
316
|
+
}
|
|
317
|
+
default: {
|
|
318
|
+
return unsupportedOperation(operation);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
type ExampleRecord = Readonly<Record<string, unknown>>;
|
|
324
|
+
|
|
325
|
+
const pickValueFields = (
|
|
326
|
+
example: ExampleRecord,
|
|
327
|
+
fields: readonly string[]
|
|
328
|
+
): Record<string, unknown> =>
|
|
329
|
+
Object.fromEntries(
|
|
330
|
+
fields.flatMap((field) =>
|
|
331
|
+
Object.hasOwn(example, field) ? [[field, example[field]]] : []
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const omitValueFields = (
|
|
336
|
+
example: ExampleRecord,
|
|
337
|
+
fields: readonly string[]
|
|
338
|
+
): Record<string, unknown> => {
|
|
339
|
+
const omitted = new Set(fields);
|
|
340
|
+
|
|
341
|
+
return Object.fromEntries(
|
|
342
|
+
Object.entries(example).filter(([field]) => !omitted.has(field))
|
|
343
|
+
);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const formatExampleName = (
|
|
347
|
+
contour: AnyContour,
|
|
348
|
+
operation: DeriveTrailOperation,
|
|
349
|
+
example: ExampleRecord,
|
|
350
|
+
index: number
|
|
351
|
+
): string => {
|
|
352
|
+
const identifier = example[contour.identity];
|
|
353
|
+
const suffix =
|
|
354
|
+
identifier === undefined ? String(index + 1) : String(identifier);
|
|
355
|
+
return `${titleCase(operation)} ${contour.name} ${suffix}`;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Derive a single trail example from a contour fixture.
|
|
360
|
+
*
|
|
361
|
+
* @remarks
|
|
362
|
+
* For `list` operations, each derived example wraps a single fixture in an
|
|
363
|
+
* array (`expected: [example]`) and uses the fixture's identity as input
|
|
364
|
+
* filters. This means the expected output is always a one-element array,
|
|
365
|
+
* which may not match the real accessor behavior when multiple fixtures
|
|
366
|
+
* share the same filter. A custom `blaze` with hand-authored examples is
|
|
367
|
+
* required for multi-result list assertions.
|
|
368
|
+
*/
|
|
369
|
+
const deriveExample = (
|
|
370
|
+
contour: AnyContour,
|
|
371
|
+
operation: DeriveTrailOperation,
|
|
372
|
+
example: ExampleRecord,
|
|
373
|
+
index: number,
|
|
374
|
+
generated: readonly string[]
|
|
375
|
+
): TrailExample<unknown, unknown> => {
|
|
376
|
+
const name = formatExampleName(contour, operation, example, index);
|
|
377
|
+
const identity = pickValueFields(example, [contour.identity]);
|
|
378
|
+
|
|
379
|
+
switch (operation) {
|
|
380
|
+
case 'create': {
|
|
381
|
+
return {
|
|
382
|
+
expected: example,
|
|
383
|
+
input: omitValueFields(example, generated),
|
|
384
|
+
name,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
case 'read': {
|
|
388
|
+
return {
|
|
389
|
+
expected: example,
|
|
390
|
+
input: identity,
|
|
391
|
+
name,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
case 'update': {
|
|
395
|
+
return {
|
|
396
|
+
expected: example,
|
|
397
|
+
input: {
|
|
398
|
+
...omitValueFields(example, [...generated, contour.identity]),
|
|
399
|
+
...identity,
|
|
400
|
+
},
|
|
401
|
+
name,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
case 'delete': {
|
|
405
|
+
return {
|
|
406
|
+
input: identity,
|
|
407
|
+
name,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
case 'list': {
|
|
411
|
+
return {
|
|
412
|
+
expected: [example],
|
|
413
|
+
input: {},
|
|
414
|
+
name,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
default: {
|
|
418
|
+
return unsupportedOperation(operation);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const deriveExamples = (
|
|
424
|
+
contour: AnyContour,
|
|
425
|
+
operation: DeriveTrailOperation,
|
|
426
|
+
generated: readonly string[]
|
|
427
|
+
): readonly TrailExample<unknown, unknown>[] | undefined => {
|
|
428
|
+
if (contour.examples === undefined || contour.examples.length === 0) {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// List examples use a single example with all fixtures in expected,
|
|
433
|
+
// since input: {} returns the full set from a seeded mock. Expected
|
|
434
|
+
// values keep generated fields (createdAt, etc.) because the mock
|
|
435
|
+
// populates them — stripping them would fail output schema validation.
|
|
436
|
+
if (operation === 'list') {
|
|
437
|
+
return Object.freeze([
|
|
438
|
+
{
|
|
439
|
+
expected: contour.examples,
|
|
440
|
+
input: {},
|
|
441
|
+
name: `${contour.name} list example`,
|
|
442
|
+
},
|
|
443
|
+
]);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return Object.freeze(
|
|
447
|
+
contour.examples.map((example, index) =>
|
|
448
|
+
deriveExample(
|
|
449
|
+
contour,
|
|
450
|
+
operation,
|
|
451
|
+
example as ExampleRecord,
|
|
452
|
+
index,
|
|
453
|
+
generated
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
// Default-blaze synthesis
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
type GenericAccessor = StoreAccessorProtocol<
|
|
464
|
+
unknown,
|
|
465
|
+
unknown,
|
|
466
|
+
unknown,
|
|
467
|
+
unknown
|
|
468
|
+
>;
|
|
469
|
+
|
|
470
|
+
const wrapUnexpected = (
|
|
471
|
+
contourName: string,
|
|
472
|
+
operation: DeriveTrailOperation,
|
|
473
|
+
error: unknown
|
|
474
|
+
): Error => {
|
|
475
|
+
if (isTrailsError(error)) {
|
|
476
|
+
return error;
|
|
477
|
+
}
|
|
478
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
479
|
+
return new InternalError(
|
|
480
|
+
`deriveTrail("${contourName}.${operation}") synthesized blaze failed: ${cause.message}`,
|
|
481
|
+
{ cause }
|
|
482
|
+
);
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const notFoundError = (contourName: string, id: unknown): NotFoundError =>
|
|
486
|
+
new NotFoundError(
|
|
487
|
+
`deriveTrail("${contourName}"): entity "${String(id)}" not found`
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const resolveAccessor = (
|
|
491
|
+
contour: AnyContour,
|
|
492
|
+
operation: DeriveTrailOperation,
|
|
493
|
+
resource: AnyResource,
|
|
494
|
+
ctx: TrailContext
|
|
495
|
+
): GenericAccessor | Error => {
|
|
496
|
+
try {
|
|
497
|
+
const connection = resource.from(ctx) as
|
|
498
|
+
| Readonly<Record<string, GenericAccessor>>
|
|
499
|
+
| undefined;
|
|
500
|
+
if (connection === undefined || connection === null) {
|
|
501
|
+
return new InternalError(
|
|
502
|
+
`deriveTrail("${contour.name}.${operation}"): resource "${resource.id}" produced no connection`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
const accessor = connection[contour.name];
|
|
506
|
+
if (accessor === undefined) {
|
|
507
|
+
return new InternalError(
|
|
508
|
+
`deriveTrail("${contour.name}.${operation}"): resource "${resource.id}" does not expose an accessor for "${contour.name}"`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
return accessor;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
return wrapUnexpected(contour.name, operation, error);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const extractIdentity = (contour: AnyContour, input: unknown): unknown => {
|
|
518
|
+
const record = input as Record<string, unknown>;
|
|
519
|
+
return record[contour.identity];
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const callRead = async (
|
|
523
|
+
contour: AnyContour,
|
|
524
|
+
accessor: GenericAccessor,
|
|
525
|
+
input: unknown
|
|
526
|
+
): Promise<Result<unknown, Error>> => {
|
|
527
|
+
if (typeof accessor.get !== 'function') {
|
|
528
|
+
return Result.err(
|
|
529
|
+
new InternalError(
|
|
530
|
+
`deriveTrail("${contour.name}.read"): accessor is missing a \`get\` method`
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const id = extractIdentity(contour, input);
|
|
536
|
+
const entity = await accessor.get(id);
|
|
537
|
+
if (entity === null || entity === undefined) {
|
|
538
|
+
return Result.err(notFoundError(contour.name, id));
|
|
539
|
+
}
|
|
540
|
+
return Result.ok(entity);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
return Result.err(wrapUnexpected(contour.name, 'read', error));
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const callCreate = async (
|
|
547
|
+
contour: AnyContour,
|
|
548
|
+
accessor: GenericAccessor,
|
|
549
|
+
input: unknown,
|
|
550
|
+
ctx: TrailContext
|
|
551
|
+
): Promise<Result<unknown, Error>> => {
|
|
552
|
+
try {
|
|
553
|
+
if (typeof accessor.insert === 'function') {
|
|
554
|
+
const created = await accessor.insert(input);
|
|
555
|
+
return Result.ok(created);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Fallback: tabular contract allows `upsert` when `insert` is absent.
|
|
559
|
+
// The warden flags this at build time via a pattern rule (trl-251).
|
|
560
|
+
if (typeof accessor.upsert !== 'function') {
|
|
561
|
+
return Result.err(
|
|
562
|
+
new InternalError(
|
|
563
|
+
`deriveTrail("${contour.name}.create"): accessor is missing both \`insert\` and \`upsert\``
|
|
564
|
+
)
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
ctx.logger?.debug(
|
|
568
|
+
`deriveTrail("${contour.name}.create"): accessor has no \`insert\`; falling back to \`upsert\``
|
|
569
|
+
);
|
|
570
|
+
const created = await accessor.upsert(input);
|
|
571
|
+
return Result.ok(created);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
return Result.err(wrapUnexpected(contour.name, 'create', error));
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Strip framework-managed generated fields from a merged payload so that
|
|
579
|
+
* the update-via-upsert fallback doesn't carry stale managed values.
|
|
580
|
+
*
|
|
581
|
+
* Only strips fields that appear in the `generated` array — user-defined
|
|
582
|
+
* fields with the same name (e.g. an API `version` string) are preserved.
|
|
583
|
+
*/
|
|
584
|
+
const stripGeneratedFields = (
|
|
585
|
+
payload: Record<string, unknown>,
|
|
586
|
+
generated: readonly string[],
|
|
587
|
+
identity: string
|
|
588
|
+
): Record<string, unknown> => {
|
|
589
|
+
if (generated.length === 0) {
|
|
590
|
+
return payload;
|
|
591
|
+
}
|
|
592
|
+
const managedKeys = new Set(generated);
|
|
593
|
+
return Object.fromEntries(
|
|
594
|
+
Object.entries(payload).filter(
|
|
595
|
+
([key]) => key === identity || !managedKeys.has(key)
|
|
596
|
+
)
|
|
597
|
+
);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Fallback for accessors that lack a native `update`: read the current
|
|
602
|
+
* entity, merge the patch, strip any `version` field so versioned tables
|
|
603
|
+
* keep `update`'s "does not participate in optimistic concurrency" semantic,
|
|
604
|
+
* then `upsert`.
|
|
605
|
+
*/
|
|
606
|
+
const updateViaReadAndUpsert = async (
|
|
607
|
+
contour: AnyContour,
|
|
608
|
+
accessor: GenericAccessor,
|
|
609
|
+
id: unknown,
|
|
610
|
+
patch: Record<string, unknown>,
|
|
611
|
+
generated: readonly string[]
|
|
612
|
+
): Promise<Result<unknown, Error>> => {
|
|
613
|
+
if (typeof accessor.get !== 'function') {
|
|
614
|
+
return Result.err(
|
|
615
|
+
new InternalError(
|
|
616
|
+
`deriveTrail("${contour.name}.update"): accessor is missing both \`update\` and \`get\``
|
|
617
|
+
)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (typeof accessor.upsert !== 'function') {
|
|
621
|
+
return Result.err(
|
|
622
|
+
new InternalError(
|
|
623
|
+
`deriveTrail("${contour.name}.update"): accessor is missing both \`update\` and \`upsert\``
|
|
624
|
+
)
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
const current = await accessor.get(id);
|
|
628
|
+
if (current === null || current === undefined) {
|
|
629
|
+
return Result.err(notFoundError(contour.name, id));
|
|
630
|
+
}
|
|
631
|
+
const merged = stripGeneratedFields(
|
|
632
|
+
{ ...(current as Record<string, unknown>), ...patch },
|
|
633
|
+
generated,
|
|
634
|
+
contour.identity
|
|
635
|
+
);
|
|
636
|
+
const updated = await accessor.upsert(merged);
|
|
637
|
+
return Result.ok(updated);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const callUpdate = async (
|
|
641
|
+
contour: AnyContour,
|
|
642
|
+
accessor: GenericAccessor,
|
|
643
|
+
input: unknown,
|
|
644
|
+
generated: readonly string[]
|
|
645
|
+
): Promise<Result<unknown, Error>> => {
|
|
646
|
+
const id = extractIdentity(contour, input);
|
|
647
|
+
const patch = Object.fromEntries(
|
|
648
|
+
Object.entries(input as Record<string, unknown>).filter(
|
|
649
|
+
([field]) => field !== contour.identity
|
|
650
|
+
)
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
if (typeof accessor.update === 'function') {
|
|
655
|
+
const updated = await accessor.update(id, patch);
|
|
656
|
+
if (updated === null || updated === undefined) {
|
|
657
|
+
return Result.err(notFoundError(contour.name, id));
|
|
658
|
+
}
|
|
659
|
+
return Result.ok(updated);
|
|
660
|
+
}
|
|
661
|
+
return await updateViaReadAndUpsert(
|
|
662
|
+
contour,
|
|
663
|
+
accessor,
|
|
664
|
+
id,
|
|
665
|
+
patch,
|
|
666
|
+
generated
|
|
667
|
+
);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
return Result.err(wrapUnexpected(contour.name, 'update', error));
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const callDelete = async (
|
|
674
|
+
contour: AnyContour,
|
|
675
|
+
accessor: GenericAccessor,
|
|
676
|
+
input: unknown
|
|
677
|
+
): Promise<Result<undefined, Error>> => {
|
|
678
|
+
if (typeof accessor.remove !== 'function') {
|
|
679
|
+
return Result.err(
|
|
680
|
+
new InternalError(
|
|
681
|
+
`deriveTrail("${contour.name}.delete"): accessor is missing a \`remove\` method`
|
|
682
|
+
)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const id = extractIdentity(contour, input);
|
|
687
|
+
await accessor.remove(id);
|
|
688
|
+
// `{ deleted: false }` is a no-op on an absent row, not an error —
|
|
689
|
+
// matches the accessor's documented semantic.
|
|
690
|
+
return Result.ok();
|
|
691
|
+
} catch (error) {
|
|
692
|
+
return Result.err(wrapUnexpected(contour.name, 'delete', error));
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Default `list` synthesis passes the entire input as the filter bag. The
|
|
698
|
+
* derived input type is `Partial<ContourInput>` which matches the accessor's
|
|
699
|
+
* filter shape field-for-field. Pagination controls are not derived — callers
|
|
700
|
+
* that need pagination must provide an explicit blaze.
|
|
701
|
+
*/
|
|
702
|
+
const callList = async (
|
|
703
|
+
contour: AnyContour,
|
|
704
|
+
accessor: GenericAccessor,
|
|
705
|
+
input: unknown
|
|
706
|
+
): Promise<Result<unknown[], Error>> => {
|
|
707
|
+
if (typeof accessor.list !== 'function') {
|
|
708
|
+
return Result.err(
|
|
709
|
+
new InternalError(
|
|
710
|
+
`deriveTrail("${contour.name}.list"): accessor is missing a \`list\` method`
|
|
711
|
+
)
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
const listed = await accessor.list(input);
|
|
716
|
+
return Result.ok([...listed]);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
return Result.err(wrapUnexpected(contour.name, 'list', error));
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
const synthesizeDefaultBlaze = <
|
|
723
|
+
TContour extends AnyContour,
|
|
724
|
+
TOperation extends DeriveTrailOperation,
|
|
725
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined,
|
|
726
|
+
>(
|
|
727
|
+
contour: TContour,
|
|
728
|
+
operation: TOperation,
|
|
729
|
+
resource: AnyResource,
|
|
730
|
+
generated: readonly string[]
|
|
731
|
+
): Implementation<
|
|
732
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>,
|
|
733
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
734
|
+
> => {
|
|
735
|
+
const impl: Implementation<unknown, unknown> = (input, ctx) => {
|
|
736
|
+
const accessor = resolveAccessor(contour, operation, resource, ctx);
|
|
737
|
+
if (accessor instanceof Error) {
|
|
738
|
+
return Promise.resolve(Result.err(accessor));
|
|
739
|
+
}
|
|
740
|
+
switch (operation) {
|
|
741
|
+
case 'create': {
|
|
742
|
+
return callCreate(contour, accessor, input, ctx);
|
|
743
|
+
}
|
|
744
|
+
case 'read': {
|
|
745
|
+
return callRead(contour, accessor, input);
|
|
746
|
+
}
|
|
747
|
+
case 'update': {
|
|
748
|
+
return callUpdate(contour, accessor, input, generated);
|
|
749
|
+
}
|
|
750
|
+
case 'delete': {
|
|
751
|
+
return callDelete(contour, accessor, input);
|
|
752
|
+
}
|
|
753
|
+
case 'list': {
|
|
754
|
+
return callList(contour, accessor, input);
|
|
755
|
+
}
|
|
756
|
+
default: {
|
|
757
|
+
return unsupportedOperation(operation);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
return impl as Implementation<
|
|
763
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>,
|
|
764
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
765
|
+
>;
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Mechanically project one CRUD-shaped trail from a contour declaration.
|
|
770
|
+
*
|
|
771
|
+
* When `spec.blaze` is omitted and the call declares a single resource, the
|
|
772
|
+
* helper derives a default blaze that dispatches to the resource accessor
|
|
773
|
+
* through the structural {@link StoreAccessorProtocol}. Multi-resource calls
|
|
774
|
+
* must supply an explicit blaze and are rejected with {@link DerivationError}
|
|
775
|
+
* at construction time when they do not.
|
|
776
|
+
*/
|
|
777
|
+
export const deriveTrail = <
|
|
778
|
+
TContour extends AnyContour,
|
|
779
|
+
TOperation extends DeriveTrailOperation,
|
|
780
|
+
TGenerated extends readonly ContourFieldKey<TContour>[] | undefined =
|
|
781
|
+
| readonly ContourFieldKey<TContour>[]
|
|
782
|
+
| undefined,
|
|
783
|
+
>(
|
|
784
|
+
contour: TContour,
|
|
785
|
+
operation: TOperation,
|
|
786
|
+
spec: DeriveTrailSpec<TContour, TOperation, TGenerated>
|
|
787
|
+
): Trail<
|
|
788
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>,
|
|
789
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
790
|
+
> => {
|
|
791
|
+
const resources = normalizeResources(spec.resource);
|
|
792
|
+
const generated = uniqueStrings(
|
|
793
|
+
spec.generated as readonly string[] | undefined
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
let blaze: Implementation<
|
|
797
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>,
|
|
798
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
799
|
+
>;
|
|
800
|
+
if (typeof spec.blaze === 'function') {
|
|
801
|
+
({ blaze } = spec);
|
|
802
|
+
} else if (resources.length === 1) {
|
|
803
|
+
blaze = synthesizeDefaultBlaze<TContour, TOperation, TGenerated>(
|
|
804
|
+
contour,
|
|
805
|
+
operation,
|
|
806
|
+
resources[0] as AnyResource,
|
|
807
|
+
generated
|
|
808
|
+
);
|
|
809
|
+
} else {
|
|
810
|
+
throw new DerivationError(
|
|
811
|
+
`deriveTrail("${contour.name}.${operation}") requires an explicit \`blaze\` when ${describeDeriveTrailResourceDeclaration(resources.length)} — default synthesis is single-resource only`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
const {
|
|
815
|
+
blaze: _blaze,
|
|
816
|
+
resource: _resource,
|
|
817
|
+
generated: _generated,
|
|
818
|
+
...trailSpec
|
|
819
|
+
} = spec;
|
|
820
|
+
const derivedSpec = {
|
|
821
|
+
...trailSpec,
|
|
822
|
+
blaze,
|
|
823
|
+
contours: [contour],
|
|
824
|
+
examples: deriveExamples(contour, operation, generated),
|
|
825
|
+
input: deriveInputSchema<TContour, TOperation, TGenerated>(
|
|
826
|
+
contour,
|
|
827
|
+
operation,
|
|
828
|
+
generated
|
|
829
|
+
),
|
|
830
|
+
intent: operationIntent[operation],
|
|
831
|
+
output: deriveOutputSchema(contour, operation),
|
|
832
|
+
resources,
|
|
833
|
+
} as unknown as TrailSpec<
|
|
834
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>,
|
|
835
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
836
|
+
>;
|
|
837
|
+
|
|
838
|
+
return trail(`${contour.name}.${operation}`, derivedSpec) as unknown as Trail<
|
|
839
|
+
DeriveTrailInput<TContour, TOperation, TGenerated>,
|
|
840
|
+
DeriveTrailOutput<TContour, TOperation>
|
|
841
|
+
>;
|
|
842
|
+
};
|