@ontrails/http 1.0.0-beta.12 → 1.0.0-beta.13
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 +51 -23
- package/README.md +10 -10
- package/dist/build.d.ts +9 -9
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +18 -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 +33 -0
- package/dist/hono/trailhead.d.ts.map +1 -0
- package/dist/hono/trailhead.js +232 -0
- package/dist/hono/trailhead.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/build.test.ts +45 -44
- package/src/build.ts +27 -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} +16 -16
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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';
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hono
|
|
2
|
+
* Hono connector for Trails HTTP routes.
|
|
3
3
|
*
|
|
4
4
|
* Takes framework-agnostic HttpRouteDefinition[] and wires them into a
|
|
5
5
|
* Hono application, handling request parsing, response mapping, and errors.
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
8
|
* const app = topo("myapp", entity);
|
|
9
|
-
* await
|
|
9
|
+
* await trailhead(app, { port: 3000 });
|
|
10
10
|
* ```
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { isTrailsError, statusCodeMap, validateTopo } from '@ontrails/core';
|
|
14
14
|
import type {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Gate,
|
|
16
|
+
ProvisionOverrideMap,
|
|
17
17
|
Topo,
|
|
18
18
|
TrailContextInit,
|
|
19
19
|
} from '@ontrails/core';
|
|
@@ -29,9 +29,9 @@ import { buildHttpRoutes } from '../build.js';
|
|
|
29
29
|
// Options
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
|
|
32
|
-
export interface
|
|
32
|
+
export interface TrailheadHttpOptions {
|
|
33
33
|
readonly basePath?: string | undefined;
|
|
34
|
-
/** Config values for
|
|
34
|
+
/** Config values for provisions that declare a `config` schema, keyed by provision ID. */
|
|
35
35
|
readonly configValues?:
|
|
36
36
|
| Readonly<Record<string, Record<string, unknown>>>
|
|
37
37
|
| undefined;
|
|
@@ -39,10 +39,10 @@ export interface BlazeHttpOptions {
|
|
|
39
39
|
| (() => TrailContextInit | Promise<TrailContextInit>)
|
|
40
40
|
| undefined;
|
|
41
41
|
readonly hostname?: string | undefined;
|
|
42
|
-
readonly
|
|
42
|
+
readonly gates?: readonly Gate[] | undefined;
|
|
43
43
|
readonly name?: string | undefined;
|
|
44
44
|
readonly port?: number | undefined;
|
|
45
|
-
readonly
|
|
45
|
+
readonly provisions?: ProvisionOverrideMap | undefined;
|
|
46
46
|
/** Set false to return the Hono app without starting a server. */
|
|
47
47
|
readonly serve?: boolean | undefined;
|
|
48
48
|
/** Set to `false` to skip topo validation at startup. Defaults to `true`. */
|
|
@@ -233,10 +233,10 @@ const createHonoHandler =
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
const requestId = c.req.header('X-Request-ID') ?? undefined;
|
|
236
|
-
const { signal } = c.req.raw;
|
|
236
|
+
const { signal: abortSignal } = c.req.raw;
|
|
237
237
|
|
|
238
238
|
try {
|
|
239
|
-
const result = await route.execute(rawInput, requestId,
|
|
239
|
+
const result = await route.execute(rawInput, requestId, abortSignal);
|
|
240
240
|
return mapResultToResponse(result, c);
|
|
241
241
|
} catch (error: unknown) {
|
|
242
242
|
return handleCaughtError(error, c);
|
|
@@ -309,16 +309,16 @@ const assertValidTopo = (app: Topo, skip = false): void => {
|
|
|
309
309
|
};
|
|
310
310
|
|
|
311
311
|
// ---------------------------------------------------------------------------
|
|
312
|
-
//
|
|
312
|
+
// trailhead
|
|
313
313
|
// ---------------------------------------------------------------------------
|
|
314
314
|
|
|
315
315
|
/**
|
|
316
316
|
* Build HTTP routes from a topo, create a Hono app, and optionally start serving.
|
|
317
317
|
*/
|
|
318
|
-
// oxlint-disable-next-line require-await -- async for consistency with other
|
|
319
|
-
export const
|
|
318
|
+
// oxlint-disable-next-line require-await -- async for consistency with other trailhead() entrypoints
|
|
319
|
+
export const trailhead = async (
|
|
320
320
|
app: Topo,
|
|
321
|
-
options:
|
|
321
|
+
options: TrailheadHttpOptions = {}
|
|
322
322
|
): Promise<Hono> => {
|
|
323
323
|
assertValidTopo(app, options.validate === false);
|
|
324
324
|
|
|
@@ -330,8 +330,8 @@ export const blaze = async (
|
|
|
330
330
|
basePath: options.basePath,
|
|
331
331
|
configValues: options.configValues,
|
|
332
332
|
createContext: options.createContext,
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
gates: options.gates,
|
|
334
|
+
provisions: options.provisions,
|
|
335
335
|
});
|
|
336
336
|
|
|
337
337
|
if (routesResult.isErr()) {
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/build.ts","./src/index.ts","./src/hono/
|
|
1
|
+
{"root":["./src/build.ts","./src/index.ts","./src/hono/index.ts","./src/hono/trailhead.ts"],"version":"5.9.3"}
|