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
|
@@ -37,7 +37,7 @@ When asked to plan an API or procedure set:
|
|
|
37
37
|
- AJV is configured with `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
|
|
38
38
|
- `schema.params` and `schema.input` are mutually exclusive — defining both throws `ProcedureRegistrationError`.
|
|
39
39
|
- Path param names in route template (`:id`) must match `schema.input.pathParams` property names.
|
|
40
|
-
- Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints.
|
|
40
|
+
- Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints. Pass your taxonomy directly: `new DocRegistry({ errors: appErrors })` — framework defaults are auto-merged and deduped. For errors outside your taxonomy (middleware, infrastructure, doc-only), chain `.documentError(...docs)`.
|
|
41
41
|
- Two first-class peer error-handling modes: **declarative taxonomy** (`defineErrorTaxonomy` + `errors` config) OR **imperative callback** (`onError`). Neither is deprecated. Pick the taxonomy for structured apps with typed client dispatch; pick `onError` for simple apps or full response control. Mixing both is allowed — the taxonomy handles what it covers, `onError` handles the tail. The anti-pattern is `instanceof` ladders inside `onError` (see anti-pattern #20 in the skill reference) — that's exactly what the taxonomy expresses declaratively.
|
|
42
42
|
- Per-route `errors: ['UseCaseError', ...]` narrows typed errors on the generated client. Declare via `APIConfig<keyof typeof appErrors & string>` / `RPCConfig<keyof typeof appErrors & string>` for compile-time typo protection.
|
|
43
43
|
- Generated `_errors.ts` emits real runtime classes extending `${Service}ProcedureError` — consumers catch with `instanceof ${Service}Errors.${Name}` and access `err.body`, `err.status`, `err.procedureName`, `err.scope`. Use the generated `create${Service}Client(config)` factory to wire the error registry automatically.
|
|
@@ -171,7 +171,7 @@ The npm package ships user-facing documentation with narrative explanations and
|
|
|
171
171
|
|------|--------|
|
|
172
172
|
| `docs/core.md` | Procedures factory, Create, CreateStream, schema.input, error handling |
|
|
173
173
|
| `docs/streaming.md` | Streaming procedures, AbortSignal, SSE patterns |
|
|
174
|
-
| `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, **error taxonomy (canonical)**, DocRegistry
|
|
174
|
+
| `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, **error taxonomy (canonical)**, DocRegistry (unified constructor, `.documentError()`) |
|
|
175
175
|
| `docs/client-and-codegen.md` | Client code generation, `createApiClient`/`createClient`, typed error dispatch, per-route `Errors` unions, per-call options, client-level defaults, typed RequestMeta augmentation, CLI options |
|
|
176
176
|
| `CHANGELOG.md` | Release notes — see `[6.0.0]` for the current peer error-handling model (taxonomy + `onError` + `onRequestError`), per-route errors, and client runtime error classes. |
|
|
177
177
|
|
|
@@ -476,13 +476,13 @@ import { DocRegistry } from 'ts-procedures/http-docs'
|
|
|
476
476
|
const docs = new DocRegistry({
|
|
477
477
|
basePath: '/api',
|
|
478
478
|
headers: [{ name: 'Authorization', description: 'Bearer token' }],
|
|
479
|
-
errors:
|
|
479
|
+
errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged
|
|
480
480
|
}).from(rpcBuilder).from(apiBuilder).from(streamBuilder)
|
|
481
481
|
|
|
482
482
|
app.get('/docs', (c) => c.json(docs.toJSON()))
|
|
483
483
|
```
|
|
484
484
|
|
|
485
|
-
**Why:** `DocRegistry` provides a typed `DocEnvelope`,
|
|
485
|
+
**Why:** `DocRegistry` provides a typed `DocEnvelope`, auto-merges framework error defaults (`ProcedureError`, `ProcedureValidationError`, `ProcedureYieldValidationError`, `ProcedureRegistrationError`) with JSON Schemas, reads docs lazily (order-independent), and supports filtering and transformation via `toJSON()` options.
|
|
486
486
|
|
|
487
487
|
---
|
|
488
488
|
|
|
@@ -295,13 +295,7 @@ When `toResponse` is omitted, the body defaults to `{ name: key, message: err.me
|
|
|
295
295
|
|
|
296
296
|
Pre-built taxonomy covering `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), and direct `ProcedureError` (500, unwrapped throws only — wrapped ones fall through so user taxonomy / `unknownError` sees the real error). Applied as a fallback after the user taxonomy unless `includeDefaults: false`.
|
|
297
297
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
function taxonomyToErrorDocs(taxonomy: ErrorTaxonomy): ErrorDoc[]
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Converts a taxonomy to the `ErrorDoc[]` format consumed by `DocRegistry.errors` — single source of truth so the documented shape cannot drift from runtime behavior.
|
|
298
|
+
Taxonomy-to-doc conversion is handled automatically by `DocRegistry` when you pass your taxonomy to the constructor. There is no public helper.
|
|
305
299
|
|
|
306
300
|
### resolveErrorResponse(params)
|
|
307
301
|
|
|
@@ -795,30 +789,24 @@ import type { DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, HeaderDo
|
|
|
795
789
|
```
|
|
796
790
|
|
|
797
791
|
```typescript
|
|
792
|
+
import { DocRegistry } from 'ts-procedures/http-docs'
|
|
793
|
+
import type { DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, HeaderDoc, ErrorDoc, DocSource } from 'ts-procedures/http-docs'
|
|
794
|
+
|
|
795
|
+
interface DocRegistryConfig {
|
|
796
|
+
basePath?: string
|
|
797
|
+
headers?: HeaderDoc[]
|
|
798
|
+
errors?: ErrorTaxonomy | ErrorDoc[] // polymorphic — pass taxonomy or raw docs
|
|
799
|
+
includeDefaults?: boolean // default: true (auto-merges framework defaults)
|
|
800
|
+
}
|
|
801
|
+
|
|
798
802
|
class DocRegistry {
|
|
799
|
-
constructor(config?:
|
|
800
|
-
basePath?: string
|
|
801
|
-
headers?: HeaderDoc[]
|
|
802
|
-
errors?: ErrorDoc[]
|
|
803
|
-
})
|
|
803
|
+
constructor(config?: DocRegistryConfig)
|
|
804
804
|
|
|
805
805
|
from(source: DocSource<AnyHttpRouteDoc>): this
|
|
806
|
+
documentError(...docs: ErrorDoc[]): this
|
|
807
|
+
toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T
|
|
806
808
|
|
|
807
|
-
toJSON<T = DocEnvelope>(options?: {
|
|
808
|
-
filter?: (route: AnyHttpRouteDoc) => boolean
|
|
809
|
-
transform?: (envelope: DocEnvelope) => T
|
|
810
|
-
}): T
|
|
811
|
-
|
|
812
|
-
// Framework error defaults derived from defaultErrorTaxonomy — single source
|
|
813
|
-
// of truth so runtime and documented shapes cannot drift.
|
|
814
809
|
static defaultErrors(): ErrorDoc[]
|
|
815
|
-
|
|
816
|
-
// Convenience: seed envelope errors from a taxonomy + framework defaults
|
|
817
|
-
// in one call (deduped — user entries win when keys overlap).
|
|
818
|
-
static fromTaxonomy(
|
|
819
|
-
taxonomy: ErrorTaxonomy,
|
|
820
|
-
config?: Omit<DocRegistryConfig, 'errors'> & { includeDefaults?: boolean }
|
|
821
|
-
): DocRegistry
|
|
822
810
|
}
|
|
823
811
|
```
|
|
824
812
|
|
|
@@ -875,7 +863,7 @@ type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc
|
|
|
875
863
|
|
|
876
864
|
- `from()` stores a **reference** to the builder — routes are read lazily at `toJSON()` time, so builders can be registered before or after `.build()`
|
|
877
865
|
- `toJSON()` collects routes via `flatMap(source => source.docs)`, applies optional `filter`, builds envelope, then applies optional `transform`
|
|
878
|
-
- `defaultErrors()` returns 4 `ErrorDoc` entries describing `ProcedureError` (500), `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), `ProcedureRegistrationError` (500) — each with a JSON Schema for the error response body shape
|
|
866
|
+
- `defaultErrors()` returns 4 `ErrorDoc` entries describing `ProcedureError` (500), `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), `ProcedureRegistrationError` (500) — each with a JSON Schema for the error response body shape. Most consumers do not need to call this directly; the `DocRegistry` constructor auto-merges these unless `includeDefaults: false` is passed.
|
|
879
867
|
- `JSON.stringify(registry)` works directly (calls `toJSON()` implicitly)
|
|
880
868
|
|
|
881
869
|
---
|
|
@@ -186,7 +186,7 @@ Seed the DocEnvelope with both taxonomy errors and framework defaults in one cal
|
|
|
186
186
|
```typescript
|
|
187
187
|
import { DocRegistry } from 'ts-procedures/http-docs'
|
|
188
188
|
|
|
189
|
-
const envelope = DocRegistry
|
|
189
|
+
const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' })
|
|
190
190
|
.from(apiApp)
|
|
191
191
|
.from(rpcApp)
|
|
192
192
|
.toJSON()
|
|
@@ -194,6 +194,13 @@ const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
|
|
|
194
194
|
// envelope.routes[i].errors: per-route subset declared on config.errors
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
+
```typescript
|
|
198
|
+
// For errors outside your taxonomy (middleware, infrastructure, doc-only):
|
|
199
|
+
const docsWithExtras = new DocRegistry({ errors: appErrors, basePath: '/api' })
|
|
200
|
+
.from(apiBuilder)
|
|
201
|
+
.documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'too many requests' })
|
|
202
|
+
```
|
|
203
|
+
|
|
197
204
|
### Typed catch blocks on the client (via codegen)
|
|
198
205
|
|
|
199
206
|
The codegen emits runtime error classes (not just types) and a registry object. `createApiClient` wires the registry automatically so non-2xx responses arrive as typed class instances.
|
|
@@ -805,7 +812,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
|
|
|
805
812
|
const docs = new DocRegistry({
|
|
806
813
|
basePath: '/api',
|
|
807
814
|
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
808
|
-
errors:
|
|
815
|
+
errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged and deduped
|
|
809
816
|
})
|
|
810
817
|
.from(rpcBuilder)
|
|
811
818
|
.from(apiBuilder)
|
|
@@ -824,7 +831,7 @@ app.get('/docs', (c) => c.json(docs.toJSON({
|
|
|
824
831
|
**Key points:**
|
|
825
832
|
- `from()` stores a reference — register builders before or after `.build()`
|
|
826
833
|
- `toJSON()` reads docs lazily, so late-registered procedures are included
|
|
827
|
-
- `
|
|
834
|
+
- Pass your `ErrorTaxonomy` directly to `errors` — the constructor auto-merges framework defaults and dedupes; opt out with `includeDefaults: false`
|
|
828
835
|
- Accepts any object satisfying `{ readonly docs: AnyHttpRouteDoc[] }` — not limited to built-in builders
|
|
829
836
|
|
|
830
837
|
---
|
|
@@ -926,7 +933,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
|
|
|
926
933
|
const docs = new DocRegistry({
|
|
927
934
|
basePath: '/api',
|
|
928
935
|
headers: [{ name: 'Authorization', description: 'Bearer token' }],
|
|
929
|
-
errors:
|
|
936
|
+
errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged
|
|
930
937
|
})
|
|
931
938
|
.from(rpcBuilder)
|
|
932
939
|
.from(apiBuilder)
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
### SUGGESTION
|
|
76
76
|
- [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
|
|
77
|
-
- [ ] Uses `DocRegistry
|
|
77
|
+
- [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs from multiple builders
|
|
78
78
|
- [ ] Lifecycle hooks used for observability (logging, metrics)
|
|
79
79
|
- [ ] Per-route `errors: [...]` declared so generated clients can narrow `catch` types
|
|
80
80
|
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
|
|
116
116
|
### SUGGESTION
|
|
117
117
|
- [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
|
|
118
|
-
- [ ] Uses `DocRegistry
|
|
118
|
+
- [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs across builders instead of manual assembly
|
|
119
119
|
- [ ] Custom `queryParser` provided if complex query string formats needed
|
|
120
120
|
- [ ] Lifecycle hooks used for observability (logging, metrics)
|
|
121
121
|
|
|
@@ -218,7 +218,8 @@ class UseCaseError extends Error {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
const appErrors = defineErrorTaxonomy({
|
|
221
|
-
//
|
|
221
|
+
// class: entries are topologically sorted (subclasses checked first) automatically.
|
|
222
|
+
// Predicate (match:) entries keep declared order — put narrower predicates first.
|
|
222
223
|
UseCaseError: {
|
|
223
224
|
class: UseCaseError,
|
|
224
225
|
statusCode: 422,
|
|
@@ -254,7 +255,7 @@ const API = Procedures<Ctx, MyAPIConfig>()
|
|
|
254
255
|
API.Create('GetUser', { path: '/users/:id', method: 'get', errors: ['UseCaseError'], /* ... */ }, ...)
|
|
255
256
|
|
|
256
257
|
// Seed envelope errors from the taxonomy + framework defaults in one call
|
|
257
|
-
const envelope = DocRegistry
|
|
258
|
+
const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
|
|
258
259
|
```
|
|
259
260
|
|
|
260
261
|
### Client-side typed catch blocks (via codegen)
|
|
@@ -218,7 +218,8 @@ class UseCaseError extends Error {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
const appErrors = defineErrorTaxonomy({
|
|
221
|
-
//
|
|
221
|
+
// class: entries are topologically sorted (subclasses checked first) automatically.
|
|
222
|
+
// Predicate (match:) entries keep declared order — put narrower predicates first.
|
|
222
223
|
UseCaseError: {
|
|
223
224
|
class: UseCaseError,
|
|
224
225
|
statusCode: 422,
|
|
@@ -254,7 +255,7 @@ const API = Procedures<Ctx, MyAPIConfig>()
|
|
|
254
255
|
API.Create('GetUser', { path: '/users/:id', method: 'get', errors: ['UseCaseError'], /* ... */ }, ...)
|
|
255
256
|
|
|
256
257
|
// Seed envelope errors from the taxonomy + framework defaults in one call
|
|
257
|
-
const envelope = DocRegistry
|
|
258
|
+
const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
|
|
258
259
|
```
|
|
259
260
|
|
|
260
261
|
### Client-side typed catch blocks (via codegen)
|
|
@@ -1,33 +1,28 @@
|
|
|
1
1
|
import type { AnyHttpRouteDoc, DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, DocSource, ErrorDoc } from '../types.js';
|
|
2
|
-
import { ErrorTaxonomy } from './error-taxonomy.js';
|
|
3
2
|
export type { AnyHttpRouteDoc, DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, DocSource, ErrorDoc, HeaderDoc, } from '../types.js';
|
|
4
3
|
export declare class DocRegistry {
|
|
5
4
|
private readonly basePath;
|
|
6
5
|
private readonly headers;
|
|
7
|
-
private
|
|
6
|
+
private errors;
|
|
8
7
|
private readonly sources;
|
|
9
8
|
constructor(config?: DocRegistryConfig);
|
|
10
9
|
from(source: DocSource<AnyHttpRouteDoc>): this;
|
|
11
|
-
toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T;
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* Adds one or more {@link ErrorDoc} entries to the envelope. Use for errors
|
|
12
|
+
* outside your runtime taxonomy — middleware-level errors, infrastructure
|
|
13
|
+
* errors (502/503/504), or doc-only meta errors.
|
|
14
|
+
*
|
|
15
|
+
* Deduped by `name` — last write wins.
|
|
18
16
|
*/
|
|
19
|
-
|
|
17
|
+
documentError(...docs: ErrorDoc[]): this;
|
|
18
|
+
toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T;
|
|
20
19
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* are included unless `includeDefaults: false` is passed.
|
|
20
|
+
* Framework error defaults — derived from {@link defaultErrorTaxonomy} plus
|
|
21
|
+
* `ProcedureRegistrationError` (which is thrown only at registration time
|
|
22
|
+
* and therefore lives in the catalog, not the runtime taxonomy).
|
|
25
23
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* .from(apiApp)
|
|
24
|
+
* Most consumers do not need to call this directly — the `DocRegistry`
|
|
25
|
+
* constructor auto-includes these unless `includeDefaults: false` is passed.
|
|
29
26
|
*/
|
|
30
|
-
static
|
|
31
|
-
includeDefaults?: boolean;
|
|
32
|
-
}): DocRegistry;
|
|
27
|
+
static defaultErrors(): ErrorDoc[];
|
|
33
28
|
}
|
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import { defaultErrorTaxonomy, taxonomyToErrorDocs, } from './error-taxonomy.js';
|
|
1
|
+
import { PROCEDURE_REGISTRATION_ERROR_DOC, defaultErrorTaxonomy, taxonomyToErrorDocs, } from './error-taxonomy.js';
|
|
2
|
+
function isTaxonomy(input) {
|
|
3
|
+
return !Array.isArray(input);
|
|
4
|
+
}
|
|
2
5
|
/**
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
5
|
-
* here so consumers still see it in the error catalog.
|
|
6
|
+
* Dedupes ErrorDocs by `name`, last occurrence wins. Map insertion order
|
|
7
|
+
* preserves the latest position of each name.
|
|
6
8
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
properties: {
|
|
14
|
-
name: { type: 'string', const: 'ProcedureRegistrationError' },
|
|
15
|
-
procedureName: { type: 'string' },
|
|
16
|
-
message: { type: 'string' },
|
|
17
|
-
},
|
|
18
|
-
required: ['name', 'procedureName', 'message'],
|
|
19
|
-
},
|
|
20
|
-
};
|
|
9
|
+
function dedupeByName(docs) {
|
|
10
|
+
const byName = new Map();
|
|
11
|
+
for (const doc of docs)
|
|
12
|
+
byName.set(doc.name, doc);
|
|
13
|
+
return Array.from(byName.values());
|
|
14
|
+
}
|
|
21
15
|
export class DocRegistry {
|
|
22
16
|
basePath;
|
|
23
17
|
headers;
|
|
@@ -26,12 +20,34 @@ export class DocRegistry {
|
|
|
26
20
|
constructor(config) {
|
|
27
21
|
this.basePath = config?.basePath ?? '';
|
|
28
22
|
this.headers = config?.headers ?? [];
|
|
29
|
-
|
|
23
|
+
const includeDefaults = config?.includeDefaults ?? true;
|
|
24
|
+
const userErrors = config?.errors
|
|
25
|
+
? isTaxonomy(config.errors)
|
|
26
|
+
? taxonomyToErrorDocs(config.errors)
|
|
27
|
+
: config.errors
|
|
28
|
+
: [];
|
|
29
|
+
// Precedence: defaults come first, user errors override via dedupe
|
|
30
|
+
// (last-write-wins). Matches runtime resolution order.
|
|
31
|
+
const merged = includeDefaults
|
|
32
|
+
? [...DocRegistry.defaultErrors(), ...userErrors]
|
|
33
|
+
: userErrors;
|
|
34
|
+
this.errors = dedupeByName(merged);
|
|
30
35
|
}
|
|
31
36
|
from(source) {
|
|
32
37
|
this.sources.push(source);
|
|
33
38
|
return this;
|
|
34
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Adds one or more {@link ErrorDoc} entries to the envelope. Use for errors
|
|
42
|
+
* outside your runtime taxonomy — middleware-level errors, infrastructure
|
|
43
|
+
* errors (502/503/504), or doc-only meta errors.
|
|
44
|
+
*
|
|
45
|
+
* Deduped by `name` — last write wins.
|
|
46
|
+
*/
|
|
47
|
+
documentError(...docs) {
|
|
48
|
+
this.errors = dedupeByName([...this.errors, ...docs]);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
35
51
|
toJSON(options) {
|
|
36
52
|
let routes = this.sources.flatMap((source) => source.docs);
|
|
37
53
|
if (options?.filter) {
|
|
@@ -49,11 +65,12 @@ export class DocRegistry {
|
|
|
49
65
|
return envelope;
|
|
50
66
|
}
|
|
51
67
|
/**
|
|
52
|
-
* Framework error defaults
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
68
|
+
* Framework error defaults — derived from {@link defaultErrorTaxonomy} plus
|
|
69
|
+
* `ProcedureRegistrationError` (which is thrown only at registration time
|
|
70
|
+
* and therefore lives in the catalog, not the runtime taxonomy).
|
|
71
|
+
*
|
|
72
|
+
* Most consumers do not need to call this directly — the `DocRegistry`
|
|
73
|
+
* constructor auto-includes these unless `includeDefaults: false` is passed.
|
|
57
74
|
*/
|
|
58
75
|
static defaultErrors() {
|
|
59
76
|
return [
|
|
@@ -61,27 +78,5 @@ export class DocRegistry {
|
|
|
61
78
|
PROCEDURE_REGISTRATION_ERROR_DOC,
|
|
62
79
|
];
|
|
63
80
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Convenience constructor that seeds `config.errors` from a taxonomy so the
|
|
66
|
-
* DocEnvelope automatically documents every error class registered with the
|
|
67
|
-
* HTTP builders. Framework defaults (including `ProcedureRegistrationError`)
|
|
68
|
-
* are included unless `includeDefaults: false` is passed.
|
|
69
|
-
*
|
|
70
|
-
* @example
|
|
71
|
-
* const registry = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
|
|
72
|
-
* .from(apiApp)
|
|
73
|
-
*/
|
|
74
|
-
static fromTaxonomy(taxonomy, config) {
|
|
75
|
-
const { includeDefaults = true, ...rest } = config ?? {};
|
|
76
|
-
const errors = [
|
|
77
|
-
...taxonomyToErrorDocs(taxonomy),
|
|
78
|
-
...(includeDefaults ? DocRegistry.defaultErrors() : []),
|
|
79
|
-
];
|
|
80
|
-
// Dedupe by name — user entries take precedence over defaults with the
|
|
81
|
-
// same key, matching runtime resolution order.
|
|
82
|
-
const seen = new Set();
|
|
83
|
-
const deduped = errors.filter((e) => seen.has(e.name) ? false : (seen.add(e.name), true));
|
|
84
|
-
return new DocRegistry({ ...rest, errors: deduped });
|
|
85
|
-
}
|
|
86
81
|
}
|
|
87
82
|
//# sourceMappingURL=doc-registry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doc-registry.js","sourceRoot":"","sources":["../../../src/implementations/http/doc-registry.ts"],"names":[],"mappings":"AASA,OAAO,
|
|
1
|
+
{"version":3,"file":"doc-registry.js","sourceRoot":"","sources":["../../../src/implementations/http/doc-registry.ts"],"names":[],"mappings":"AASA,OAAO,EACL,gCAAgC,EAChC,oBAAoB,EACpB,mBAAmB,GAEpB,MAAM,qBAAqB,CAAA;AAY5B,SAAS,UAAU,CAAC,KAAiC;IACnD,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,IAAgB;IACpC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC1C,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IACjD,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;AACpC,CAAC;AAED,MAAM,OAAO,WAAW;IACL,QAAQ,CAAQ;IAChB,OAAO,CAAa;IAC7B,MAAM,CAAY;IACT,OAAO,GAAiC,EAAE,CAAA;IAE3D,YAAY,MAA0B;QACpC,IAAI,CAAC,QAAQ,GAAG,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAA;QACtC,IAAI,CAAC,OAAO,GAAG,MAAM,EAAE,OAAO,IAAI,EAAE,CAAA;QAEpC,MAAM,eAAe,GAAG,MAAM,EAAE,eAAe,IAAI,IAAI,CAAA;QACvD,MAAM,UAAU,GAAe,MAAM,EAAE,MAAM;YAC3C,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC;gBACzB,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC;gBACpC,CAAC,CAAC,MAAM,CAAC,MAAM;YACjB,CAAC,CAAC,EAAE,CAAA;QAEN,mEAAmE;QACnE,uDAAuD;QACvD,MAAM,MAAM,GAAG,eAAe;YAC5B,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,aAAa,EAAE,EAAE,GAAG,UAAU,CAAC;YACjD,CAAC,CAAC,UAAU,CAAA;QAEd,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;IACpC,CAAC;IAED,IAAI,CAAC,MAAkC;QACrC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;;OAMG;IACH,aAAa,CAAC,GAAG,IAAgB;QAC/B,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;QACrD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,CAAkB,OAAqC;QAC3D,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAE1D,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACpB,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACxC,CAAC;QAED,MAAM,QAAQ,GAAgB;YAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;YAC1B,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;YACxB,MAAM;SACP,CAAA;QAED,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;QACpC,CAAC;QAED,OAAO,QAAa,CAAA;IACtB,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,aAAa;QAClB,OAAO;YACL,GAAG,mBAAmB,CAAC,oBAAoB,CAAC;YAC5C,gCAAgC;SACjC,CAAA;IACH,CAAC;CACF"}
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// Helpers — minimal doc fixtures
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
@@ -45,14 +46,14 @@ describe('DocRegistry', () => {
|
|
|
45
46
|
// --------------------------------------------------------------------------
|
|
46
47
|
describe('constructor', () => {
|
|
47
48
|
test('uses defaults when no config provided', () => {
|
|
48
|
-
const registry = new DocRegistry();
|
|
49
|
+
const registry = new DocRegistry({ includeDefaults: false });
|
|
49
50
|
const out = registry.toJSON();
|
|
50
51
|
expect(out.basePath).toBe('');
|
|
51
52
|
expect(out.headers).toEqual([]);
|
|
52
53
|
expect(out.errors).toEqual([]);
|
|
53
54
|
});
|
|
54
55
|
test('accepts partial config', () => {
|
|
55
|
-
const registry = new DocRegistry({ basePath: '/v1' });
|
|
56
|
+
const registry = new DocRegistry({ basePath: '/v1', includeDefaults: false });
|
|
56
57
|
const out = registry.toJSON();
|
|
57
58
|
expect(out.basePath).toBe('/v1');
|
|
58
59
|
expect(out.headers).toEqual([]);
|
|
@@ -61,7 +62,7 @@ describe('DocRegistry', () => {
|
|
|
61
62
|
test('accepts full config', () => {
|
|
62
63
|
const headers = [{ name: 'Authorization', description: 'Bearer token', required: true }];
|
|
63
64
|
const errors = [{ name: 'Unauthorized', statusCode: 401, description: 'Missing token' }];
|
|
64
|
-
const registry = new DocRegistry({ basePath: '/api', headers, errors });
|
|
65
|
+
const registry = new DocRegistry({ basePath: '/api', headers, errors, includeDefaults: false });
|
|
65
66
|
const out = registry.toJSON();
|
|
66
67
|
expect(out.basePath).toBe('/api');
|
|
67
68
|
expect(out.headers).toEqual(headers);
|
|
@@ -142,7 +143,7 @@ describe('DocRegistry', () => {
|
|
|
142
143
|
test('headers and errors are copies', () => {
|
|
143
144
|
const headers = [{ name: 'X-Custom' }];
|
|
144
145
|
const errors = [{ name: 'E', statusCode: 500, description: 'd' }];
|
|
145
|
-
const registry = new DocRegistry({ headers, errors });
|
|
146
|
+
const registry = new DocRegistry({ headers, errors, includeDefaults: false });
|
|
146
147
|
const out = registry.toJSON();
|
|
147
148
|
expect(out.headers).toEqual(headers);
|
|
148
149
|
expect(out.headers).not.toBe(headers);
|
|
@@ -249,6 +250,141 @@ describe('DocRegistry', () => {
|
|
|
249
250
|
});
|
|
250
251
|
});
|
|
251
252
|
// --------------------------------------------------------------------------
|
|
253
|
+
// taxonomy input + auto-defaults + dedupe (v6.0.1 simplification)
|
|
254
|
+
// --------------------------------------------------------------------------
|
|
255
|
+
describe('errors: ErrorTaxonomy (polymorphic constructor input)', () => {
|
|
256
|
+
test('accepts an ErrorTaxonomy directly and converts to ErrorDoc[]', () => {
|
|
257
|
+
const taxonomy = defineErrorTaxonomy({
|
|
258
|
+
AuthError: {
|
|
259
|
+
class: class AuthError extends Error {
|
|
260
|
+
},
|
|
261
|
+
statusCode: 401,
|
|
262
|
+
description: 'unauthenticated',
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const envelope = new DocRegistry({ errors: taxonomy }).toJSON();
|
|
266
|
+
const auth = envelope.errors.find((e) => e.name === 'AuthError');
|
|
267
|
+
expect(auth).toBeDefined();
|
|
268
|
+
expect(auth?.statusCode).toBe(401);
|
|
269
|
+
expect(auth?.description).toBe('unauthenticated');
|
|
270
|
+
});
|
|
271
|
+
test('auto-includes framework defaults when errors is a taxonomy', () => {
|
|
272
|
+
const taxonomy = defineErrorTaxonomy({
|
|
273
|
+
AuthError: { class: class AuthError extends Error {
|
|
274
|
+
}, statusCode: 401 },
|
|
275
|
+
});
|
|
276
|
+
const names = new DocRegistry({ errors: taxonomy }).toJSON().errors.map((e) => e.name);
|
|
277
|
+
expect(names).toContain('AuthError');
|
|
278
|
+
expect(names).toContain('ProcedureValidationError');
|
|
279
|
+
expect(names).toContain('ProcedureYieldValidationError');
|
|
280
|
+
expect(names).toContain('ProcedureError');
|
|
281
|
+
expect(names).toContain('ProcedureRegistrationError');
|
|
282
|
+
});
|
|
283
|
+
test('auto-includes framework defaults when errors is ErrorDoc[]', () => {
|
|
284
|
+
const custom = { name: 'CustomThing', statusCode: 418, description: 'teapot' };
|
|
285
|
+
const names = new DocRegistry({ errors: [custom] }).toJSON().errors.map((e) => e.name);
|
|
286
|
+
expect(names).toContain('CustomThing');
|
|
287
|
+
expect(names).toContain('ProcedureValidationError');
|
|
288
|
+
});
|
|
289
|
+
test('includeDefaults: false omits framework defaults', () => {
|
|
290
|
+
const taxonomy = defineErrorTaxonomy({
|
|
291
|
+
AuthError: { class: class AuthError extends Error {
|
|
292
|
+
}, statusCode: 401 },
|
|
293
|
+
});
|
|
294
|
+
const names = new DocRegistry({ errors: taxonomy, includeDefaults: false })
|
|
295
|
+
.toJSON()
|
|
296
|
+
.errors.map((e) => e.name);
|
|
297
|
+
expect(names).toEqual(['AuthError']);
|
|
298
|
+
});
|
|
299
|
+
test('user taxonomy entry with same name as default overrides default (dedupe, user wins)', () => {
|
|
300
|
+
const taxonomy = defineErrorTaxonomy({
|
|
301
|
+
ProcedureError: {
|
|
302
|
+
class: Error,
|
|
303
|
+
statusCode: 418,
|
|
304
|
+
description: 'custom override',
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
const envelope = new DocRegistry({ errors: taxonomy }).toJSON();
|
|
308
|
+
const proc = envelope.errors.filter((e) => e.name === 'ProcedureError');
|
|
309
|
+
expect(proc).toHaveLength(1);
|
|
310
|
+
expect(proc[0].statusCode).toBe(418);
|
|
311
|
+
expect(proc[0].description).toBe('custom override');
|
|
312
|
+
});
|
|
313
|
+
test('user ErrorDoc with same name as default overrides default (dedupe, user wins)', () => {
|
|
314
|
+
const custom = {
|
|
315
|
+
name: 'ProcedureError',
|
|
316
|
+
statusCode: 418,
|
|
317
|
+
description: 'custom override',
|
|
318
|
+
};
|
|
319
|
+
const envelope = new DocRegistry({ errors: [custom] }).toJSON();
|
|
320
|
+
const proc = envelope.errors.filter((e) => e.name === 'ProcedureError');
|
|
321
|
+
expect(proc).toHaveLength(1);
|
|
322
|
+
expect(proc[0].statusCode).toBe(418);
|
|
323
|
+
expect(proc[0].description).toBe('custom override');
|
|
324
|
+
});
|
|
325
|
+
test('empty errors config still returns framework defaults', () => {
|
|
326
|
+
const names = new DocRegistry().toJSON().errors.map((e) => e.name);
|
|
327
|
+
expect(names).toContain('ProcedureError');
|
|
328
|
+
expect(names).toContain('ProcedureValidationError');
|
|
329
|
+
expect(names).toContain('ProcedureYieldValidationError');
|
|
330
|
+
expect(names).toContain('ProcedureRegistrationError');
|
|
331
|
+
});
|
|
332
|
+
test('includeDefaults: false with no errors produces empty error list', () => {
|
|
333
|
+
const envelope = new DocRegistry({ includeDefaults: false }).toJSON();
|
|
334
|
+
expect(envelope.errors).toEqual([]);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
// --------------------------------------------------------------------------
|
|
338
|
+
// .documentError() fluent extension
|
|
339
|
+
// --------------------------------------------------------------------------
|
|
340
|
+
describe('.documentError()', () => {
|
|
341
|
+
test('adds a single ErrorDoc to the envelope', () => {
|
|
342
|
+
const registry = new DocRegistry({ includeDefaults: false }).documentError({
|
|
343
|
+
name: 'RateLimitExceeded',
|
|
344
|
+
statusCode: 429,
|
|
345
|
+
description: 'too many requests',
|
|
346
|
+
});
|
|
347
|
+
const names = registry.toJSON().errors.map((e) => e.name);
|
|
348
|
+
expect(names).toEqual(['RateLimitExceeded']);
|
|
349
|
+
});
|
|
350
|
+
test('accepts multiple docs via variadic args', () => {
|
|
351
|
+
const registry = new DocRegistry({ includeDefaults: false }).documentError({ name: 'A', statusCode: 400, description: 'a' }, { name: 'B', statusCode: 500, description: 'b' });
|
|
352
|
+
const names = registry.toJSON().errors.map((e) => e.name);
|
|
353
|
+
expect(names).toEqual(['A', 'B']);
|
|
354
|
+
});
|
|
355
|
+
test('returns this for chaining', () => {
|
|
356
|
+
const registry = new DocRegistry();
|
|
357
|
+
const returned = registry.documentError({
|
|
358
|
+
name: 'Foo',
|
|
359
|
+
statusCode: 500,
|
|
360
|
+
description: 'x',
|
|
361
|
+
});
|
|
362
|
+
expect(returned).toBe(registry);
|
|
363
|
+
});
|
|
364
|
+
test('composes with taxonomy input — extends docs without replacing', () => {
|
|
365
|
+
const taxonomy = defineErrorTaxonomy({
|
|
366
|
+
AuthError: { class: class AuthError extends Error {
|
|
367
|
+
}, statusCode: 401 },
|
|
368
|
+
});
|
|
369
|
+
const envelope = new DocRegistry({ errors: taxonomy })
|
|
370
|
+
.documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'x' })
|
|
371
|
+
.toJSON();
|
|
372
|
+
const names = envelope.errors.map((e) => e.name);
|
|
373
|
+
expect(names).toContain('AuthError');
|
|
374
|
+
expect(names).toContain('RateLimitExceeded');
|
|
375
|
+
expect(names).toContain('ProcedureValidationError');
|
|
376
|
+
});
|
|
377
|
+
test('dedupes against existing errors (last write wins)', () => {
|
|
378
|
+
const registry = new DocRegistry({ includeDefaults: false })
|
|
379
|
+
.documentError({ name: 'Foo', statusCode: 400, description: 'first' })
|
|
380
|
+
.documentError({ name: 'Foo', statusCode: 500, description: 'second' });
|
|
381
|
+
const foo = registry.toJSON().errors.filter((e) => e.name === 'Foo');
|
|
382
|
+
expect(foo).toHaveLength(1);
|
|
383
|
+
expect(foo[0].statusCode).toBe(500);
|
|
384
|
+
expect(foo[0].description).toBe('second');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
// --------------------------------------------------------------------------
|
|
252
388
|
// kind discriminant
|
|
253
389
|
// --------------------------------------------------------------------------
|
|
254
390
|
describe('kind discriminant', () => {
|
|
@@ -279,7 +415,6 @@ describe('DocRegistry', () => {
|
|
|
279
415
|
const registry = new DocRegistry({
|
|
280
416
|
basePath: '/api',
|
|
281
417
|
headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
|
|
282
|
-
errors: DocRegistry.defaultErrors(),
|
|
283
418
|
})
|
|
284
419
|
.from(makeSource([rpcDoc]))
|
|
285
420
|
.from(makeSource([apiDoc]))
|
|
@@ -291,10 +426,7 @@ describe('DocRegistry', () => {
|
|
|
291
426
|
expect(out.routes).toHaveLength(3);
|
|
292
427
|
});
|
|
293
428
|
test('filter + transform combined', () => {
|
|
294
|
-
const registry = new DocRegistry({
|
|
295
|
-
basePath: '/api',
|
|
296
|
-
errors: DocRegistry.defaultErrors(),
|
|
297
|
-
})
|
|
429
|
+
const registry = new DocRegistry({ basePath: '/api' })
|
|
298
430
|
.from(makeSource([rpcDoc]))
|
|
299
431
|
.from(makeSource([apiDoc]))
|
|
300
432
|
.from(makeSource([streamDoc]));
|
|
@@ -313,7 +445,6 @@ describe('DocRegistry', () => {
|
|
|
313
445
|
const registry = new DocRegistry({
|
|
314
446
|
basePath: '/api',
|
|
315
447
|
headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
|
|
316
|
-
errors: DocRegistry.defaultErrors(),
|
|
317
448
|
})
|
|
318
449
|
.from(makeSource([rpcDoc]))
|
|
319
450
|
.from(makeSource([apiDoc]));
|