ts-procedures 5.16.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/README.md +2 -0
- package/agent_config/claude-code/agents/ts-procedures-architect.md +13 -6
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +26 -4
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +87 -19
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +162 -16
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +179 -16
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +20 -12
- package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +2 -1
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +22 -15
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +20 -17
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +20 -16
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +20 -17
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +16 -3
- package/agent_config/copilot/copilot-instructions.md +78 -12
- package/agent_config/cursor/cursorrules +78 -12
- package/build/client/call.d.ts +2 -1
- package/build/client/call.js +9 -1
- package/build/client/call.js.map +1 -1
- package/build/client/error-dispatch.d.ts +13 -0
- package/build/client/error-dispatch.js +26 -0
- package/build/client/error-dispatch.js.map +1 -0
- package/build/client/error-dispatch.test.d.ts +1 -0
- package/build/client/error-dispatch.test.js +56 -0
- package/build/client/error-dispatch.test.js.map +1 -0
- package/build/client/fetch-adapter.js +10 -4
- package/build/client/fetch-adapter.js.map +1 -1
- package/build/client/index.d.ts +2 -1
- package/build/client/index.js +5 -1
- package/build/client/index.js.map +1 -1
- package/build/client/stream.d.ts +2 -1
- package/build/client/stream.js +13 -3
- package/build/client/stream.js.map +1 -1
- package/build/client/typed-error-dispatch.test.d.ts +1 -0
- package/build/client/typed-error-dispatch.test.js +168 -0
- package/build/client/typed-error-dispatch.test.js.map +1 -0
- package/build/client/types.d.ts +37 -0
- package/build/codegen/e2e.test.js +9 -4
- package/build/codegen/e2e.test.js.map +1 -1
- package/build/codegen/emit-client-runtime.js +4 -0
- package/build/codegen/emit-client-runtime.js.map +1 -1
- package/build/codegen/emit-errors.d.ts +17 -6
- package/build/codegen/emit-errors.integration.test.d.ts +1 -0
- package/build/codegen/emit-errors.integration.test.js +162 -0
- package/build/codegen/emit-errors.integration.test.js.map +1 -0
- package/build/codegen/emit-errors.js +50 -39
- package/build/codegen/emit-errors.js.map +1 -1
- package/build/codegen/emit-errors.test.js +75 -78
- package/build/codegen/emit-errors.test.js.map +1 -1
- package/build/codegen/emit-index.d.ts +7 -0
- package/build/codegen/emit-index.js +26 -4
- package/build/codegen/emit-index.js.map +1 -1
- package/build/codegen/emit-index.test.js +55 -23
- package/build/codegen/emit-index.test.js.map +1 -1
- package/build/codegen/emit-scope.d.ts +8 -0
- package/build/codegen/emit-scope.js +82 -7
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/pipeline.js +22 -2
- package/build/codegen/pipeline.js.map +1 -1
- package/build/implementations/http/doc-registry.d.ts +17 -1
- package/build/implementations/http/doc-registry.js +47 -79
- package/build/implementations/http/doc-registry.js.map +1 -1
- package/build/implementations/http/doc-registry.test.js +149 -16
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/error-taxonomy.d.ts +249 -0
- package/build/implementations/http/error-taxonomy.js +252 -0
- package/build/implementations/http/error-taxonomy.js.map +1 -0
- package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/error-taxonomy.test.js +399 -0
- package/build/implementations/http/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +39 -8
- package/build/implementations/http/express-rpc/index.js +39 -8
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-api/index.d.ts +38 -1
- package/build/implementations/http/hono-api/index.js +32 -0
- package/build/implementations/http/hono-api/index.js.map +1 -1
- package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.d.ts +34 -7
- package/build/implementations/http/hono-rpc/index.js +31 -4
- package/build/implementations/http/hono-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js +87 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-stream/index.d.ts +40 -3
- package/build/implementations/http/hono-stream/index.js +37 -10
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +45 -18
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/build/implementations/http/on-request-error.test.d.ts +1 -0
- package/build/implementations/http/on-request-error.test.js +173 -0
- package/build/implementations/http/on-request-error.test.js.map +1 -0
- package/build/implementations/http/route-errors.test.d.ts +1 -0
- package/build/implementations/http/route-errors.test.js +139 -0
- package/build/implementations/http/route-errors.test.js.map +1 -0
- package/build/implementations/types.d.ts +43 -3
- package/docs/client-and-codegen.md +105 -12
- package/docs/core.md +14 -5
- package/docs/http-integrations.md +138 -5
- package/docs/streaming.md +3 -1
- package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
- package/package.json +7 -2
- package/src/client/call.ts +10 -1
- package/src/client/error-dispatch.test.ts +72 -0
- package/src/client/error-dispatch.ts +27 -0
- package/src/client/fetch-adapter.ts +11 -5
- package/src/client/index.ts +9 -0
- package/src/client/stream.ts +14 -3
- package/src/client/typed-error-dispatch.test.ts +211 -0
- package/src/client/types.ts +42 -0
- package/src/codegen/e2e.test.ts +9 -4
- package/src/codegen/emit-client-runtime.ts +4 -0
- package/src/codegen/emit-errors.integration.test.ts +183 -0
- package/src/codegen/emit-errors.test.ts +91 -87
- package/src/codegen/emit-errors.ts +123 -41
- package/src/codegen/emit-index.test.ts +68 -24
- package/src/codegen/emit-index.ts +66 -4
- package/src/codegen/emit-scope.ts +124 -7
- package/src/codegen/pipeline.ts +25 -2
- package/src/implementations/http/README.md +21 -7
- package/src/implementations/http/doc-registry.test.ts +164 -16
- package/src/implementations/http/doc-registry.ts +58 -82
- package/src/implementations/http/error-taxonomy.test.ts +438 -0
- package/src/implementations/http/error-taxonomy.ts +361 -0
- package/src/implementations/http/express-rpc/README.md +23 -24
- package/src/implementations/http/express-rpc/error-taxonomy.test.ts +103 -0
- package/src/implementations/http/express-rpc/index.ts +75 -14
- package/src/implementations/http/hono-api/README.md +284 -0
- package/src/implementations/http/hono-api/error-taxonomy.test.ts +179 -0
- package/src/implementations/http/hono-api/index.ts +76 -1
- package/src/implementations/http/hono-rpc/README.md +20 -21
- package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +82 -0
- package/src/implementations/http/hono-rpc/index.ts +65 -9
- package/src/implementations/http/hono-stream/README.md +44 -25
- package/src/implementations/http/hono-stream/error-taxonomy.test.ts +98 -0
- package/src/implementations/http/hono-stream/index.test.ts +54 -18
- package/src/implementations/http/hono-stream/index.ts +83 -13
- package/src/implementations/http/on-request-error.test.ts +201 -0
- package/src/implementations/http/route-errors.test.ts +176 -0
- package/src/implementations/types.ts +43 -3
|
@@ -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)
|
|
@@ -263,20 +265,24 @@ describe('DocRegistry', () => {
|
|
|
263
265
|
|
|
264
266
|
test('has correct error names', () => {
|
|
265
267
|
const names = DocRegistry.defaultErrors().map((e) => e.name)
|
|
268
|
+
// Runtime errors come from the taxonomy (topologically sorted —
|
|
269
|
+
// subclasses first), followed by the doc-only registration error.
|
|
266
270
|
expect(names).toEqual([
|
|
267
|
-
'ProcedureError',
|
|
268
271
|
'ProcedureValidationError',
|
|
269
272
|
'ProcedureYieldValidationError',
|
|
273
|
+
'ProcedureError',
|
|
270
274
|
'ProcedureRegistrationError',
|
|
271
275
|
])
|
|
272
276
|
})
|
|
273
277
|
|
|
274
278
|
test('has correct status codes', () => {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
expect(
|
|
279
|
-
expect(
|
|
279
|
+
const byName = Object.fromEntries(
|
|
280
|
+
DocRegistry.defaultErrors().map((e) => [e.name, e.statusCode])
|
|
281
|
+
)
|
|
282
|
+
expect(byName.ProcedureError).toBe(500)
|
|
283
|
+
expect(byName.ProcedureValidationError).toBe(400)
|
|
284
|
+
expect(byName.ProcedureYieldValidationError).toBe(500)
|
|
285
|
+
expect(byName.ProcedureRegistrationError).toBe(500)
|
|
280
286
|
})
|
|
281
287
|
|
|
282
288
|
test('each entry has schema', () => {
|
|
@@ -294,6 +300,153 @@ describe('DocRegistry', () => {
|
|
|
294
300
|
})
|
|
295
301
|
})
|
|
296
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
|
+
|
|
297
450
|
// --------------------------------------------------------------------------
|
|
298
451
|
// kind discriminant
|
|
299
452
|
// --------------------------------------------------------------------------
|
|
@@ -329,7 +482,6 @@ describe('DocRegistry', () => {
|
|
|
329
482
|
const registry = new DocRegistry({
|
|
330
483
|
basePath: '/api',
|
|
331
484
|
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
332
|
-
errors: DocRegistry.defaultErrors(),
|
|
333
485
|
})
|
|
334
486
|
.from(makeSource([rpcDoc]))
|
|
335
487
|
.from(makeSource([apiDoc]))
|
|
@@ -343,10 +495,7 @@ describe('DocRegistry', () => {
|
|
|
343
495
|
})
|
|
344
496
|
|
|
345
497
|
test('filter + transform combined', () => {
|
|
346
|
-
const registry = new DocRegistry({
|
|
347
|
-
basePath: '/api',
|
|
348
|
-
errors: DocRegistry.defaultErrors(),
|
|
349
|
-
})
|
|
498
|
+
const registry = new DocRegistry({ basePath: '/api' })
|
|
350
499
|
.from(makeSource([rpcDoc]))
|
|
351
500
|
.from(makeSource([apiDoc]))
|
|
352
501
|
.from(makeSource([streamDoc]))
|
|
@@ -368,7 +517,6 @@ describe('DocRegistry', () => {
|
|
|
368
517
|
const registry = new DocRegistry({
|
|
369
518
|
basePath: '/api',
|
|
370
519
|
headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
|
|
371
|
-
errors: DocRegistry.defaultErrors(),
|
|
372
520
|
})
|
|
373
521
|
.from(makeSource([rpcDoc]))
|
|
374
522
|
.from(makeSource([apiDoc]))
|
|
@@ -7,6 +7,12 @@ import type {
|
|
|
7
7
|
ErrorDoc,
|
|
8
8
|
HeaderDoc,
|
|
9
9
|
} from '../types.js'
|
|
10
|
+
import {
|
|
11
|
+
PROCEDURE_REGISTRATION_ERROR_DOC,
|
|
12
|
+
defaultErrorTaxonomy,
|
|
13
|
+
taxonomyToErrorDocs,
|
|
14
|
+
type ErrorTaxonomy,
|
|
15
|
+
} from './error-taxonomy.js'
|
|
10
16
|
|
|
11
17
|
export type {
|
|
12
18
|
AnyHttpRouteDoc,
|
|
@@ -18,16 +24,44 @@ export type {
|
|
|
18
24
|
HeaderDoc,
|
|
19
25
|
} from '../types.js'
|
|
20
26
|
|
|
27
|
+
function isTaxonomy(input: ErrorTaxonomy | ErrorDoc[]): input is ErrorTaxonomy {
|
|
28
|
+
return !Array.isArray(input)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Dedupes ErrorDocs by `name`, last occurrence wins. Map insertion order
|
|
33
|
+
* preserves the latest position of each name.
|
|
34
|
+
*/
|
|
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())
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
export class DocRegistry {
|
|
22
42
|
private readonly basePath: string
|
|
23
43
|
private readonly headers: HeaderDoc[]
|
|
24
|
-
private
|
|
44
|
+
private errors: ErrorDoc[]
|
|
25
45
|
private readonly sources: DocSource<AnyHttpRouteDoc>[] = []
|
|
26
46
|
|
|
27
47
|
constructor(config?: DocRegistryConfig) {
|
|
28
48
|
this.basePath = config?.basePath ?? ''
|
|
29
49
|
this.headers = config?.headers ?? []
|
|
30
|
-
|
|
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)
|
|
31
65
|
}
|
|
32
66
|
|
|
33
67
|
from(source: DocSource<AnyHttpRouteDoc>): this {
|
|
@@ -35,6 +69,18 @@ export class DocRegistry {
|
|
|
35
69
|
return this
|
|
36
70
|
}
|
|
37
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
|
+
|
|
38
84
|
toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T {
|
|
39
85
|
let routes = this.sources.flatMap((source) => source.docs)
|
|
40
86
|
|
|
@@ -56,88 +102,18 @@ export class DocRegistry {
|
|
|
56
102
|
return envelope as T
|
|
57
103
|
}
|
|
58
104
|
|
|
59
|
-
|
|
105
|
+
/**
|
|
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.
|
|
112
|
+
*/
|
|
60
113
|
static defaultErrors(): ErrorDoc[] {
|
|
61
114
|
return [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
statusCode: 500,
|
|
65
|
-
description: 'An error thrown from within a procedure handler via ctx.error().',
|
|
66
|
-
schema: {
|
|
67
|
-
type: 'object',
|
|
68
|
-
properties: {
|
|
69
|
-
name: { type: 'string', const: 'ProcedureError' },
|
|
70
|
-
procedureName: { type: 'string' },
|
|
71
|
-
message: { type: 'string' },
|
|
72
|
-
meta: { type: 'object' },
|
|
73
|
-
},
|
|
74
|
-
required: ['name', 'procedureName', 'message'],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: 'ProcedureValidationError',
|
|
79
|
-
statusCode: 400,
|
|
80
|
-
description: 'Schema validation failed for the procedure input parameters.',
|
|
81
|
-
schema: {
|
|
82
|
-
type: 'object',
|
|
83
|
-
properties: {
|
|
84
|
-
name: { type: 'string', const: 'ProcedureValidationError' },
|
|
85
|
-
procedureName: { type: 'string' },
|
|
86
|
-
message: { type: 'string' },
|
|
87
|
-
errors: {
|
|
88
|
-
type: 'array',
|
|
89
|
-
items: {
|
|
90
|
-
type: 'object',
|
|
91
|
-
properties: {
|
|
92
|
-
instancePath: { type: 'string' },
|
|
93
|
-
message: { type: 'string' },
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
required: ['name', 'procedureName', 'message'],
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: 'ProcedureYieldValidationError',
|
|
103
|
-
statusCode: 500,
|
|
104
|
-
description:
|
|
105
|
-
'Schema validation failed for a yielded value in a streaming procedure.',
|
|
106
|
-
schema: {
|
|
107
|
-
type: 'object',
|
|
108
|
-
properties: {
|
|
109
|
-
name: { type: 'string', const: 'ProcedureYieldValidationError' },
|
|
110
|
-
procedureName: { type: 'string' },
|
|
111
|
-
message: { type: 'string' },
|
|
112
|
-
errors: {
|
|
113
|
-
type: 'array',
|
|
114
|
-
items: {
|
|
115
|
-
type: 'object',
|
|
116
|
-
properties: {
|
|
117
|
-
instancePath: { type: 'string' },
|
|
118
|
-
message: { type: 'string' },
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
required: ['name', 'procedureName', 'message'],
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: 'ProcedureRegistrationError',
|
|
128
|
-
statusCode: 500,
|
|
129
|
-
description:
|
|
130
|
-
'An invalid schema or configuration was detected at procedure registration time.',
|
|
131
|
-
schema: {
|
|
132
|
-
type: 'object',
|
|
133
|
-
properties: {
|
|
134
|
-
name: { type: 'string', const: 'ProcedureRegistrationError' },
|
|
135
|
-
procedureName: { type: 'string' },
|
|
136
|
-
message: { type: 'string' },
|
|
137
|
-
},
|
|
138
|
-
required: ['name', 'procedureName', 'message'],
|
|
139
|
-
},
|
|
140
|
-
},
|
|
115
|
+
...taxonomyToErrorDocs(defaultErrorTaxonomy),
|
|
116
|
+
PROCEDURE_REGISTRATION_ERROR_DOC,
|
|
141
117
|
]
|
|
142
118
|
}
|
|
143
119
|
}
|