@ontrails/http 1.0.0-beta.12 → 1.0.0-beta.14
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +62 -23
- package/README.md +10 -10
- package/dist/build.d.ts +11 -9
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +24 -18
- package/dist/build.js.map +1 -1
- package/dist/hono/blaze.js +2 -2
- package/dist/hono/blaze.js.map +1 -1
- package/dist/hono/index.d.ts +1 -1
- package/dist/hono/index.d.ts.map +1 -1
- package/dist/hono/index.js +1 -1
- package/dist/hono/index.js.map +1 -1
- package/dist/hono/trailhead.d.ts +36 -0
- package/dist/hono/trailhead.d.ts.map +1 -0
- package/dist/hono/trailhead.js +222 -0
- package/dist/hono/trailhead.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/build.test.ts +60 -44
- package/src/build.ts +37 -27
- package/src/hono/__tests__/{blaze.test.ts → trailhead.test.ts} +79 -77
- package/src/hono/index.ts +1 -1
- package/src/hono/{blaze.ts → trailhead.ts} +21 -33
- package/tsconfig.tsbuildinfo +1 -1
package/src/build.ts
CHANGED
|
@@ -2,19 +2,20 @@
|
|
|
2
2
|
* Build framework-agnostic HTTP route definitions from a Trails topo.
|
|
3
3
|
*
|
|
4
4
|
* Each route definition describes the path, method, input source, and an
|
|
5
|
-
* `execute` function that validates input, composes
|
|
5
|
+
* `execute` function that validates input, composes gates, and runs the
|
|
6
6
|
* implementation -- all without referencing any HTTP framework types.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
Result,
|
|
11
|
-
|
|
11
|
+
TRAILHEAD_KEY,
|
|
12
12
|
ValidationError,
|
|
13
13
|
executeTrail,
|
|
14
|
+
validateEstablishedTopo,
|
|
14
15
|
} from '@ontrails/core';
|
|
15
16
|
import type {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
Gate,
|
|
18
|
+
ProvisionOverrideMap,
|
|
18
19
|
Topo,
|
|
19
20
|
Trail,
|
|
20
21
|
TrailContextInit,
|
|
@@ -26,15 +27,17 @@ import type {
|
|
|
26
27
|
|
|
27
28
|
export interface BuildHttpRoutesOptions {
|
|
28
29
|
readonly basePath?: string | undefined;
|
|
29
|
-
/** Config values for
|
|
30
|
+
/** Config values for provisions that declare a `config` schema, keyed by provision ID. */
|
|
30
31
|
readonly configValues?:
|
|
31
32
|
| Readonly<Record<string, Record<string, unknown>>>
|
|
32
33
|
| undefined;
|
|
33
34
|
readonly createContext?:
|
|
34
35
|
| (() => TrailContextInit | Promise<TrailContextInit>)
|
|
35
36
|
| undefined;
|
|
36
|
-
readonly
|
|
37
|
-
readonly
|
|
37
|
+
readonly gates?: readonly Gate[] | undefined;
|
|
38
|
+
readonly provisions?: ProvisionOverrideMap | undefined;
|
|
39
|
+
/** Set to `false` to skip topo validation while building routes. */
|
|
40
|
+
readonly validate?: boolean | undefined;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
|
@@ -49,19 +52,19 @@ export interface HttpRouteDefinition {
|
|
|
49
52
|
readonly inputSource: InputSource;
|
|
50
53
|
readonly trail: Trail<unknown, unknown>;
|
|
51
54
|
/**
|
|
52
|
-
* Validate input, compose
|
|
55
|
+
* Validate input, compose gates, and execute the trail implementation.
|
|
53
56
|
*
|
|
54
57
|
* The caller is responsible for parsing raw input from the request and
|
|
55
58
|
* mapping the Result to an HTTP response. This function is framework-agnostic.
|
|
56
59
|
*
|
|
57
|
-
* @param
|
|
60
|
+
* @param abortSignal - Optional AbortSignal from the HTTP request. When provided,
|
|
58
61
|
* it takes final precedence over any context factory signal, allowing
|
|
59
62
|
* client-initiated cancellation to propagate into trail execution.
|
|
60
63
|
*/
|
|
61
64
|
readonly execute: (
|
|
62
65
|
input: unknown,
|
|
63
66
|
requestId?: string | undefined,
|
|
64
|
-
|
|
67
|
+
abortSignal?: AbortSignal | undefined
|
|
65
68
|
) => Promise<Result<unknown, Error>>;
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -93,15 +96,15 @@ const deriveInputSource = (method: HttpMethod): InputSource =>
|
|
|
93
96
|
|
|
94
97
|
/** Check if a trail should be included (skip internal trails). */
|
|
95
98
|
const shouldInclude = (trail: Trail<unknown, unknown>): boolean =>
|
|
96
|
-
trail.
|
|
99
|
+
trail.meta?.['internal'] !== true;
|
|
97
100
|
|
|
98
|
-
/** Build per-request context overrides with the HTTP
|
|
99
|
-
const
|
|
101
|
+
/** Build per-request context overrides with the HTTP trailhead marker. */
|
|
102
|
+
const withHttpTrailhead = (
|
|
100
103
|
requestId: string | undefined
|
|
101
104
|
): Partial<TrailContextInit> => ({
|
|
102
105
|
...(requestId === undefined ? {} : { requestId }),
|
|
103
106
|
extensions: {
|
|
104
|
-
[
|
|
107
|
+
[TRAILHEAD_KEY]: 'http' as const,
|
|
105
108
|
},
|
|
106
109
|
});
|
|
107
110
|
|
|
@@ -118,17 +121,17 @@ const withHttpSurface = (
|
|
|
118
121
|
const createExecute =
|
|
119
122
|
(
|
|
120
123
|
t: Trail<unknown, unknown>,
|
|
121
|
-
|
|
124
|
+
gates: readonly Gate[],
|
|
122
125
|
options: BuildHttpRoutesOptions
|
|
123
126
|
): HttpRouteDefinition['execute'] =>
|
|
124
|
-
(input, requestId,
|
|
127
|
+
(input, requestId, abortSignal) =>
|
|
125
128
|
executeTrail(t, input, {
|
|
129
|
+
abortSignal,
|
|
126
130
|
configValues: options.configValues,
|
|
127
131
|
createContext: options.createContext,
|
|
128
|
-
ctx:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
signal,
|
|
132
|
+
ctx: withHttpTrailhead(requestId),
|
|
133
|
+
gates,
|
|
134
|
+
provisions: options.provisions,
|
|
132
135
|
});
|
|
133
136
|
|
|
134
137
|
// ---------------------------------------------------------------------------
|
|
@@ -143,13 +146,13 @@ const eligibleTrails = (app: Topo): Trail<unknown, unknown>[] =>
|
|
|
143
146
|
const buildRoute = (
|
|
144
147
|
trail: Trail<unknown, unknown>,
|
|
145
148
|
basePath: string,
|
|
146
|
-
|
|
149
|
+
gates: readonly Gate[],
|
|
147
150
|
options: BuildHttpRoutesOptions
|
|
148
151
|
): HttpRouteDefinition => {
|
|
149
152
|
const method = deriveMethod(trail);
|
|
150
153
|
const path = derivePath(basePath, trail.id);
|
|
151
154
|
return {
|
|
152
|
-
execute: createExecute(trail,
|
|
155
|
+
execute: createExecute(trail, gates, options),
|
|
153
156
|
inputSource: deriveInputSource(method),
|
|
154
157
|
method,
|
|
155
158
|
path,
|
|
@@ -190,14 +193,14 @@ const registerRoute = (
|
|
|
190
193
|
const accumulateRoutes = (
|
|
191
194
|
trails: Trail<unknown, unknown>[],
|
|
192
195
|
basePath: string,
|
|
193
|
-
|
|
196
|
+
gates: readonly Gate[],
|
|
194
197
|
options: BuildHttpRoutesOptions
|
|
195
198
|
): Result<HttpRouteDefinition[], Error> => {
|
|
196
199
|
const routes: HttpRouteDefinition[] = [];
|
|
197
200
|
const seenRoutes = new Map<string, string>();
|
|
198
201
|
|
|
199
202
|
for (const trail of trails) {
|
|
200
|
-
const route = buildRoute(trail, basePath,
|
|
203
|
+
const route = buildRoute(trail, basePath, gates, options);
|
|
201
204
|
const registered = registerRoute(route, seenRoutes, routes);
|
|
202
205
|
if (registered.isErr()) {
|
|
203
206
|
return registered;
|
|
@@ -218,7 +221,7 @@ const accumulateRoutes = (
|
|
|
218
221
|
* - An HTTP method derived from intent (read -> GET, write -> POST, destroy -> DELETE)
|
|
219
222
|
* - A path derived from the trail ID (dots become slashes)
|
|
220
223
|
* - An input source derived from the method (GET -> query, others -> body)
|
|
221
|
-
* - An `execute` function that validates,
|
|
224
|
+
* - An `execute` function that validates, gates, and runs the implementation
|
|
222
225
|
*
|
|
223
226
|
* Returns `Result.err(ValidationError)` if two trails derive the same
|
|
224
227
|
* (method, path) pair. Returns `Result.ok(routes)` on success.
|
|
@@ -227,7 +230,14 @@ export const buildHttpRoutes = (
|
|
|
227
230
|
app: Topo,
|
|
228
231
|
options: BuildHttpRoutesOptions = {}
|
|
229
232
|
): Result<HttpRouteDefinition[], Error> => {
|
|
233
|
+
if (options.validate !== false) {
|
|
234
|
+
const validated = validateEstablishedTopo(app);
|
|
235
|
+
if (validated.isErr()) {
|
|
236
|
+
return Result.err(validated.error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
230
240
|
const basePath = (options.basePath ?? '').replace(/\/+$/, '');
|
|
231
|
-
const
|
|
232
|
-
return accumulateRoutes(eligibleTrails(app), basePath,
|
|
241
|
+
const gates = options.gates ?? [];
|
|
242
|
+
return accumulateRoutes(eligibleTrails(app), basePath, gates, options);
|
|
233
243
|
};
|
|
@@ -4,55 +4,55 @@ import {
|
|
|
4
4
|
InternalError,
|
|
5
5
|
NotFoundError,
|
|
6
6
|
Result,
|
|
7
|
-
|
|
7
|
+
provision,
|
|
8
8
|
trail,
|
|
9
9
|
topo,
|
|
10
10
|
} from '@ontrails/core';
|
|
11
|
-
import type {
|
|
11
|
+
import type { Gate } from '@ontrails/core';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { trailhead } from '../trailhead.js';
|
|
15
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Test trails
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
20
|
const echoTrail = trail('echo', {
|
|
21
|
+
blaze: (input) => Result.ok({ reply: input.message }),
|
|
21
22
|
description: 'Echo a message back',
|
|
22
23
|
input: z.object({ message: z.string() }),
|
|
23
24
|
intent: 'read',
|
|
24
25
|
output: z.object({ reply: z.string() }),
|
|
25
|
-
run: (input) => Result.ok({ reply: input.message }),
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
const createTrail = trail('item.create', {
|
|
29
|
+
blaze: (input) => Result.ok({ id: '123', name: input.name }),
|
|
29
30
|
description: 'Create an item',
|
|
30
31
|
input: z.object({ name: z.string() }),
|
|
31
32
|
output: z.object({ id: z.string(), name: z.string() }),
|
|
32
|
-
run: (input) => Result.ok({ id: '123', name: input.name }),
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
const deleteTrail = trail('item.delete', {
|
|
36
|
+
blaze: (_input) => Result.ok({ deleted: true }),
|
|
36
37
|
description: 'Delete an item',
|
|
37
38
|
input: z.object({ id: z.string() }),
|
|
38
39
|
intent: 'destroy',
|
|
39
|
-
run: (_input) => Result.ok({ deleted: true }),
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
const notFoundTrail = trail('item.get', {
|
|
43
|
+
blaze: (_input) => Result.err(new NotFoundError('Item not found')),
|
|
43
44
|
description: 'Get an item that does not exist',
|
|
44
45
|
input: z.object({ id: z.string() }),
|
|
45
46
|
intent: 'read',
|
|
46
|
-
run: (_input) => Result.err(new NotFoundError('Item not found')),
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const internalTrail = trail('crash', {
|
|
50
|
+
blaze: () => Result.err(new InternalError('Something broke')),
|
|
50
51
|
description: 'Always fails with internal error',
|
|
51
52
|
input: z.object({}),
|
|
52
|
-
run: () => Result.err(new InternalError('Something broke')),
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const dbProvision = provision('db.main', {
|
|
56
56
|
create: () =>
|
|
57
57
|
Result.ok({
|
|
58
58
|
source: 'factory',
|
|
@@ -65,7 +65,7 @@ const dbService = service('db.main', {
|
|
|
65
65
|
|
|
66
66
|
/** Make a request against a Hono test app. */
|
|
67
67
|
const request = (
|
|
68
|
-
app: Awaited<ReturnType<typeof
|
|
68
|
+
app: Awaited<ReturnType<typeof trailhead>>,
|
|
69
69
|
method: string,
|
|
70
70
|
path: string,
|
|
71
71
|
body?: Record<string, unknown>,
|
|
@@ -82,7 +82,7 @@ const request = (
|
|
|
82
82
|
|
|
83
83
|
/** Make a request with a raw string body. */
|
|
84
84
|
const requestRaw = (
|
|
85
|
-
app: Awaited<ReturnType<typeof
|
|
85
|
+
app: Awaited<ReturnType<typeof trailhead>>,
|
|
86
86
|
method: string,
|
|
87
87
|
path: string,
|
|
88
88
|
rawBody: string,
|
|
@@ -100,29 +100,31 @@ const requestRaw = (
|
|
|
100
100
|
// Tests
|
|
101
101
|
// ---------------------------------------------------------------------------
|
|
102
102
|
|
|
103
|
-
describe('
|
|
103
|
+
describe('trailhead (Hono connector)', () => {
|
|
104
104
|
describe('validation', () => {
|
|
105
|
-
test('
|
|
105
|
+
test('trailhead throws on invalid topo', async () => {
|
|
106
106
|
const t = trail('broken', {
|
|
107
|
-
|
|
107
|
+
blaze: () => Result.ok({}),
|
|
108
|
+
crosses: ['nonexistent.trail'],
|
|
108
109
|
input: z.object({}),
|
|
109
110
|
output: z.object({}),
|
|
110
|
-
run: () => Result.ok({}),
|
|
111
111
|
});
|
|
112
112
|
const app = topo('test', { t });
|
|
113
|
-
await expect(
|
|
113
|
+
await expect(trailhead(app, { serve: false })).rejects.toThrow(
|
|
114
|
+
/validation/i
|
|
115
|
+
);
|
|
114
116
|
});
|
|
115
117
|
|
|
116
|
-
test('
|
|
118
|
+
test('trailhead skips validation when validate: false', async () => {
|
|
117
119
|
const t = trail('broken', {
|
|
118
|
-
|
|
120
|
+
blaze: () => Result.ok({}),
|
|
121
|
+
crosses: ['nonexistent.trail'],
|
|
119
122
|
input: z.object({}),
|
|
120
123
|
output: z.object({}),
|
|
121
|
-
run: () => Result.ok({}),
|
|
122
124
|
});
|
|
123
125
|
const app = topo('test', { t });
|
|
124
126
|
await expect(
|
|
125
|
-
|
|
127
|
+
trailhead(app, { serve: false, validate: false })
|
|
126
128
|
).resolves.toBeDefined();
|
|
127
129
|
});
|
|
128
130
|
});
|
|
@@ -130,7 +132,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
130
132
|
describe('GET handler', () => {
|
|
131
133
|
test('returns 200 with data on success', async () => {
|
|
132
134
|
const app = topo('testapp', { echoTrail });
|
|
133
|
-
const hono = await
|
|
135
|
+
const hono = await trailhead(app, { serve: false });
|
|
134
136
|
|
|
135
137
|
const res = await request(hono, 'GET', '/echo?message=hello');
|
|
136
138
|
expect(res.status).toBe(200);
|
|
@@ -141,7 +143,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
141
143
|
|
|
142
144
|
test('returns 400 on invalid input', async () => {
|
|
143
145
|
const app = topo('testapp', { echoTrail });
|
|
144
|
-
const hono = await
|
|
146
|
+
const hono = await trailhead(app, { serve: false });
|
|
145
147
|
|
|
146
148
|
const res = await request(hono, 'GET', '/echo');
|
|
147
149
|
expect(res.status).toBe(400);
|
|
@@ -155,7 +157,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
155
157
|
describe('POST handler', () => {
|
|
156
158
|
test('returns 200 with data on success', async () => {
|
|
157
159
|
const app = topo('testapp', { createTrail });
|
|
158
|
-
const hono = await
|
|
160
|
+
const hono = await trailhead(app, { serve: false });
|
|
159
161
|
|
|
160
162
|
const res = await request(hono, 'POST', '/item/create', {
|
|
161
163
|
name: 'Widget',
|
|
@@ -168,7 +170,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
168
170
|
|
|
169
171
|
test('returns 400 on invalid input', async () => {
|
|
170
172
|
const app = topo('testapp', { createTrail });
|
|
171
|
-
const hono = await
|
|
173
|
+
const hono = await trailhead(app, { serve: false });
|
|
172
174
|
|
|
173
175
|
const res = await request(hono, 'POST', '/item/create', {});
|
|
174
176
|
expect(res.status).toBe(400);
|
|
@@ -179,14 +181,14 @@ describe('blaze (Hono adapter)', () => {
|
|
|
179
181
|
|
|
180
182
|
test('POST with empty input schema succeeds without a body', async () => {
|
|
181
183
|
const emptyWriteTrail = trail('empty.write', {
|
|
184
|
+
blaze: () => Result.ok({ ok: true }),
|
|
182
185
|
input: z.object({}),
|
|
183
186
|
intent: 'write',
|
|
184
187
|
output: z.object({ ok: z.boolean() }),
|
|
185
|
-
run: () => Result.ok({ ok: true }),
|
|
186
188
|
});
|
|
187
189
|
|
|
188
190
|
const app = topo('testapp', { emptyWriteTrail });
|
|
189
|
-
const hono = await
|
|
191
|
+
const hono = await trailhead(app, { serve: false });
|
|
190
192
|
|
|
191
193
|
// No body, no Content-Type header — mirrors a client that obeys the
|
|
192
194
|
// OpenAPI spec (no requestBody declared for empty-input POST routes).
|
|
@@ -201,7 +203,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
201
203
|
describe('DELETE handler', () => {
|
|
202
204
|
test('returns 200 with data on success', async () => {
|
|
203
205
|
const app = topo('testapp', { deleteTrail });
|
|
204
|
-
const hono = await
|
|
206
|
+
const hono = await trailhead(app, { serve: false });
|
|
205
207
|
|
|
206
208
|
const res = await request(hono, 'DELETE', '/item/delete', {
|
|
207
209
|
id: 'abc',
|
|
@@ -216,7 +218,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
216
218
|
describe('error mapping', () => {
|
|
217
219
|
test('NotFoundError maps to 404', async () => {
|
|
218
220
|
const app = topo('testapp', { notFoundTrail });
|
|
219
|
-
const hono = await
|
|
221
|
+
const hono = await trailhead(app, { serve: false });
|
|
220
222
|
|
|
221
223
|
const res = await request(hono, 'GET', '/item/get?id=missing');
|
|
222
224
|
expect(res.status).toBe(404);
|
|
@@ -228,7 +230,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
228
230
|
|
|
229
231
|
test('InternalError maps to 500', async () => {
|
|
230
232
|
const app = topo('testapp', { internalTrail });
|
|
231
|
-
const hono = await
|
|
233
|
+
const hono = await trailhead(app, { serve: false });
|
|
232
234
|
|
|
233
235
|
const res = await request(hono, 'POST', '/crash', {});
|
|
234
236
|
expect(res.status).toBe(500);
|
|
@@ -239,14 +241,14 @@ describe('blaze (Hono adapter)', () => {
|
|
|
239
241
|
|
|
240
242
|
test('thrown exceptions map to 500', async () => {
|
|
241
243
|
const throwTrail = trail('throw', {
|
|
242
|
-
|
|
243
|
-
run: () => {
|
|
244
|
+
blaze: () => {
|
|
244
245
|
throw new Error('unexpected crash');
|
|
245
246
|
},
|
|
247
|
+
input: z.object({}),
|
|
246
248
|
});
|
|
247
249
|
|
|
248
250
|
const app = topo('testapp', { throwTrail });
|
|
249
|
-
const hono = await
|
|
251
|
+
const hono = await trailhead(app, { serve: false });
|
|
250
252
|
|
|
251
253
|
const res = await request(hono, 'POST', '/throw', {});
|
|
252
254
|
expect(res.status).toBe(500);
|
|
@@ -256,12 +258,12 @@ describe('blaze (Hono adapter)', () => {
|
|
|
256
258
|
});
|
|
257
259
|
});
|
|
258
260
|
|
|
259
|
-
describe('
|
|
260
|
-
test('
|
|
261
|
+
describe('gates', () => {
|
|
262
|
+
test('gates compose around trail execution', async () => {
|
|
261
263
|
const calls: string[] = [];
|
|
262
264
|
|
|
263
|
-
const
|
|
264
|
-
name: 'test-
|
|
265
|
+
const testGate: Gate = {
|
|
266
|
+
name: 'test-gate',
|
|
265
267
|
wrap(_trail, impl) {
|
|
266
268
|
return async (input, ctx) => {
|
|
267
269
|
calls.push('before');
|
|
@@ -273,7 +275,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
273
275
|
};
|
|
274
276
|
|
|
275
277
|
const app = topo('testapp', { echoTrail });
|
|
276
|
-
const hono = await
|
|
278
|
+
const hono = await trailhead(app, { gates: [testGate], serve: false });
|
|
277
279
|
|
|
278
280
|
const res = await request(hono, 'GET', '/echo?message=hi');
|
|
279
281
|
expect(res.status).toBe(200);
|
|
@@ -284,7 +286,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
284
286
|
describe('malformed JSON body', () => {
|
|
285
287
|
test('returns 400 for invalid JSON in POST body', async () => {
|
|
286
288
|
const app = topo('testapp', { createTrail });
|
|
287
|
-
const hono = await
|
|
289
|
+
const hono = await trailhead(app, { serve: false });
|
|
288
290
|
|
|
289
291
|
const res = await requestRaw(hono, 'POST', '/item/create', '{invalid');
|
|
290
292
|
expect(res.status).toBe(400);
|
|
@@ -297,7 +299,7 @@ describe('blaze (Hono adapter)', () => {
|
|
|
297
299
|
|
|
298
300
|
test('returns 400 for invalid JSON in DELETE body', async () => {
|
|
299
301
|
const app = topo('testapp', { deleteTrail });
|
|
300
|
-
const hono = await
|
|
302
|
+
const hono = await trailhead(app, { serve: false });
|
|
301
303
|
|
|
302
304
|
const res = await requestRaw(hono, 'DELETE', '/item/delete', 'not-json');
|
|
303
305
|
expect(res.status).toBe(400);
|
|
@@ -310,13 +312,13 @@ describe('blaze (Hono adapter)', () => {
|
|
|
310
312
|
describe('query param parsing', () => {
|
|
311
313
|
test('numeric-looking string is preserved as string', async () => {
|
|
312
314
|
const stringIdTrail = trail('lookup', {
|
|
315
|
+
blaze: (input) => Result.ok({ id: input.id }),
|
|
313
316
|
input: z.object({ id: z.string() }),
|
|
314
317
|
intent: 'read',
|
|
315
|
-
run: (input) => Result.ok({ id: input.id }),
|
|
316
318
|
});
|
|
317
319
|
|
|
318
320
|
const app = topo('testapp', { stringIdTrail });
|
|
319
|
-
const hono = await
|
|
321
|
+
const hono = await trailhead(app, { serve: false });
|
|
320
322
|
|
|
321
323
|
const res = await request(hono, 'GET', '/lookup?id=00123');
|
|
322
324
|
expect(res.status).toBe(200);
|
|
@@ -327,13 +329,13 @@ describe('blaze (Hono adapter)', () => {
|
|
|
327
329
|
|
|
328
330
|
test('repeated keys become arrays', async () => {
|
|
329
331
|
const tagsTrail = trail('tags', {
|
|
332
|
+
blaze: (input) => Result.ok({ tags: input.tags }),
|
|
330
333
|
input: z.object({ tags: z.array(z.string()) }),
|
|
331
334
|
intent: 'read',
|
|
332
|
-
run: (input) => Result.ok({ tags: input.tags }),
|
|
333
335
|
});
|
|
334
336
|
|
|
335
337
|
const app = topo('testapp', { tagsTrail });
|
|
336
|
-
const hono = await
|
|
338
|
+
const hono = await trailhead(app, { serve: false });
|
|
337
339
|
|
|
338
340
|
const res = await request(hono, 'GET', '/tags?tags=a&tags=b');
|
|
339
341
|
expect(res.status).toBe(200);
|
|
@@ -344,13 +346,13 @@ describe('blaze (Hono adapter)', () => {
|
|
|
344
346
|
|
|
345
347
|
test('single value is wrapped in array when schema expects z.array()', async () => {
|
|
346
348
|
const tagsTrail = trail('tags.single', {
|
|
349
|
+
blaze: (input) => Result.ok({ tags: input.tags }),
|
|
347
350
|
input: z.object({ tags: z.array(z.string()) }),
|
|
348
351
|
intent: 'read',
|
|
349
|
-
run: (input) => Result.ok({ tags: input.tags }),
|
|
350
352
|
});
|
|
351
353
|
|
|
352
354
|
const app = topo('testapp', { tagsTrail });
|
|
353
|
-
const hono = await
|
|
355
|
+
const hono = await trailhead(app, { serve: false });
|
|
354
356
|
|
|
355
357
|
const res = await request(hono, 'GET', '/tags/single?tags=foo');
|
|
356
358
|
expect(res.status).toBe(200);
|
|
@@ -361,13 +363,13 @@ describe('blaze (Hono adapter)', () => {
|
|
|
361
363
|
|
|
362
364
|
test('single value stays scalar when schema expects a string', async () => {
|
|
363
365
|
const nameTrail = trail('name.check', {
|
|
366
|
+
blaze: (input) => Result.ok({ name: input.name }),
|
|
364
367
|
input: z.object({ name: z.string() }),
|
|
365
368
|
intent: 'read',
|
|
366
|
-
run: (input) => Result.ok({ name: input.name }),
|
|
367
369
|
});
|
|
368
370
|
|
|
369
371
|
const app = topo('testapp', { nameTrail });
|
|
370
|
-
const hono = await
|
|
372
|
+
const hono = await trailhead(app, { serve: false });
|
|
371
373
|
|
|
372
374
|
const res = await request(hono, 'GET', '/name/check?name=bar');
|
|
373
375
|
expect(res.status).toBe(200);
|
|
@@ -378,13 +380,13 @@ describe('blaze (Hono adapter)', () => {
|
|
|
378
380
|
|
|
379
381
|
test('optional array field with single value is wrapped in array', async () => {
|
|
380
382
|
const optArrayTrail = trail('opt.array', {
|
|
383
|
+
blaze: (input) => Result.ok({ ids: input.ids }),
|
|
381
384
|
input: z.object({ ids: z.array(z.string()).optional() }),
|
|
382
385
|
intent: 'read',
|
|
383
|
-
run: (input) => Result.ok({ ids: input.ids }),
|
|
384
386
|
});
|
|
385
387
|
|
|
386
388
|
const app = topo('testapp', { optArrayTrail });
|
|
387
|
-
const hono = await
|
|
389
|
+
const hono = await trailhead(app, { serve: false });
|
|
388
390
|
|
|
389
391
|
const res = await request(hono, 'GET', '/opt/array?ids=one');
|
|
390
392
|
expect(res.status).toBe(200);
|
|
@@ -399,17 +401,17 @@ describe('blaze (Hono adapter)', () => {
|
|
|
399
401
|
let capturedSignal: AbortSignal | undefined;
|
|
400
402
|
|
|
401
403
|
const signalTrail = trail('signal.check', {
|
|
404
|
+
blaze: (_input, ctx) => {
|
|
405
|
+
capturedSignal = ctx.abortSignal;
|
|
406
|
+
return Result.ok({ ok: true });
|
|
407
|
+
},
|
|
402
408
|
input: z.object({}),
|
|
403
409
|
intent: 'read',
|
|
404
410
|
output: z.object({ ok: z.boolean() }),
|
|
405
|
-
run: (_input, ctx) => {
|
|
406
|
-
capturedSignal = ctx.signal;
|
|
407
|
-
return Result.ok({ ok: true });
|
|
408
|
-
},
|
|
409
411
|
});
|
|
410
412
|
|
|
411
413
|
const app = topo('testapp', { signalTrail });
|
|
412
|
-
const hono = await
|
|
414
|
+
const hono = await trailhead(app, { serve: false });
|
|
413
415
|
|
|
414
416
|
const res = await request(hono, 'GET', '/signal/check');
|
|
415
417
|
expect(res.status).toBe(200);
|
|
@@ -420,17 +422,17 @@ describe('blaze (Hono adapter)', () => {
|
|
|
420
422
|
let capturedSignal: AbortSignal | undefined;
|
|
421
423
|
|
|
422
424
|
const signalTrail = trail('signal.aborted', {
|
|
425
|
+
blaze: (_input, ctx) => {
|
|
426
|
+
capturedSignal = ctx.abortSignal;
|
|
427
|
+
return Result.ok({ ok: true });
|
|
428
|
+
},
|
|
423
429
|
input: z.object({}),
|
|
424
430
|
intent: 'read',
|
|
425
431
|
output: z.object({ ok: z.boolean() }),
|
|
426
|
-
run: (_input, ctx) => {
|
|
427
|
-
capturedSignal = ctx.signal;
|
|
428
|
-
return Result.ok({ ok: true });
|
|
429
|
-
},
|
|
430
432
|
});
|
|
431
433
|
|
|
432
434
|
const app = topo('testapp', { signalTrail });
|
|
433
|
-
const hono = await
|
|
435
|
+
const hono = await trailhead(app, { serve: false });
|
|
434
436
|
|
|
435
437
|
const controller = new AbortController();
|
|
436
438
|
controller.abort();
|
|
@@ -453,16 +455,16 @@ describe('blaze (Hono adapter)', () => {
|
|
|
453
455
|
let capturedRequestId: string | undefined;
|
|
454
456
|
|
|
455
457
|
const ctxTrail = trail('ctx.check', {
|
|
456
|
-
|
|
457
|
-
intent: 'read',
|
|
458
|
-
run: (_input, ctx) => {
|
|
458
|
+
blaze: (_input, ctx) => {
|
|
459
459
|
capturedRequestId = ctx.requestId;
|
|
460
460
|
return Result.ok({ ok: true });
|
|
461
461
|
},
|
|
462
|
+
input: z.object({}),
|
|
463
|
+
intent: 'read',
|
|
462
464
|
});
|
|
463
465
|
|
|
464
466
|
const app = topo('testapp', { ctxTrail });
|
|
465
|
-
const hono = await
|
|
467
|
+
const hono = await trailhead(app, { serve: false });
|
|
466
468
|
|
|
467
469
|
const res = await request(hono, 'GET', '/ctx/check', undefined, {
|
|
468
470
|
'X-Request-ID': 'custom-req-123',
|
|
@@ -476,20 +478,20 @@ describe('blaze (Hono adapter)', () => {
|
|
|
476
478
|
let contextUsed = false;
|
|
477
479
|
|
|
478
480
|
const ctxTrail = trail('ctx.custom', {
|
|
479
|
-
|
|
480
|
-
intent: 'read',
|
|
481
|
-
run: (_input, ctx) => {
|
|
481
|
+
blaze: (_input, ctx) => {
|
|
482
482
|
contextUsed = ctx.extensions?.['custom'] === true;
|
|
483
483
|
return Result.ok({ ok: true });
|
|
484
484
|
},
|
|
485
|
+
input: z.object({}),
|
|
486
|
+
intent: 'read',
|
|
485
487
|
});
|
|
486
488
|
|
|
487
489
|
const app = topo('testapp', { ctxTrail });
|
|
488
|
-
const hono = await
|
|
490
|
+
const hono = await trailhead(app, {
|
|
489
491
|
createContext: () => ({
|
|
492
|
+
abortSignal: new AbortController().signal,
|
|
490
493
|
extensions: { custom: true },
|
|
491
494
|
requestId: 'test-id',
|
|
492
|
-
signal: new AbortController().signal,
|
|
493
495
|
}),
|
|
494
496
|
serve: false,
|
|
495
497
|
});
|
|
@@ -499,23 +501,23 @@ describe('blaze (Hono adapter)', () => {
|
|
|
499
501
|
expect(contextUsed).toBe(true);
|
|
500
502
|
});
|
|
501
503
|
|
|
502
|
-
test('
|
|
503
|
-
const
|
|
504
|
+
test('provision overrides reach the trail through trailhead()', async () => {
|
|
505
|
+
const provisionTrail = trail('provision.check', {
|
|
506
|
+
blaze: (_input, ctx) =>
|
|
507
|
+
Result.ok({ source: dbProvision.from(ctx).source as string }),
|
|
504
508
|
input: z.object({}),
|
|
505
509
|
intent: 'read',
|
|
506
510
|
output: z.object({ source: z.string() }),
|
|
507
|
-
|
|
508
|
-
Result.ok({ source: dbService.from(ctx).source as string }),
|
|
509
|
-
services: [dbService],
|
|
511
|
+
provisions: [dbProvision],
|
|
510
512
|
});
|
|
511
513
|
|
|
512
|
-
const app = topo('testapp', {
|
|
513
|
-
const hono = await
|
|
514
|
+
const app = topo('testapp', { dbProvision, provisionTrail });
|
|
515
|
+
const hono = await trailhead(app, {
|
|
516
|
+
provisions: { 'db.main': { source: 'override' } },
|
|
514
517
|
serve: false,
|
|
515
|
-
services: { 'db.main': { source: 'override' } },
|
|
516
518
|
});
|
|
517
519
|
|
|
518
|
-
const res = await request(hono, 'GET', '/
|
|
520
|
+
const res = await request(hono, 'GET', '/provision/check');
|
|
519
521
|
expect(res.status).toBe(200);
|
|
520
522
|
|
|
521
523
|
const json = await res.json();
|
package/src/hono/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { trailhead, type TrailheadHttpOptions } from './trailhead.js';
|