@ontrails/http 1.0.0-beta.14 → 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/CHANGELOG.md +54 -0
- package/README.md +54 -17
- package/package.json +10 -9
- package/src/build.ts +1137 -78
- package/src/index.ts +18 -4
- package/src/method.ts +24 -0
- package/src/openapi.ts +357 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/blaze.d.ts +0 -25
- package/dist/blaze.d.ts.map +0 -1
- package/dist/blaze.js +0 -69
- package/dist/blaze.js.map +0 -1
- package/dist/build.d.ts +0 -54
- package/dist/build.d.ts.map +0 -1
- package/dist/build.js +0 -127
- package/dist/build.js.map +0 -1
- package/dist/hono/blaze.d.ts +0 -33
- package/dist/hono/blaze.d.ts.map +0 -1
- package/dist/hono/blaze.js +0 -232
- package/dist/hono/blaze.js.map +0 -1
- package/dist/hono/index.d.ts +0 -2
- package/dist/hono/index.d.ts.map +0 -1
- package/dist/hono/index.js +0 -2
- package/dist/hono/index.js.map +0 -1
- package/dist/hono/trailhead.d.ts +0 -36
- package/dist/hono/trailhead.d.ts.map +0 -1
- package/dist/hono/trailhead.js +0 -222
- package/dist/hono/trailhead.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +0 -1
- package/src/__tests__/build.test.ts +0 -494
- package/src/hono/__tests__/trailhead.test.ts +0 -527
- package/src/hono/index.ts +0 -1
- package/src/hono/trailhead.ts +0 -340
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,494 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
InternalError,
|
|
5
|
-
NotFoundError,
|
|
6
|
-
Result,
|
|
7
|
-
TRAILHEAD_KEY,
|
|
8
|
-
provision,
|
|
9
|
-
ValidationError,
|
|
10
|
-
trail,
|
|
11
|
-
topo,
|
|
12
|
-
} from '@ontrails/core';
|
|
13
|
-
import type { Gate } from '@ontrails/core';
|
|
14
|
-
import { z } from 'zod';
|
|
15
|
-
|
|
16
|
-
import { buildHttpRoutes } from '../build.js';
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Test trails
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
const echoTrail = trail('echo', {
|
|
23
|
-
blaze: (input) => Result.ok({ reply: input.message }),
|
|
24
|
-
description: 'Echo a message back',
|
|
25
|
-
input: z.object({ message: z.string() }),
|
|
26
|
-
intent: 'read',
|
|
27
|
-
output: z.object({ reply: z.string() }),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const createTrail = trail('item.create', {
|
|
31
|
-
blaze: (input) => Result.ok({ id: '123', name: input.name }),
|
|
32
|
-
description: 'Create an item',
|
|
33
|
-
input: z.object({ name: z.string() }),
|
|
34
|
-
output: z.object({ id: z.string(), name: z.string() }),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const deleteTrail = trail('item.delete', {
|
|
38
|
-
blaze: (_input) => Result.ok({ deleted: true }),
|
|
39
|
-
description: 'Delete an item',
|
|
40
|
-
input: z.object({ id: z.string() }),
|
|
41
|
-
intent: 'destroy',
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const notFoundTrail = trail('item.get', {
|
|
45
|
-
blaze: (_input) => Result.err(new NotFoundError('Item not found')),
|
|
46
|
-
description: 'Get an item that does not exist',
|
|
47
|
-
input: z.object({ id: z.string() }),
|
|
48
|
-
intent: 'read',
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const internalTrail = trail('crash', {
|
|
52
|
-
blaze: () => Result.err(new InternalError('Something broke')),
|
|
53
|
-
description: 'Always fails with internal error',
|
|
54
|
-
input: z.object({}),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const internalMetaTrail = trail('secret', {
|
|
58
|
-
blaze: () => Result.ok({ ok: true }),
|
|
59
|
-
description: 'Internal trail that should be skipped',
|
|
60
|
-
input: z.object({}),
|
|
61
|
-
meta: { internal: true },
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const dbProvision = provision('db.main', {
|
|
65
|
-
create: () =>
|
|
66
|
-
Result.ok({
|
|
67
|
-
source: 'factory',
|
|
68
|
-
}),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Tests
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
describe('buildHttpRoutes', () => {
|
|
76
|
-
describe('method derivation', () => {
|
|
77
|
-
test('intent: read maps to GET', () => {
|
|
78
|
-
const app = topo('testapp', { echoTrail });
|
|
79
|
-
const result = buildHttpRoutes(app);
|
|
80
|
-
|
|
81
|
-
expect(result.isOk()).toBe(true);
|
|
82
|
-
const routes = result.value;
|
|
83
|
-
expect(routes).toHaveLength(1);
|
|
84
|
-
expect(routes[0]?.method).toBe('GET');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('intent: destroy maps to DELETE', () => {
|
|
88
|
-
const app = topo('testapp', { deleteTrail });
|
|
89
|
-
const result = buildHttpRoutes(app);
|
|
90
|
-
|
|
91
|
-
expect(result.isOk()).toBe(true);
|
|
92
|
-
const routes = result.value;
|
|
93
|
-
expect(routes).toHaveLength(1);
|
|
94
|
-
expect(routes[0]?.method).toBe('DELETE');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('default intent (write) maps to POST', () => {
|
|
98
|
-
const app = topo('testapp', { createTrail });
|
|
99
|
-
const result = buildHttpRoutes(app);
|
|
100
|
-
|
|
101
|
-
expect(result.isOk()).toBe(true);
|
|
102
|
-
const routes = result.value;
|
|
103
|
-
expect(routes).toHaveLength(1);
|
|
104
|
-
expect(routes[0]?.method).toBe('POST');
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe('path derivation', () => {
|
|
109
|
-
test('dotted ID becomes slashed path', () => {
|
|
110
|
-
const app = topo('testapp', { createTrail });
|
|
111
|
-
const result = buildHttpRoutes(app);
|
|
112
|
-
|
|
113
|
-
expect(result.isOk()).toBe(true);
|
|
114
|
-
expect(result.value[0]?.path).toBe('/item/create');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('simple ID becomes /id', () => {
|
|
118
|
-
const app = topo('testapp', { echoTrail });
|
|
119
|
-
const result = buildHttpRoutes(app);
|
|
120
|
-
|
|
121
|
-
expect(result.isOk()).toBe(true);
|
|
122
|
-
expect(result.value[0]?.path).toBe('/echo');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('basePath is prepended', () => {
|
|
126
|
-
const app = topo('testapp', { echoTrail });
|
|
127
|
-
const result = buildHttpRoutes(app, { basePath: '/api/v1' });
|
|
128
|
-
|
|
129
|
-
expect(result.isOk()).toBe(true);
|
|
130
|
-
expect(result.value[0]?.path).toBe('/api/v1/echo');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('basePath trailing slash is normalized', () => {
|
|
134
|
-
const app = topo('testapp', { echoTrail });
|
|
135
|
-
const result = buildHttpRoutes(app, { basePath: '/api/v1/' });
|
|
136
|
-
|
|
137
|
-
expect(result.isOk()).toBe(true);
|
|
138
|
-
expect(result.value[0]?.path).toBe('/api/v1/echo');
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('input source derivation', () => {
|
|
143
|
-
test('GET routes use query input source', () => {
|
|
144
|
-
const app = topo('testapp', { echoTrail });
|
|
145
|
-
const result = buildHttpRoutes(app);
|
|
146
|
-
|
|
147
|
-
expect(result.isOk()).toBe(true);
|
|
148
|
-
expect(result.value[0]?.inputSource).toBe('query');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('POST routes use body input source', () => {
|
|
152
|
-
const app = topo('testapp', { createTrail });
|
|
153
|
-
const result = buildHttpRoutes(app);
|
|
154
|
-
|
|
155
|
-
expect(result.isOk()).toBe(true);
|
|
156
|
-
expect(result.value[0]?.inputSource).toBe('body');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test('DELETE routes use body input source', () => {
|
|
160
|
-
const app = topo('testapp', { deleteTrail });
|
|
161
|
-
const result = buildHttpRoutes(app);
|
|
162
|
-
|
|
163
|
-
expect(result.isOk()).toBe(true);
|
|
164
|
-
expect(result.value[0]?.inputSource).toBe('body');
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('filtering', () => {
|
|
169
|
-
test('internal trails are skipped', () => {
|
|
170
|
-
const app = topo('testapp', { echoTrail, internalMetaTrail });
|
|
171
|
-
const result = buildHttpRoutes(app);
|
|
172
|
-
|
|
173
|
-
expect(result.isOk()).toBe(true);
|
|
174
|
-
const routes = result.value;
|
|
175
|
-
expect(routes).toHaveLength(1);
|
|
176
|
-
expect(routes[0]?.trailId).toBe('echo');
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
describe('route definition shape', () => {
|
|
181
|
-
test('includes trail reference', () => {
|
|
182
|
-
const app = topo('testapp', { echoTrail });
|
|
183
|
-
const result = buildHttpRoutes(app);
|
|
184
|
-
|
|
185
|
-
expect(result.isOk()).toBe(true);
|
|
186
|
-
expect(result.value[0]?.trail).toBe(echoTrail);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test('execute is a function', () => {
|
|
190
|
-
const app = topo('testapp', { echoTrail });
|
|
191
|
-
const result = buildHttpRoutes(app);
|
|
192
|
-
|
|
193
|
-
expect(result.isOk()).toBe(true);
|
|
194
|
-
expect(typeof result.value[0]?.execute).toBe('function');
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe('execute', () => {
|
|
199
|
-
test('returns ok Result on valid input', async () => {
|
|
200
|
-
const app = topo('testapp', { echoTrail });
|
|
201
|
-
const buildResult = buildHttpRoutes(app);
|
|
202
|
-
|
|
203
|
-
expect(buildResult.isOk()).toBe(true);
|
|
204
|
-
const [route] = buildResult.value;
|
|
205
|
-
|
|
206
|
-
const result = await route?.execute({ message: 'hello' });
|
|
207
|
-
expect(result?.isOk()).toBe(true);
|
|
208
|
-
expect(result?.value).toEqual({ reply: 'hello' });
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('returns err Result on invalid input', async () => {
|
|
212
|
-
const app = topo('testapp', { echoTrail });
|
|
213
|
-
const buildResult = buildHttpRoutes(app);
|
|
214
|
-
|
|
215
|
-
expect(buildResult.isOk()).toBe(true);
|
|
216
|
-
const [route] = buildResult.value;
|
|
217
|
-
|
|
218
|
-
const result = await route?.execute({});
|
|
219
|
-
expect(result?.isErr()).toBe(true);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('returns err Result from trail error', async () => {
|
|
223
|
-
const app = topo('testapp', { notFoundTrail });
|
|
224
|
-
const buildResult = buildHttpRoutes(app);
|
|
225
|
-
|
|
226
|
-
expect(buildResult.isOk()).toBe(true);
|
|
227
|
-
const [route] = buildResult.value;
|
|
228
|
-
|
|
229
|
-
const result = await route?.execute({ id: 'missing' });
|
|
230
|
-
expect(result?.isErr()).toBe(true);
|
|
231
|
-
expect(result?.error?.message).toBe('Item not found');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test('returns err Result from internal error', async () => {
|
|
235
|
-
const app = topo('testapp', { internalTrail });
|
|
236
|
-
const buildResult = buildHttpRoutes(app);
|
|
237
|
-
|
|
238
|
-
expect(buildResult.isOk()).toBe(true);
|
|
239
|
-
const [route] = buildResult.value;
|
|
240
|
-
|
|
241
|
-
const result = await route?.execute({});
|
|
242
|
-
expect(result?.isErr()).toBe(true);
|
|
243
|
-
expect(result?.error?.message).toBe('Something broke');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test('returns err Result when run function throws', async () => {
|
|
247
|
-
const throwingTrail = trail('throwing', {
|
|
248
|
-
blaze: () => {
|
|
249
|
-
throw new Error('unexpected throw');
|
|
250
|
-
},
|
|
251
|
-
input: z.object({}),
|
|
252
|
-
});
|
|
253
|
-
const app = topo('testapp', { throwingTrail });
|
|
254
|
-
const buildResult = buildHttpRoutes(app);
|
|
255
|
-
|
|
256
|
-
expect(buildResult.isOk()).toBe(true);
|
|
257
|
-
const [route] = buildResult.value;
|
|
258
|
-
|
|
259
|
-
const result = await route?.execute({});
|
|
260
|
-
expect(result?.isErr()).toBe(true);
|
|
261
|
-
expect(result?.error).toBeInstanceOf(InternalError);
|
|
262
|
-
expect(result?.error?.message).toBe('unexpected throw');
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test('returns err Result when createContext throws', async () => {
|
|
266
|
-
const app = topo('testapp', { echoTrail });
|
|
267
|
-
const buildResult = buildHttpRoutes(app, {
|
|
268
|
-
createContext: () => {
|
|
269
|
-
throw new Error('context creation failed');
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
expect(buildResult.isOk()).toBe(true);
|
|
274
|
-
const [route] = buildResult.value;
|
|
275
|
-
|
|
276
|
-
const result = await route?.execute({ message: 'hi' });
|
|
277
|
-
expect(result?.isErr()).toBe(true);
|
|
278
|
-
expect(result?.error).toBeInstanceOf(InternalError);
|
|
279
|
-
expect(result?.error?.message).toBe('context creation failed');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('passes requestId to context', async () => {
|
|
283
|
-
let capturedRequestId: string | undefined;
|
|
284
|
-
|
|
285
|
-
const ctxTrail = trail('ctx.check', {
|
|
286
|
-
blaze: (_input, ctx) => {
|
|
287
|
-
capturedRequestId = ctx.requestId;
|
|
288
|
-
return Result.ok({ ok: true });
|
|
289
|
-
},
|
|
290
|
-
input: z.object({}),
|
|
291
|
-
intent: 'read',
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
const app = topo('testapp', { ctxTrail });
|
|
295
|
-
const buildResult = buildHttpRoutes(app);
|
|
296
|
-
|
|
297
|
-
expect(buildResult.isOk()).toBe(true);
|
|
298
|
-
const [route] = buildResult.value;
|
|
299
|
-
|
|
300
|
-
await route?.execute({}, 'custom-req-123');
|
|
301
|
-
expect(capturedRequestId).toBe('custom-req-123');
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test('uses default requestId when none provided', async () => {
|
|
305
|
-
let capturedRequestId: string | undefined;
|
|
306
|
-
|
|
307
|
-
const ctxTrail = trail('ctx.default', {
|
|
308
|
-
blaze: (_input, ctx) => {
|
|
309
|
-
capturedRequestId = ctx.requestId;
|
|
310
|
-
return Result.ok({ ok: true });
|
|
311
|
-
},
|
|
312
|
-
input: z.object({}),
|
|
313
|
-
intent: 'read',
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const app = topo('testapp', { ctxTrail });
|
|
317
|
-
const buildResult = buildHttpRoutes(app);
|
|
318
|
-
|
|
319
|
-
expect(buildResult.isOk()).toBe(true);
|
|
320
|
-
const [route] = buildResult.value;
|
|
321
|
-
|
|
322
|
-
await route?.execute({});
|
|
323
|
-
expect(capturedRequestId).toBeDefined();
|
|
324
|
-
expect(capturedRequestId).not.toBe('');
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
test('forwards provision overrides into executeTrail', async () => {
|
|
328
|
-
const provisionTrail = trail('provision.check', {
|
|
329
|
-
blaze: (_input, ctx) =>
|
|
330
|
-
Result.ok({ source: dbProvision.from(ctx).source as string }),
|
|
331
|
-
input: z.object({}),
|
|
332
|
-
output: z.object({ source: z.string() }),
|
|
333
|
-
provisions: [dbProvision],
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
const app = topo('testapp', { dbProvision, provisionTrail });
|
|
337
|
-
const buildResult = buildHttpRoutes(app, {
|
|
338
|
-
provisions: { 'db.main': { source: 'override' } },
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
expect(buildResult.isOk()).toBe(true);
|
|
342
|
-
const [route] = buildResult.value;
|
|
343
|
-
|
|
344
|
-
const result = await route?.execute({});
|
|
345
|
-
expect(result?.isOk()).toBe(true);
|
|
346
|
-
expect(result?.value).toEqual({ source: 'override' });
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
describe('gates', () => {
|
|
351
|
-
test('gates compose around trail execution', async () => {
|
|
352
|
-
const calls: string[] = [];
|
|
353
|
-
|
|
354
|
-
const testGate: Gate = {
|
|
355
|
-
name: 'test-gate',
|
|
356
|
-
wrap(_trail, impl) {
|
|
357
|
-
return async (input, ctx) => {
|
|
358
|
-
calls.push('before');
|
|
359
|
-
const result = await impl(input, ctx);
|
|
360
|
-
calls.push('after');
|
|
361
|
-
return result;
|
|
362
|
-
};
|
|
363
|
-
},
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
const app = topo('testapp', { echoTrail });
|
|
367
|
-
const buildResult = buildHttpRoutes(app, { gates: [testGate] });
|
|
368
|
-
|
|
369
|
-
expect(buildResult.isOk()).toBe(true);
|
|
370
|
-
const [route] = buildResult.value;
|
|
371
|
-
|
|
372
|
-
const result = await route?.execute({ message: 'hi' });
|
|
373
|
-
expect(result?.isOk()).toBe(true);
|
|
374
|
-
expect(calls).toEqual(['before', 'after']);
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe('custom createContext', () => {
|
|
379
|
-
test('custom createContext is used when provided', async () => {
|
|
380
|
-
const contextState = { custom: false, trailheadMarker: false };
|
|
381
|
-
|
|
382
|
-
const ctxTrail = trail('ctx.custom', {
|
|
383
|
-
blaze: (_input, ctx) => {
|
|
384
|
-
contextState.custom = ctx.extensions?.['custom'] === true;
|
|
385
|
-
contextState.trailheadMarker =
|
|
386
|
-
ctx.extensions?.[TRAILHEAD_KEY] === 'http';
|
|
387
|
-
return Result.ok({ ok: true });
|
|
388
|
-
},
|
|
389
|
-
input: z.object({}),
|
|
390
|
-
intent: 'read',
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
const app = topo('testapp', { ctxTrail });
|
|
394
|
-
const buildResult = buildHttpRoutes(app, {
|
|
395
|
-
createContext: () => ({
|
|
396
|
-
abortSignal: new AbortController().signal,
|
|
397
|
-
extensions: { custom: true },
|
|
398
|
-
requestId: 'test-id',
|
|
399
|
-
}),
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
expect(buildResult.isOk()).toBe(true);
|
|
403
|
-
const [route] = buildResult.value;
|
|
404
|
-
|
|
405
|
-
const result = await route?.execute({});
|
|
406
|
-
expect(result?.isOk()).toBe(true);
|
|
407
|
-
expect(contextState.custom).toBe(true);
|
|
408
|
-
expect(contextState.trailheadMarker).toBe(true);
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
describe('collision detection', () => {
|
|
413
|
-
test('returns err on duplicate (path, method) pair', () => {
|
|
414
|
-
// "entity.show" derives path /entity/show (dots become slashes)
|
|
415
|
-
// "entity/show" derives path /entity/show (slashes are preserved)
|
|
416
|
-
// Both have intent: read -> GET, so they collide on GET /entity/show
|
|
417
|
-
const dotTrail = trail('entity.show', {
|
|
418
|
-
blaze: () => Result.ok({ dot: true }),
|
|
419
|
-
description: 'Show entity (dot notation)',
|
|
420
|
-
input: z.object({}),
|
|
421
|
-
intent: 'read',
|
|
422
|
-
});
|
|
423
|
-
const slashTrail = trail('entity/show', {
|
|
424
|
-
blaze: () => Result.ok({ slash: true }),
|
|
425
|
-
description: 'Show entity (slash notation)',
|
|
426
|
-
input: z.object({}),
|
|
427
|
-
intent: 'read',
|
|
428
|
-
});
|
|
429
|
-
const app = topo('testapp', { dotTrail, slashTrail });
|
|
430
|
-
const result = buildHttpRoutes(app);
|
|
431
|
-
|
|
432
|
-
expect(result.isErr()).toBe(true);
|
|
433
|
-
expect(result.error).toBeInstanceOf(ValidationError);
|
|
434
|
-
expect(result.error?.message).toContain('GET /entity/show');
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
test('same path with different methods is allowed', () => {
|
|
438
|
-
// "item.resource" derives GET /item/resource (intent: read)
|
|
439
|
-
// "item/resource" derives POST /item/resource (default intent: write)
|
|
440
|
-
// Same path, different methods — no collision
|
|
441
|
-
const getItem = trail('item.resource', {
|
|
442
|
-
blaze: () => Result.ok({ get: true }),
|
|
443
|
-
description: 'Get item',
|
|
444
|
-
input: z.object({}),
|
|
445
|
-
intent: 'read',
|
|
446
|
-
});
|
|
447
|
-
const createItem = trail('item/resource', {
|
|
448
|
-
blaze: () => Result.ok({ created: true }),
|
|
449
|
-
description: 'Create item',
|
|
450
|
-
input: z.object({ name: z.string() }),
|
|
451
|
-
});
|
|
452
|
-
const app = topo('testapp', { createItem, getItem });
|
|
453
|
-
const result = buildHttpRoutes(app);
|
|
454
|
-
|
|
455
|
-
expect(result.isOk()).toBe(true);
|
|
456
|
-
expect(result.value).toHaveLength(2);
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
test('collision error message identifies both trail IDs', () => {
|
|
460
|
-
const dotTrail = trail('entity.show', {
|
|
461
|
-
blaze: () => Result.ok({ one: true }),
|
|
462
|
-
description: 'Trail one',
|
|
463
|
-
input: z.object({}),
|
|
464
|
-
intent: 'read',
|
|
465
|
-
});
|
|
466
|
-
const slashTrail = trail('entity/show', {
|
|
467
|
-
blaze: () => Result.ok({ two: true }),
|
|
468
|
-
description: 'Trail two',
|
|
469
|
-
input: z.object({}),
|
|
470
|
-
intent: 'read',
|
|
471
|
-
});
|
|
472
|
-
const app = topo('testapp', { dotTrail, slashTrail });
|
|
473
|
-
const result = buildHttpRoutes(app);
|
|
474
|
-
|
|
475
|
-
expect(result.isErr()).toBe(true);
|
|
476
|
-
expect(result.error?.message).toContain('entity');
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
describe('established graph enforcement', () => {
|
|
481
|
-
test('returns err when draft contamination remains', () => {
|
|
482
|
-
const draftTrail = trail('entity.export', {
|
|
483
|
-
blaze: () => Result.ok({ ok: true }),
|
|
484
|
-
crosses: ['_draft.entity.prepare'],
|
|
485
|
-
input: z.object({}),
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
const result = buildHttpRoutes(topo('testapp', { draftTrail }));
|
|
489
|
-
|
|
490
|
-
expect(result.isErr()).toBe(true);
|
|
491
|
-
expect(result.error?.message).toMatch(/draft/i);
|
|
492
|
-
});
|
|
493
|
-
});
|
|
494
|
-
});
|