ts-procedures 6.0.0 → 6.0.1
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/agent_config/claude-code/agents/ts-procedures-architect.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +2 -2
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +15 -27
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +11 -4
- package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +2 -2
- package/agent_config/copilot/copilot-instructions.md +3 -2
- package/agent_config/cursor/cursorrules +3 -2
- package/build/implementations/http/doc-registry.d.ts +14 -19
- package/build/implementations/http/doc-registry.js +41 -46
- package/build/implementations/http/doc-registry.js.map +1 -1
- package/build/implementations/http/doc-registry.test.js +141 -10
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/error-taxonomy.d.ts +11 -2
- package/build/implementations/http/error-taxonomy.js +24 -2
- package/build/implementations/http/error-taxonomy.js.map +1 -1
- package/build/implementations/http/route-errors.test.js +5 -6
- package/build/implementations/http/route-errors.test.js.map +1 -1
- package/build/implementations/types.d.ts +13 -1
- package/docs/http-integrations.md +7 -5
- package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
- package/package.json +1 -1
- package/src/implementations/http/README.md +2 -3
- package/src/implementations/http/doc-registry.test.ts +154 -10
- package/src/implementations/http/doc-registry.ts +46 -53
- package/src/implementations/http/error-taxonomy.ts +26 -2
- package/src/implementations/http/express-rpc/README.md +2 -2
- package/src/implementations/http/hono-rpc/README.md +2 -2
- package/src/implementations/http/route-errors.test.ts +5 -6
- package/src/implementations/types.ts +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-procedures",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.",
|
|
5
5
|
"main": "build/exports.js",
|
|
6
6
|
"types": "build/exports.d.ts",
|
|
@@ -257,8 +257,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
|
|
|
257
257
|
|
|
258
258
|
const docs = new DocRegistry({
|
|
259
259
|
basePath: '/api',
|
|
260
|
-
|
|
261
|
-
errors: DocRegistry.defaultErrors(),
|
|
260
|
+
errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged and deduped
|
|
262
261
|
})
|
|
263
262
|
.from(rpcBuilder)
|
|
264
263
|
.from(apiBuilder)
|
|
@@ -269,7 +268,7 @@ app.get('/docs', (c) => c.json(docs.toJSON()))
|
|
|
269
268
|
|
|
270
269
|
- `from()` stores a reference — routes are read lazily at `toJSON()` time
|
|
271
270
|
- `toJSON()` supports optional `filter` and `transform` options
|
|
272
|
-
- `
|
|
271
|
+
- `errors` accepts an `ErrorTaxonomy` or raw `ErrorDoc[]`; framework defaults are auto-merged (opt out via `includeDefaults: false`)
|
|
273
272
|
- All builders satisfy the `DocSource` interface (`{ readonly docs: AnyHttpRouteDoc[] }`)
|
|
274
273
|
|
|
275
274
|
### Client Code Generation
|
|
@@ -3,6 +3,7 @@ import { v } from 'suretype'
|
|
|
3
3
|
import { Procedures } from '../../index.js'
|
|
4
4
|
import { HonoRPCAppBuilder } from './hono-rpc/index.js'
|
|
5
5
|
import { DocRegistry } from './doc-registry.js'
|
|
6
|
+
import { defineErrorTaxonomy } from './error-taxonomy.js'
|
|
6
7
|
import type {
|
|
7
8
|
AnyHttpRouteDoc,
|
|
8
9
|
RPCHttpRouteDoc,
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
StreamHttpRouteDoc,
|
|
12
13
|
DocSource,
|
|
13
14
|
DocEnvelope,
|
|
15
|
+
ErrorDoc,
|
|
14
16
|
} from '../types.js'
|
|
15
17
|
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
@@ -61,7 +63,7 @@ describe('DocRegistry', () => {
|
|
|
61
63
|
// --------------------------------------------------------------------------
|
|
62
64
|
describe('constructor', () => {
|
|
63
65
|
test('uses defaults when no config provided', () => {
|
|
64
|
-
const registry = new DocRegistry()
|
|
66
|
+
const registry = new DocRegistry({ includeDefaults: false })
|
|
65
67
|
const out = registry.toJSON()
|
|
66
68
|
expect(out.basePath).toBe('')
|
|
67
69
|
expect(out.headers).toEqual([])
|
|
@@ -69,7 +71,7 @@ describe('DocRegistry', () => {
|
|
|
69
71
|
})
|
|
70
72
|
|
|
71
73
|
test('accepts partial config', () => {
|
|
72
|
-
const registry = new DocRegistry({ basePath: '/v1' })
|
|
74
|
+
const registry = new DocRegistry({ basePath: '/v1', includeDefaults: false })
|
|
73
75
|
const out = registry.toJSON()
|
|
74
76
|
expect(out.basePath).toBe('/v1')
|
|
75
77
|
expect(out.headers).toEqual([])
|
|
@@ -79,7 +81,7 @@ describe('DocRegistry', () => {
|
|
|
79
81
|
test('accepts full config', () => {
|
|
80
82
|
const headers = [{ name: 'Authorization', description: 'Bearer token', required: true }]
|
|
81
83
|
const errors = [{ name: 'Unauthorized', statusCode: 401, description: 'Missing token' }]
|
|
82
|
-
const registry = new DocRegistry({ basePath: '/api', headers, errors })
|
|
84
|
+
const registry = new DocRegistry({ basePath: '/api', headers, errors, includeDefaults: false })
|
|
83
85
|
const out = registry.toJSON()
|
|
84
86
|
expect(out.basePath).toBe('/api')
|
|
85
87
|
expect(out.headers).toEqual(headers)
|
|
@@ -175,7 +177,7 @@ describe('DocRegistry', () => {
|
|
|
175
177
|
test('headers and errors are copies', () => {
|
|
176
178
|
const headers = [{ name: 'X-Custom' }]
|
|
177
179
|
const errors = [{ name: 'E', statusCode: 500, description: 'd' }]
|
|
178
|
-
const registry = new DocRegistry({ headers, errors })
|
|
180
|
+
const registry = new DocRegistry({ headers, errors, includeDefaults: false })
|
|
179
181
|
const out = registry.toJSON()
|
|
180
182
|
expect(out.headers).toEqual(headers)
|
|
181
183
|
expect(out.headers).not.toBe(headers)
|
|
@@ -298,6 +300,153 @@ describe('DocRegistry', () => {
|
|
|
298
300
|
})
|
|
299
301
|
})
|
|
300
302
|
|
|
303
|
+
// --------------------------------------------------------------------------
|
|
304
|
+
// taxonomy input + auto-defaults + dedupe (v6.0.1 simplification)
|
|
305
|
+
// --------------------------------------------------------------------------
|
|
306
|
+
describe('errors: ErrorTaxonomy (polymorphic constructor input)', () => {
|
|
307
|
+
test('accepts an ErrorTaxonomy directly and converts to ErrorDoc[]', () => {
|
|
308
|
+
const taxonomy = defineErrorTaxonomy({
|
|
309
|
+
AuthError: {
|
|
310
|
+
class: class AuthError extends Error {},
|
|
311
|
+
statusCode: 401,
|
|
312
|
+
description: 'unauthenticated',
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
const envelope = new DocRegistry({ errors: taxonomy }).toJSON()
|
|
316
|
+
const auth = envelope.errors.find((e) => e.name === 'AuthError')
|
|
317
|
+
expect(auth).toBeDefined()
|
|
318
|
+
expect(auth?.statusCode).toBe(401)
|
|
319
|
+
expect(auth?.description).toBe('unauthenticated')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('auto-includes framework defaults when errors is a taxonomy', () => {
|
|
323
|
+
const taxonomy = defineErrorTaxonomy({
|
|
324
|
+
AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
|
|
325
|
+
})
|
|
326
|
+
const names = new DocRegistry({ errors: taxonomy }).toJSON().errors.map((e) => e.name)
|
|
327
|
+
expect(names).toContain('AuthError')
|
|
328
|
+
expect(names).toContain('ProcedureValidationError')
|
|
329
|
+
expect(names).toContain('ProcedureYieldValidationError')
|
|
330
|
+
expect(names).toContain('ProcedureError')
|
|
331
|
+
expect(names).toContain('ProcedureRegistrationError')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('auto-includes framework defaults when errors is ErrorDoc[]', () => {
|
|
335
|
+
const custom: ErrorDoc = { name: 'CustomThing', statusCode: 418, description: 'teapot' }
|
|
336
|
+
const names = new DocRegistry({ errors: [custom] }).toJSON().errors.map((e) => e.name)
|
|
337
|
+
expect(names).toContain('CustomThing')
|
|
338
|
+
expect(names).toContain('ProcedureValidationError')
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('includeDefaults: false omits framework defaults', () => {
|
|
342
|
+
const taxonomy = defineErrorTaxonomy({
|
|
343
|
+
AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
|
|
344
|
+
})
|
|
345
|
+
const names = new DocRegistry({ errors: taxonomy, includeDefaults: false })
|
|
346
|
+
.toJSON()
|
|
347
|
+
.errors.map((e) => e.name)
|
|
348
|
+
expect(names).toEqual(['AuthError'])
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('user taxonomy entry with same name as default overrides default (dedupe, user wins)', () => {
|
|
352
|
+
const taxonomy = defineErrorTaxonomy({
|
|
353
|
+
ProcedureError: {
|
|
354
|
+
class: Error,
|
|
355
|
+
statusCode: 418,
|
|
356
|
+
description: 'custom override',
|
|
357
|
+
},
|
|
358
|
+
})
|
|
359
|
+
const envelope = new DocRegistry({ errors: taxonomy }).toJSON()
|
|
360
|
+
const proc = envelope.errors.filter((e) => e.name === 'ProcedureError')
|
|
361
|
+
expect(proc).toHaveLength(1)
|
|
362
|
+
expect(proc[0]!.statusCode).toBe(418)
|
|
363
|
+
expect(proc[0]!.description).toBe('custom override')
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test('user ErrorDoc with same name as default overrides default (dedupe, user wins)', () => {
|
|
367
|
+
const custom: ErrorDoc = {
|
|
368
|
+
name: 'ProcedureError',
|
|
369
|
+
statusCode: 418,
|
|
370
|
+
description: 'custom override',
|
|
371
|
+
}
|
|
372
|
+
const envelope = new DocRegistry({ errors: [custom] }).toJSON()
|
|
373
|
+
const proc = envelope.errors.filter((e) => e.name === 'ProcedureError')
|
|
374
|
+
expect(proc).toHaveLength(1)
|
|
375
|
+
expect(proc[0]!.statusCode).toBe(418)
|
|
376
|
+
expect(proc[0]!.description).toBe('custom override')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('empty errors config still returns framework defaults', () => {
|
|
380
|
+
const names = new DocRegistry().toJSON().errors.map((e) => e.name)
|
|
381
|
+
expect(names).toContain('ProcedureError')
|
|
382
|
+
expect(names).toContain('ProcedureValidationError')
|
|
383
|
+
expect(names).toContain('ProcedureYieldValidationError')
|
|
384
|
+
expect(names).toContain('ProcedureRegistrationError')
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
test('includeDefaults: false with no errors produces empty error list', () => {
|
|
388
|
+
const envelope = new DocRegistry({ includeDefaults: false }).toJSON()
|
|
389
|
+
expect(envelope.errors).toEqual([])
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
// --------------------------------------------------------------------------
|
|
394
|
+
// .documentError() fluent extension
|
|
395
|
+
// --------------------------------------------------------------------------
|
|
396
|
+
describe('.documentError()', () => {
|
|
397
|
+
test('adds a single ErrorDoc to the envelope', () => {
|
|
398
|
+
const registry = new DocRegistry({ includeDefaults: false }).documentError({
|
|
399
|
+
name: 'RateLimitExceeded',
|
|
400
|
+
statusCode: 429,
|
|
401
|
+
description: 'too many requests',
|
|
402
|
+
})
|
|
403
|
+
const names = registry.toJSON().errors.map((e) => e.name)
|
|
404
|
+
expect(names).toEqual(['RateLimitExceeded'])
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('accepts multiple docs via variadic args', () => {
|
|
408
|
+
const registry = new DocRegistry({ includeDefaults: false }).documentError(
|
|
409
|
+
{ name: 'A', statusCode: 400, description: 'a' },
|
|
410
|
+
{ name: 'B', statusCode: 500, description: 'b' }
|
|
411
|
+
)
|
|
412
|
+
const names = registry.toJSON().errors.map((e) => e.name)
|
|
413
|
+
expect(names).toEqual(['A', 'B'])
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('returns this for chaining', () => {
|
|
417
|
+
const registry = new DocRegistry()
|
|
418
|
+
const returned = registry.documentError({
|
|
419
|
+
name: 'Foo',
|
|
420
|
+
statusCode: 500,
|
|
421
|
+
description: 'x',
|
|
422
|
+
})
|
|
423
|
+
expect(returned).toBe(registry)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test('composes with taxonomy input — extends docs without replacing', () => {
|
|
427
|
+
const taxonomy = defineErrorTaxonomy({
|
|
428
|
+
AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
|
|
429
|
+
})
|
|
430
|
+
const envelope = new DocRegistry({ errors: taxonomy })
|
|
431
|
+
.documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'x' })
|
|
432
|
+
.toJSON()
|
|
433
|
+
const names = envelope.errors.map((e) => e.name)
|
|
434
|
+
expect(names).toContain('AuthError')
|
|
435
|
+
expect(names).toContain('RateLimitExceeded')
|
|
436
|
+
expect(names).toContain('ProcedureValidationError')
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
test('dedupes against existing errors (last write wins)', () => {
|
|
440
|
+
const registry = new DocRegistry({ includeDefaults: false })
|
|
441
|
+
.documentError({ name: 'Foo', statusCode: 400, description: 'first' })
|
|
442
|
+
.documentError({ name: 'Foo', statusCode: 500, description: 'second' })
|
|
443
|
+
const foo = registry.toJSON().errors.filter((e) => e.name === 'Foo')
|
|
444
|
+
expect(foo).toHaveLength(1)
|
|
445
|
+
expect(foo[0]!.statusCode).toBe(500)
|
|
446
|
+
expect(foo[0]!.description).toBe('second')
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
301
450
|
// --------------------------------------------------------------------------
|
|
302
451
|
// kind discriminant
|
|
303
452
|
// --------------------------------------------------------------------------
|
|
@@ -333,7 +482,6 @@ describe('DocRegistry', () => {
|
|
|
333
482
|
const registry = new DocRegistry({
|
|
334
483
|
basePath: '/api',
|
|
335
484
|
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
336
|
-
errors: DocRegistry.defaultErrors(),
|
|
337
485
|
})
|
|
338
486
|
.from(makeSource([rpcDoc]))
|
|
339
487
|
.from(makeSource([apiDoc]))
|
|
@@ -347,10 +495,7 @@ describe('DocRegistry', () => {
|
|
|
347
495
|
})
|
|
348
496
|
|
|
349
497
|
test('filter + transform combined', () => {
|
|
350
|
-
const registry = new DocRegistry({
|
|
351
|
-
basePath: '/api',
|
|
352
|
-
errors: DocRegistry.defaultErrors(),
|
|
353
|
-
})
|
|
498
|
+
const registry = new DocRegistry({ basePath: '/api' })
|
|
354
499
|
.from(makeSource([rpcDoc]))
|
|
355
500
|
.from(makeSource([apiDoc]))
|
|
356
501
|
.from(makeSource([streamDoc]))
|
|
@@ -372,7 +517,6 @@ describe('DocRegistry', () => {
|
|
|
372
517
|
const registry = new DocRegistry({
|
|
373
518
|
basePath: '/api',
|
|
374
519
|
headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
|
|
375
|
-
errors: DocRegistry.defaultErrors(),
|
|
376
520
|
})
|
|
377
521
|
.from(makeSource([rpcDoc]))
|
|
378
522
|
.from(makeSource([apiDoc]))
|
|
@@ -8,9 +8,10 @@ import type {
|
|
|
8
8
|
HeaderDoc,
|
|
9
9
|
} from '../types.js'
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
PROCEDURE_REGISTRATION_ERROR_DOC,
|
|
12
12
|
defaultErrorTaxonomy,
|
|
13
13
|
taxonomyToErrorDocs,
|
|
14
|
+
type ErrorTaxonomy,
|
|
14
15
|
} from './error-taxonomy.js'
|
|
15
16
|
|
|
16
17
|
export type {
|
|
@@ -23,37 +24,44 @@ export type {
|
|
|
23
24
|
HeaderDoc,
|
|
24
25
|
} from '../types.js'
|
|
25
26
|
|
|
27
|
+
function isTaxonomy(input: ErrorTaxonomy | ErrorDoc[]): input is ErrorTaxonomy {
|
|
28
|
+
return !Array.isArray(input)
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
* here so consumers still see it in the error catalog.
|
|
32
|
+
* Dedupes ErrorDocs by `name`, last occurrence wins. Map insertion order
|
|
33
|
+
* preserves the latest position of each name.
|
|
30
34
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'An invalid schema or configuration was detected at procedure registration time.',
|
|
36
|
-
schema: {
|
|
37
|
-
type: 'object',
|
|
38
|
-
properties: {
|
|
39
|
-
name: { type: 'string', const: 'ProcedureRegistrationError' },
|
|
40
|
-
procedureName: { type: 'string' },
|
|
41
|
-
message: { type: 'string' },
|
|
42
|
-
},
|
|
43
|
-
required: ['name', 'procedureName', 'message'],
|
|
44
|
-
},
|
|
35
|
+
function dedupeByName(docs: ErrorDoc[]): ErrorDoc[] {
|
|
36
|
+
const byName = new Map<string, ErrorDoc>()
|
|
37
|
+
for (const doc of docs) byName.set(doc.name, doc)
|
|
38
|
+
return Array.from(byName.values())
|
|
45
39
|
}
|
|
46
40
|
|
|
47
41
|
export class DocRegistry {
|
|
48
42
|
private readonly basePath: string
|
|
49
43
|
private readonly headers: HeaderDoc[]
|
|
50
|
-
private
|
|
44
|
+
private errors: ErrorDoc[]
|
|
51
45
|
private readonly sources: DocSource<AnyHttpRouteDoc>[] = []
|
|
52
46
|
|
|
53
47
|
constructor(config?: DocRegistryConfig) {
|
|
54
48
|
this.basePath = config?.basePath ?? ''
|
|
55
49
|
this.headers = config?.headers ?? []
|
|
56
|
-
|
|
50
|
+
|
|
51
|
+
const includeDefaults = config?.includeDefaults ?? true
|
|
52
|
+
const userErrors: ErrorDoc[] = config?.errors
|
|
53
|
+
? isTaxonomy(config.errors)
|
|
54
|
+
? taxonomyToErrorDocs(config.errors)
|
|
55
|
+
: config.errors
|
|
56
|
+
: []
|
|
57
|
+
|
|
58
|
+
// Precedence: defaults come first, user errors override via dedupe
|
|
59
|
+
// (last-write-wins). Matches runtime resolution order.
|
|
60
|
+
const merged = includeDefaults
|
|
61
|
+
? [...DocRegistry.defaultErrors(), ...userErrors]
|
|
62
|
+
: userErrors
|
|
63
|
+
|
|
64
|
+
this.errors = dedupeByName(merged)
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
from(source: DocSource<AnyHttpRouteDoc>): this {
|
|
@@ -61,6 +69,18 @@ export class DocRegistry {
|
|
|
61
69
|
return this
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Adds one or more {@link ErrorDoc} entries to the envelope. Use for errors
|
|
74
|
+
* outside your runtime taxonomy — middleware-level errors, infrastructure
|
|
75
|
+
* errors (502/503/504), or doc-only meta errors.
|
|
76
|
+
*
|
|
77
|
+
* Deduped by `name` — last write wins.
|
|
78
|
+
*/
|
|
79
|
+
documentError(...docs: ErrorDoc[]): this {
|
|
80
|
+
this.errors = dedupeByName([...this.errors, ...docs])
|
|
81
|
+
return this
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T {
|
|
65
85
|
let routes = this.sources.flatMap((source) => source.docs)
|
|
66
86
|
|
|
@@ -83,11 +103,12 @@ export class DocRegistry {
|
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
/**
|
|
86
|
-
* Framework error defaults
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
106
|
+
* Framework error defaults — derived from {@link defaultErrorTaxonomy} plus
|
|
107
|
+
* `ProcedureRegistrationError` (which is thrown only at registration time
|
|
108
|
+
* and therefore lives in the catalog, not the runtime taxonomy).
|
|
109
|
+
*
|
|
110
|
+
* Most consumers do not need to call this directly — the `DocRegistry`
|
|
111
|
+
* constructor auto-includes these unless `includeDefaults: false` is passed.
|
|
91
112
|
*/
|
|
92
113
|
static defaultErrors(): ErrorDoc[] {
|
|
93
114
|
return [
|
|
@@ -95,32 +116,4 @@ export class DocRegistry {
|
|
|
95
116
|
PROCEDURE_REGISTRATION_ERROR_DOC,
|
|
96
117
|
]
|
|
97
118
|
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Convenience constructor that seeds `config.errors` from a taxonomy so the
|
|
101
|
-
* DocEnvelope automatically documents every error class registered with the
|
|
102
|
-
* HTTP builders. Framework defaults (including `ProcedureRegistrationError`)
|
|
103
|
-
* are included unless `includeDefaults: false` is passed.
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* const registry = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
|
|
107
|
-
* .from(apiApp)
|
|
108
|
-
*/
|
|
109
|
-
static fromTaxonomy(
|
|
110
|
-
taxonomy: ErrorTaxonomy,
|
|
111
|
-
config?: Omit<DocRegistryConfig, 'errors'> & { includeDefaults?: boolean }
|
|
112
|
-
): DocRegistry {
|
|
113
|
-
const { includeDefaults = true, ...rest } = config ?? {}
|
|
114
|
-
const errors: ErrorDoc[] = [
|
|
115
|
-
...taxonomyToErrorDocs(taxonomy),
|
|
116
|
-
...(includeDefaults ? DocRegistry.defaultErrors() : []),
|
|
117
|
-
]
|
|
118
|
-
// Dedupe by name — user entries take precedence over defaults with the
|
|
119
|
-
// same key, matching runtime resolution order.
|
|
120
|
-
const seen = new Set<string>()
|
|
121
|
-
const deduped = errors.filter((e) =>
|
|
122
|
-
seen.has(e.name) ? false : (seen.add(e.name), true)
|
|
123
|
-
)
|
|
124
|
-
return new DocRegistry({ ...rest, errors: deduped })
|
|
125
|
-
}
|
|
126
119
|
}
|
|
@@ -182,10 +182,34 @@ export const defaultErrorTaxonomy = defineErrorTaxonomy({
|
|
|
182
182
|
},
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Doc-only entry for `ProcedureRegistrationError`, which is thrown at
|
|
187
|
+
* procedure-definition time (never at request time) and therefore doesn't
|
|
188
|
+
* appear in the runtime taxonomy. Consumers still see it in the error catalog
|
|
189
|
+
* via `DocRegistry.defaultErrors()`.
|
|
190
|
+
*/
|
|
191
|
+
export const PROCEDURE_REGISTRATION_ERROR_DOC: ErrorDoc = {
|
|
192
|
+
name: 'ProcedureRegistrationError',
|
|
193
|
+
statusCode: 500,
|
|
194
|
+
description:
|
|
195
|
+
'An invalid schema or configuration was detected at procedure registration time.',
|
|
196
|
+
schema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
name: { type: 'string', const: 'ProcedureRegistrationError' },
|
|
200
|
+
procedureName: { type: 'string' },
|
|
201
|
+
message: { type: 'string' },
|
|
202
|
+
},
|
|
203
|
+
required: ['name', 'procedureName', 'message'],
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
185
207
|
/**
|
|
186
208
|
* Converts a taxonomy into {@link ErrorDoc} objects suitable for a DocEnvelope.
|
|
187
|
-
*
|
|
188
|
-
*
|
|
209
|
+
*
|
|
210
|
+
* @internal Used by `DocRegistry` to merge taxonomy entries into the envelope.
|
|
211
|
+
* Consumers should pass their taxonomy directly to `new DocRegistry({ errors: taxonomy })`
|
|
212
|
+
* rather than calling this helper — the constructor handles the conversion.
|
|
189
213
|
*/
|
|
190
214
|
export function taxonomyToErrorDocs(taxonomy: ErrorTaxonomy): ErrorDoc[] {
|
|
191
215
|
return Object.entries(taxonomy).map(([key, entry]) => ({
|
|
@@ -59,7 +59,7 @@ type ExpressRPCAppBuilderConfig = {
|
|
|
59
59
|
onRequestStart?: (req: express.Request) => void
|
|
60
60
|
onRequestEnd?: (req: express.Request, res: express.Response) => void
|
|
61
61
|
onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void
|
|
62
|
-
|
|
62
|
+
onError?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void
|
|
63
63
|
}
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -70,7 +70,7 @@ type ExpressRPCAppBuilderConfig = {
|
|
|
70
70
|
| `onRequestStart` | `(req) => void` | Called at start of each request |
|
|
71
71
|
| `onRequestEnd` | `(req, res) => void` | Called after response finishes |
|
|
72
72
|
| `onSuccess` | `(proc, req, res) => void` | Called on successful handler execution |
|
|
73
|
-
| `
|
|
73
|
+
| `onError` | `(proc, req, res, err) => void` | Imperative error handler (peer of `errors` taxonomy — see Error Handling) |
|
|
74
74
|
|
|
75
75
|
## Context Resolution
|
|
76
76
|
|
|
@@ -64,7 +64,7 @@ type HonoRPCAppBuilderConfig = {
|
|
|
64
64
|
onRequestStart?: (c: Context) => void
|
|
65
65
|
onRequestEnd?: (c: Context) => void
|
|
66
66
|
onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
|
|
67
|
-
|
|
67
|
+
onError?: (
|
|
68
68
|
procedure: TProcedureRegistration,
|
|
69
69
|
c: Context,
|
|
70
70
|
error: Error
|
|
@@ -79,7 +79,7 @@ type HonoRPCAppBuilderConfig = {
|
|
|
79
79
|
| `onRequestStart` | `(c) => void` | Called at start of each request |
|
|
80
80
|
| `onRequestEnd` | `(c) => void` | Called after handler completes |
|
|
81
81
|
| `onSuccess` | `(proc, c) => void` | Called on successful handler execution |
|
|
82
|
-
| `
|
|
82
|
+
| `onError` | `(proc, c, err) => Response` | Imperative error handler (peer of `errors` taxonomy — see Error Handling) |
|
|
83
83
|
|
|
84
84
|
## Context Resolution
|
|
85
85
|
|
|
@@ -117,9 +117,9 @@ describe('per-route errors declaration', () => {
|
|
|
117
117
|
})
|
|
118
118
|
})
|
|
119
119
|
|
|
120
|
-
describe('DocRegistry
|
|
120
|
+
describe('DocRegistry with taxonomy input', () => {
|
|
121
121
|
test('seeds envelope errors from the taxonomy + framework defaults', () => {
|
|
122
|
-
const registry = DocRegistry
|
|
122
|
+
const registry = new DocRegistry({ errors: appErrors, basePath: '/api' })
|
|
123
123
|
const envelope = registry.toJSON()
|
|
124
124
|
const names = envelope.errors.map((e) => e.name)
|
|
125
125
|
expect(names).toContain('UseCaseError')
|
|
@@ -130,7 +130,7 @@ describe('DocRegistry.fromTaxonomy', () => {
|
|
|
130
130
|
})
|
|
131
131
|
|
|
132
132
|
test('includeDefaults: false omits framework entries', () => {
|
|
133
|
-
const registry = DocRegistry
|
|
133
|
+
const registry = new DocRegistry({ errors: appErrors, includeDefaults: false })
|
|
134
134
|
const names = registry.toJSON().errors.map((e) => e.name)
|
|
135
135
|
expect(names).toEqual(['UseCaseError', 'AuthError'])
|
|
136
136
|
})
|
|
@@ -143,11 +143,10 @@ describe('DocRegistry.fromTaxonomy', () => {
|
|
|
143
143
|
description: 'custom override',
|
|
144
144
|
},
|
|
145
145
|
})
|
|
146
|
-
const envelope = DocRegistry
|
|
146
|
+
const envelope = new DocRegistry({ errors: overridden }).toJSON()
|
|
147
147
|
const proc = envelope.errors.find((e) => e.name === 'ProcedureError')
|
|
148
148
|
expect(proc?.statusCode).toBe(418)
|
|
149
149
|
expect(proc?.description).toBe('custom override')
|
|
150
|
-
// no duplicates
|
|
151
150
|
const count = envelope.errors.filter((e) => e.name === 'ProcedureError').length
|
|
152
151
|
expect(count).toBe(1)
|
|
153
152
|
})
|
|
@@ -170,7 +169,7 @@ describe('DocRegistry.fromTaxonomy', () => {
|
|
|
170
169
|
const app = new HonoAPIAppBuilder().register(API, () => ({}))
|
|
171
170
|
app.build()
|
|
172
171
|
|
|
173
|
-
const envelope = DocRegistry
|
|
172
|
+
const envelope = new DocRegistry({ errors: appErrors }).from(app).toJSON()
|
|
174
173
|
const route = envelope.routes.find((r) => r.kind === 'api' && r.name === 'GetUser')
|
|
175
174
|
expect(route?.errors).toEqual(['UseCaseError'])
|
|
176
175
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Procedures } from '../index.js'
|
|
2
|
+
import type { ErrorTaxonomy } from './http/error-taxonomy.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @typeParam TErrorKey - Union of valid taxonomy keys. Defaults to `string`
|
|
@@ -186,7 +187,18 @@ export interface ErrorDoc {
|
|
|
186
187
|
export interface DocRegistryConfig {
|
|
187
188
|
basePath?: string
|
|
188
189
|
headers?: HeaderDoc[]
|
|
189
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Errors to document in the envelope. Accepts either your runtime
|
|
192
|
+
* {@link ErrorTaxonomy} (from `defineErrorTaxonomy`) — the common case — or
|
|
193
|
+
* a raw `ErrorDoc[]` for consumers who aren't using a taxonomy.
|
|
194
|
+
*
|
|
195
|
+
* Framework defaults (`ProcedureError`, `ProcedureValidationError`,
|
|
196
|
+
* `ProcedureYieldValidationError`, `ProcedureRegistrationError`) are merged
|
|
197
|
+
* in automatically and deduped. Opt out via `includeDefaults: false`.
|
|
198
|
+
*/
|
|
199
|
+
errors?: ErrorTaxonomy | ErrorDoc[]
|
|
200
|
+
/** Whether to auto-merge framework error defaults. Defaults to `true`. */
|
|
201
|
+
includeDefaults?: boolean
|
|
190
202
|
}
|
|
191
203
|
|
|
192
204
|
export interface DocRegistryOutputOptions<TEnvelope = DocEnvelope> {
|