@ontrails/http 1.0.0-beta.15 → 1.0.0-beta.16

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/src/index.ts CHANGED
@@ -2,7 +2,21 @@
2
2
  export {
3
3
  deriveHttpRoutes,
4
4
  type DeriveHttpRoutesOptions,
5
- type HttpMethod,
5
+ type HttpExecutionContext,
6
+ type HttpHeaderSource,
7
+ type HttpLayerInputProjection,
6
8
  type HttpRouteDefinition,
7
- type InputSource,
9
+ type ResolveHttpPermit,
10
+ type ResolveHttpPermitInput,
8
11
  } from './build.js';
12
+ export {
13
+ deriveHttpInputSource,
14
+ deriveHttpMethod,
15
+ deriveHttpOperationMethod,
16
+ httpMethodByIntent,
17
+ } from './method.js';
18
+ export type { HttpMethod, HttpOperationMethod, InputSource } from './method.js';
19
+
20
+ // OpenAPI
21
+ export { deriveOpenApiSpec } from './openapi.js';
22
+ export type { OpenApiOptions, OpenApiSpec, OpenApiServer } from './openapi.js';
package/src/method.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { Intent, WebhookMethod } from '@ontrails/core';
2
+
3
+ export type HttpMethod = 'GET' | 'POST' | 'DELETE' | WebhookMethod;
4
+
5
+ export type HttpOperationMethod = Lowercase<HttpMethod>;
6
+
7
+ export type InputSource = 'query' | 'body' | 'webhook';
8
+
9
+ export const httpMethodByIntent = {
10
+ destroy: 'DELETE',
11
+ read: 'GET',
12
+ write: 'POST',
13
+ } as const satisfies Record<Intent, HttpMethod>;
14
+
15
+ export const deriveHttpMethod = (intent: Intent): HttpMethod =>
16
+ (httpMethodByIntent as Partial<Record<string, HttpMethod>>)[intent] ?? 'POST';
17
+
18
+ export const deriveHttpOperationMethod = (
19
+ intent: Intent
20
+ ): HttpOperationMethod =>
21
+ deriveHttpMethod(intent).toLowerCase() as HttpOperationMethod;
22
+
23
+ export const deriveHttpInputSource = (method: HttpMethod): InputSource =>
24
+ method === 'GET' ? 'query' : 'body';
package/src/openapi.ts ADDED
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Derive an OpenAPI 3.1 specification from a Topo.
3
+ *
4
+ * Converts each trail into an HTTP operation, deriving paths, methods,
5
+ * parameters, and response schemas from the trail contract.
6
+ */
7
+
8
+ import {
9
+ ValidationError,
10
+ filterSurfaceTrails,
11
+ projectErrorClassSurface,
12
+ validateSurfaceTopo,
13
+ zodToJsonSchema,
14
+ } from '@ontrails/core';
15
+ import type {
16
+ SurfaceSelectionOptions,
17
+ SurfaceValidationOptions,
18
+ Topo,
19
+ Trail,
20
+ } from '@ontrails/core';
21
+
22
+ import { deriveHttpOperationMethod } from './method.js';
23
+
24
+ type JsonSchema = Readonly<Record<string, unknown>>;
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Public types
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export interface OpenApiServer {
31
+ readonly url: string;
32
+ readonly description?: string | undefined;
33
+ }
34
+
35
+ export interface OpenApiOptions
36
+ extends SurfaceSelectionOptions, SurfaceValidationOptions {
37
+ /** Default: `graph.name` */
38
+ readonly title?: string | undefined;
39
+ /** Default: `'1.0.0'` */
40
+ readonly version?: string | undefined;
41
+ readonly description?: string | undefined;
42
+ readonly servers?: readonly OpenApiServer[] | undefined;
43
+ /** Prefix for all paths. Default: `''` */
44
+ readonly basePath?: string | undefined;
45
+ }
46
+
47
+ /** Minimal OpenAPI 3.1 spec shape — intentionally plain objects, no heavy library. */
48
+ export interface OpenApiSpec {
49
+ readonly openapi: '3.1.0';
50
+ readonly info: {
51
+ readonly title: string;
52
+ readonly version: string;
53
+ readonly description?: string | undefined;
54
+ };
55
+ readonly servers?: readonly OpenApiServer[] | undefined;
56
+ readonly paths: Record<string, Record<string, unknown>>;
57
+ readonly components: { readonly schemas: Record<string, unknown> };
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /** `entity.show` → `/entity/show` */
65
+ const trailIdToPath = (id: string, basePath: string): string =>
66
+ `${basePath}/${id.split('.').join('/')}`;
67
+
68
+ /** First segment of a dotted ID, used as an OpenAPI tag. */
69
+ const tagFromId = (id: string): string => id.split('.')[0] ?? id;
70
+
71
+ /** Convert a Zod schema to JSON Schema via the core helper. */
72
+ const toJsonSchema = (schema: unknown): JsonSchema =>
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ zodToJsonSchema(schema as any) as JsonSchema;
75
+
76
+ /** Build query parameters from a JSON Schema `properties` object. */
77
+ const buildQueryParameters = (
78
+ jsonSchema: JsonSchema
79
+ ): Record<string, unknown>[] => {
80
+ const properties = jsonSchema['properties'] as
81
+ | Record<string, Record<string, unknown>>
82
+ | undefined;
83
+ if (!properties) {
84
+ return [];
85
+ }
86
+
87
+ const required = new Set(
88
+ Array.isArray(jsonSchema['required'])
89
+ ? (jsonSchema['required'] as string[])
90
+ : []
91
+ );
92
+
93
+ return Object.entries(properties).map(([name, schema]) => ({
94
+ in: 'query',
95
+ name,
96
+ required: required.has(name),
97
+ schema,
98
+ }));
99
+ };
100
+
101
+ /** Map a single error example to a status code entry, or undefined if not mappable. */
102
+ const errorExampleToEntry = (
103
+ errorName: string,
104
+ seen: Set<number>
105
+ ): [string, { description: string }] | undefined => {
106
+ const projection = projectErrorClassSurface('http', errorName);
107
+ if (projection === undefined) {
108
+ return undefined;
109
+ }
110
+
111
+ const { code } = projection;
112
+ if (seen.has(code)) {
113
+ return undefined;
114
+ }
115
+ seen.add(code);
116
+ return [String(code), { description: errorName }];
117
+ };
118
+
119
+ /** Extract error status codes from trail examples that have an `error` field. */
120
+ const errorResponsesFromExamples = (
121
+ examples: readonly { error?: string | undefined }[]
122
+ ): Record<string, { description: string }> => {
123
+ const responses: Record<string, { description: string }> = {};
124
+ const seen = new Set<number>();
125
+
126
+ for (const ex of examples) {
127
+ if (!ex.error) {
128
+ continue;
129
+ }
130
+ const entry = errorExampleToEntry(ex.error, seen);
131
+ if (entry) {
132
+ const [code, value] = entry;
133
+ responses[code] = value;
134
+ }
135
+ }
136
+
137
+ return responses;
138
+ };
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Operation builder — split into focused helpers
142
+ // ---------------------------------------------------------------------------
143
+
144
+ /** True when the body is required — non-object schemas are always required,
145
+ * object schemas are required only when they have at least one required property. */
146
+ const isBodyRequired = (schema: JsonSchema): boolean => {
147
+ if (schema['type'] !== 'object') {
148
+ return true;
149
+ }
150
+ return (
151
+ Array.isArray(schema['required']) &&
152
+ (schema['required'] as unknown[]).length > 0
153
+ );
154
+ };
155
+
156
+ /** Build the input portion of an operation (parameters or requestBody). */
157
+ const buildInputSpec = (
158
+ t: Trail<unknown, unknown, unknown>,
159
+ method: string
160
+ ): Record<string, unknown> => {
161
+ if (!t.input) {
162
+ return {};
163
+ }
164
+ const inputSchema = toJsonSchema(t.input);
165
+
166
+ if (method === 'get') {
167
+ const params = buildQueryParameters(inputSchema);
168
+ return params.length > 0 ? { parameters: params } : {};
169
+ }
170
+
171
+ const properties = inputSchema['properties'] as
172
+ | Record<string, unknown>
173
+ | undefined;
174
+ if (properties !== undefined && Object.keys(properties).length === 0) {
175
+ return {};
176
+ }
177
+
178
+ return {
179
+ requestBody: {
180
+ content: { 'application/json': { schema: inputSchema } },
181
+ required: isBodyRequired(inputSchema),
182
+ },
183
+ };
184
+ };
185
+
186
+ /** Wrap a raw output schema in the `{ data: ... }` envelope the HTTP adapter uses. */
187
+ const wrapInDataEnvelope = (outputSchema: JsonSchema): JsonSchema => ({
188
+ properties: { data: outputSchema },
189
+ required: ['data'],
190
+ type: 'object',
191
+ });
192
+
193
+ /** Shared error response body schema: `{ error: { message, code, category } }`. */
194
+ const errorResponseSchema: JsonSchema = {
195
+ properties: {
196
+ error: {
197
+ properties: {
198
+ category: { type: 'string' },
199
+ code: { type: 'string' },
200
+ message: { type: 'string' },
201
+ },
202
+ required: ['message', 'code', 'category'],
203
+ type: 'object',
204
+ },
205
+ },
206
+ required: ['error'],
207
+ type: 'object',
208
+ };
209
+
210
+ /** Build the 200 response entry. */
211
+ const buildSuccessResponse = (
212
+ t: Trail<unknown, unknown, unknown>
213
+ ): Record<string, unknown> => {
214
+ if (!t.output) {
215
+ return { '200': { description: 'Success' } };
216
+ }
217
+ const outputSchema = toJsonSchema(t.output);
218
+ return {
219
+ '200': {
220
+ content: {
221
+ 'application/json': { schema: wrapInDataEnvelope(outputSchema) },
222
+ },
223
+ description: 'Success',
224
+ },
225
+ };
226
+ };
227
+
228
+ /** Build the default 400 validation error response. */
229
+ const validationErrorResponse: Record<
230
+ string,
231
+ { content: Record<string, unknown>; description: string }
232
+ > = {
233
+ '400': {
234
+ content: { 'application/json': { schema: errorResponseSchema } },
235
+ description: 'Validation error',
236
+ },
237
+ };
238
+
239
+ /** Build all responses (success + error) for a trail. */
240
+ const buildResponses = (
241
+ t: Trail<unknown, unknown, unknown>
242
+ ): Record<string, unknown> => {
243
+ const examples = (t.examples ?? []) as readonly {
244
+ error?: string | undefined;
245
+ }[];
246
+ return {
247
+ ...buildSuccessResponse(t),
248
+ ...errorResponsesFromExamples(examples),
249
+ ...validationErrorResponse,
250
+ };
251
+ };
252
+
253
+ const operationIdFromTrailId = (trailId: string): string =>
254
+ trailId.replaceAll('.', '_');
255
+
256
+ /** Build a complete OpenAPI operation for a trail. */
257
+ const buildOperation = (
258
+ t: Trail<unknown, unknown, unknown>,
259
+ method: string
260
+ ): Record<string, unknown> => ({
261
+ operationId: operationIdFromTrailId(t.id),
262
+ responses: buildResponses(t),
263
+ tags: [tagFromId(t.id)],
264
+ ...(t.description ? { summary: t.description } : {}),
265
+ ...buildInputSpec(t, method),
266
+ });
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Path collection
270
+ // ---------------------------------------------------------------------------
271
+
272
+ /** Collect all paths from public trails in the graph. */
273
+ const collectPaths = (
274
+ graph: Topo,
275
+ basePath: string,
276
+ options?: OpenApiOptions
277
+ ): Record<string, Record<string, unknown>> => {
278
+ const paths: Record<string, Record<string, unknown>> = {};
279
+ const seenRoutes = new Map<string, string>();
280
+ const seenOperationIds = new Map<string, string>();
281
+
282
+ for (const t of filterSurfaceTrails(graph.list(), {
283
+ exclude: options?.exclude,
284
+ include: options?.include,
285
+ intent: options?.intent,
286
+ })) {
287
+ const method = deriveHttpOperationMethod(t.intent);
288
+ const path = trailIdToPath(t.id, basePath);
289
+ const routeKey = `${method.toUpperCase()} ${path}`;
290
+ const existingId = seenRoutes.get(routeKey);
291
+ if (existingId !== undefined) {
292
+ throw new ValidationError(
293
+ `HTTP route collision: trails "${existingId}" and "${t.id}" both derive ${routeKey}`
294
+ );
295
+ }
296
+ seenRoutes.set(routeKey, t.id);
297
+
298
+ const operationId = operationIdFromTrailId(t.id);
299
+ const existingOperationId = seenOperationIds.get(operationId);
300
+ if (existingOperationId !== undefined) {
301
+ throw new ValidationError(
302
+ `OpenAPI operationId collision: trails "${existingOperationId}" and "${t.id}" both derive operationId "${operationId}"`
303
+ );
304
+ }
305
+ seenOperationIds.set(operationId, t.id);
306
+
307
+ paths[path] ??= {};
308
+ paths[path][method] = buildOperation(t, method);
309
+ }
310
+
311
+ return paths;
312
+ };
313
+
314
+ /** Build the info object from options and graph name. */
315
+ const buildInfo = (
316
+ graphName: string,
317
+ options?: OpenApiOptions
318
+ ): OpenApiSpec['info'] => ({
319
+ title: options?.title ?? graphName,
320
+ version: options?.version ?? '1.0.0',
321
+ ...(options?.description ? { description: options.description } : {}),
322
+ });
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Public API
326
+ // ---------------------------------------------------------------------------
327
+
328
+ /**
329
+ * Derive an OpenAPI 3.1 specification from a Topo.
330
+ *
331
+ * Iterates all trails, skipping signals and internal trails, and produces
332
+ * paths, operations, parameters, and response schemas derived from
333
+ * the trail contract.
334
+ */
335
+ export const deriveOpenApiSpec = (
336
+ graph: Topo,
337
+ options?: OpenApiOptions
338
+ ): OpenApiSpec => {
339
+ const validated = validateSurfaceTopo(graph, options);
340
+ if (validated.isErr()) {
341
+ throw validated.error;
342
+ }
343
+
344
+ return {
345
+ components: { schemas: {} },
346
+ info: buildInfo(graph.name, options),
347
+ openapi: '3.1.0',
348
+ paths: collectPaths(
349
+ graph,
350
+ (options?.basePath ?? '').replace(/\/+$/, ''),
351
+ options
352
+ ),
353
+ ...(options?.servers && options.servers.length > 0
354
+ ? { servers: options.servers }
355
+ : {}),
356
+ };
357
+ };
@@ -1 +0,0 @@
1
- $ tsc -b
@@ -1,3 +0,0 @@
1
- $ oxlint ./src
2
- Found 0 warnings and 0 errors.
3
- Finished in 67ms on 4 files with 93 rules using 24 threads.
@@ -1 +0,0 @@
1
- $ tsc --noEmit
package/dist/build.d.ts DELETED
@@ -1,57 +0,0 @@
1
- /**
2
- * Build framework-agnostic HTTP route definitions from a Trails topo.
3
- *
4
- * Each route definition describes the path, method, input source, and an
5
- * `execute` function that validates input, composes layers, and runs the
6
- * implementation -- all without referencing any HTTP framework types.
7
- */
8
- import { Result } from '@ontrails/core';
9
- import type { Intent, Layer, ResourceOverrideMap, Topo, Trail, TrailContextInit } from '@ontrails/core';
10
- export interface DeriveHttpRoutesOptions {
11
- readonly basePath?: string | undefined;
12
- /** Config values for resources that declare a `config` schema, keyed by resource ID. */
13
- readonly configValues?: Readonly<Record<string, Record<string, unknown>>> | undefined;
14
- readonly createContext?: (() => TrailContextInit | Promise<TrailContextInit>) | undefined;
15
- readonly exclude?: readonly string[] | undefined;
16
- readonly include?: readonly string[] | undefined;
17
- readonly intent?: readonly Intent[] | undefined;
18
- readonly layers?: readonly Layer[] | undefined;
19
- readonly resources?: ResourceOverrideMap | undefined;
20
- /** Set to `false` to skip topo validation while building routes. */
21
- readonly validate?: boolean | undefined;
22
- }
23
- export type HttpMethod = 'GET' | 'POST' | 'DELETE';
24
- /** Input source derived from the HTTP method. */
25
- export type InputSource = 'query' | 'body';
26
- export interface HttpRouteDefinition {
27
- readonly method: HttpMethod;
28
- readonly path: string;
29
- readonly trailId: string;
30
- readonly inputSource: InputSource;
31
- readonly trail: Trail<unknown, unknown, unknown>;
32
- /**
33
- * Validate input, compose layers, and execute the trail implementation.
34
- *
35
- * The caller is responsible for parsing raw input from the request and
36
- * mapping the Result to an HTTP response. This function is framework-agnostic.
37
- *
38
- * @param abortSignal - Optional AbortSignal from the HTTP request. When provided,
39
- * it takes final precedence over any context factory signal, allowing
40
- * client-initiated cancellation to propagate into trail execution.
41
- */
42
- readonly execute: (input: unknown, requestId?: string | undefined, abortSignal?: AbortSignal | undefined) => Promise<Result<unknown, Error>>;
43
- }
44
- /**
45
- * Build HTTP route definitions from a topo.
46
- *
47
- * Each trail becomes an HttpRouteDefinition with:
48
- * - An HTTP method derived from intent (read -> GET, write -> POST, destroy -> DELETE)
49
- * - A path derived from the trail ID (dots become slashes)
50
- * - An input source derived from the method (GET -> query, others -> body)
51
- * - An `execute` function that validates, layers, and runs the implementation
52
- *
53
- * Returns `Result.err(ValidationError)` if two trails derive the same
54
- * (method, path) pair. Returns `Result.ok(routes)` on success.
55
- */
56
- export declare const deriveHttpRoutes: (graph: Topo, options?: DeriveHttpRoutesOptions) => Result<HttpRouteDefinition[], Error>;
57
- //# sourceMappingURL=build.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,MAAM,EAMP,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EACV,MAAM,EACN,KAAK,EACL,mBAAmB,EACnB,IAAI,EACJ,KAAK,EACL,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAMxB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,wFAAwF;IACxF,QAAQ,CAAC,YAAY,CAAC,EAClB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GACjD,SAAS,CAAC;IACd,QAAQ,CAAC,aAAa,CAAC,EACnB,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC,GACpD,SAAS,CAAC;IACd,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAChD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,KAAK,EAAE,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACrD,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACzC;AAED,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnD,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACjD;;;;;;;;;OASG;IACH,QAAQ,CAAC,OAAO,EAAE,CAChB,KAAK,EAAE,OAAO,EACd,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,EAC9B,WAAW,CAAC,EAAE,WAAW,GAAG,SAAS,KAClC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;CACtC;AA2JD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,GAC3B,OAAO,IAAI,EACX,UAAS,uBAA4B,KACpC,MAAM,CAAC,mBAAmB,EAAE,EAAE,KAAK,CAiBrC,CAAC"}
package/dist/build.js DELETED
@@ -1,130 +0,0 @@
1
- /**
2
- * Build framework-agnostic HTTP route definitions from a Trails topo.
3
- *
4
- * Each route definition describes the path, method, input source, and an
5
- * `execute` function that validates input, composes layers, and runs the
6
- * implementation -- all without referencing any HTTP framework types.
7
- */
8
- import { Result, TRAILHEAD_KEY, ValidationError, executeTrail, filterSurfaceTrails, validateEstablishedTopo, } from '@ontrails/core';
9
- // ---------------------------------------------------------------------------
10
- // Internal helpers
11
- // ---------------------------------------------------------------------------
12
- /** Explicit intent → HTTP method mapping. */
13
- const intentToMethod = {
14
- destroy: 'DELETE',
15
- read: 'GET',
16
- write: 'POST',
17
- };
18
- /** Derive HTTP method from trail intent. */
19
- const deriveMethod = (trail) => intentToMethod[trail.intent] ?? 'POST';
20
- /** Derive HTTP path from trail ID: `entity.show` -> `/entity/show`. */
21
- const derivePath = (basePath, trailId) => {
22
- const segments = trailId.replaceAll('.', '/');
23
- const base = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
24
- return `${base}/${segments}`;
25
- };
26
- /** Derive input source from HTTP method. */
27
- const deriveInputSource = (method) => method === 'GET' ? 'query' : 'body';
28
- /** Build per-request context overrides with the HTTP trailhead marker. */
29
- const withHttpTrailhead = (requestId) => ({
30
- ...(requestId === undefined ? {} : { requestId }),
31
- extensions: {
32
- [TRAILHEAD_KEY]: 'http',
33
- },
34
- });
35
- // ---------------------------------------------------------------------------
36
- // Execute factory
37
- // ---------------------------------------------------------------------------
38
- /**
39
- * Create an `execute` function for a single trail.
40
- *
41
- * Delegates to the centralized `executeTrail` pipeline in core.
42
- * The returned function returns a `Result` and never throws.
43
- */
44
- const createExecute = (graph, t, layers, options) => (input, requestId, abortSignal) => executeTrail(t, input, {
45
- abortSignal,
46
- configValues: options.configValues,
47
- createContext: options.createContext,
48
- ctx: withHttpTrailhead(requestId),
49
- layers,
50
- resources: options.resources,
51
- topo: graph,
52
- });
53
- // ---------------------------------------------------------------------------
54
- // Builder helpers
55
- // ---------------------------------------------------------------------------
56
- /** Filter topo items to eligible trails. */
57
- const eligibleTrails = (graph, options) => filterSurfaceTrails(graph.list(), {
58
- exclude: options.exclude,
59
- include: options.include,
60
- intent: options.intent,
61
- });
62
- /** Build a single route definition from a trail. */
63
- const buildRoute = (graph, trail, basePath, layers, options) => {
64
- const method = deriveMethod(trail);
65
- const path = derivePath(basePath, trail.id);
66
- return {
67
- execute: createExecute(graph, trail, layers, options),
68
- inputSource: deriveInputSource(method),
69
- method,
70
- path,
71
- trail,
72
- trailId: trail.id,
73
- };
74
- };
75
- // ---------------------------------------------------------------------------
76
- // Collision detection
77
- // ---------------------------------------------------------------------------
78
- /** Derive the lookup key for (method, path) collision detection. */
79
- const routeKey = (route) => `${route.method} ${route.path}`;
80
- /** Register a route, checking for (path, method) collisions. */
81
- const registerRoute = (route, seenRoutes, routes) => {
82
- const key = routeKey(route);
83
- const existingId = seenRoutes.get(key);
84
- if (existingId !== undefined) {
85
- return Result.err(new ValidationError(`HTTP route collision: trails "${existingId}" and "${route.trailId}" both derive ${route.method} ${route.path}`));
86
- }
87
- seenRoutes.set(key, route.trailId);
88
- routes.push(route);
89
- return Result.ok();
90
- };
91
- /** Accumulate route definitions, returning early on the first collision. */
92
- const accumulateRoutes = (graph, trails, basePath, layers, options) => {
93
- const routes = [];
94
- const seenRoutes = new Map();
95
- for (const trail of trails) {
96
- const route = buildRoute(graph, trail, basePath, layers, options);
97
- const registered = registerRoute(route, seenRoutes, routes);
98
- if (registered.isErr()) {
99
- return registered;
100
- }
101
- }
102
- return Result.ok(routes);
103
- };
104
- // ---------------------------------------------------------------------------
105
- // Builder
106
- // ---------------------------------------------------------------------------
107
- /**
108
- * Build HTTP route definitions from a topo.
109
- *
110
- * Each trail becomes an HttpRouteDefinition with:
111
- * - An HTTP method derived from intent (read -> GET, write -> POST, destroy -> DELETE)
112
- * - A path derived from the trail ID (dots become slashes)
113
- * - An input source derived from the method (GET -> query, others -> body)
114
- * - An `execute` function that validates, layers, and runs the implementation
115
- *
116
- * Returns `Result.err(ValidationError)` if two trails derive the same
117
- * (method, path) pair. Returns `Result.ok(routes)` on success.
118
- */
119
- export const deriveHttpRoutes = (graph, options = {}) => {
120
- if (options.validate !== false) {
121
- const validated = validateEstablishedTopo(graph);
122
- if (validated.isErr()) {
123
- return Result.err(validated.error);
124
- }
125
- }
126
- const basePath = (options.basePath ?? '').replace(/\/+$/, '');
127
- const layers = options.layers ?? [];
128
- return accumulateRoutes(graph, eligibleTrails(graph, options), basePath, layers, options);
129
- };
130
- //# sourceMappingURL=build.js.map
package/dist/build.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export { deriveHttpRoutes, type DeriveHttpRoutesOptions, type HttpMethod, type HttpRouteDefinition, type InputSource, } from './build.js';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC"}
package/dist/index.js DELETED
@@ -1,3 +0,0 @@
1
- // Build (framework-agnostic)
2
- export { deriveHttpRoutes, } from './build.js';
3
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,OAAO,EACL,gBAAgB,GAKjB,MAAM,YAAY,CAAC"}