ts-procedures 8.2.1 → 8.3.0
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/skills/ts-procedures/SKILL.md +5 -1
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +3 -1
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +30 -6
- package/agent_config/copilot/copilot-instructions.md +10 -6
- package/agent_config/cursor/cursorrules +10 -6
- package/build/codegen/emit-errors.integration.test.js +22 -0
- package/build/codegen/emit-errors.integration.test.js.map +1 -1
- package/build/implementations/http/error-taxonomy.d.ts +40 -0
- package/build/implementations/http/error-taxonomy.js +57 -5
- package/build/implementations/http/error-taxonomy.js.map +1 -1
- package/build/implementations/http/error-taxonomy.test.js +95 -1
- package/build/implementations/http/error-taxonomy.test.js.map +1 -1
- package/build/implementations/http/hono/handlers/http.js +19 -24
- package/build/implementations/http/hono/handlers/http.js.map +1 -1
- package/build/implementations/http/hono/handlers/http.test.js +64 -1
- package/build/implementations/http/hono/handlers/http.test.js.map +1 -1
- package/docs/client-and-codegen.md +8 -0
- package/docs/core.md +2 -0
- package/docs/http-integrations.md +4 -0
- package/package.json +1 -1
- package/src/codegen/emit-errors.integration.test.ts +26 -0
- package/src/implementations/http/error-taxonomy.test.ts +111 -0
- package/src/implementations/http/error-taxonomy.ts +60 -5
- package/src/implementations/http/hono/handlers/http.test.ts +69 -1
- package/src/implementations/http/hono/handlers/http.ts +19 -21
|
@@ -68,33 +68,28 @@ export function installHttpRoute(params) {
|
|
|
68
68
|
: undefined;
|
|
69
69
|
const result = await procedure.handler({ ...context, signal: c.req.raw.signal }, reqParams);
|
|
70
70
|
cfg.api?.onSuccess?.(procedure, c);
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
headers = result.headers;
|
|
85
|
-
}
|
|
86
|
-
else if (result && typeof result === 'object' && 'headers' in result && !('body' in result)) {
|
|
87
|
-
for (const [k, v] of Object.entries(result.headers)) {
|
|
71
|
+
// The `{ body, headers }` envelope is OPT-IN via `schema.res.headers`: when
|
|
72
|
+
// the route declares response headers, the handler returns `{ body, headers }`;
|
|
73
|
+
// otherwise its return value IS the body. Gating on the schema (rather than
|
|
74
|
+
// duck-typing the result for a `body`/`headers` key) lets a domain object
|
|
75
|
+
// safely carry its own `body`/`headers` field without being unwrapped.
|
|
76
|
+
// Normalize both shapes to a single `{ body, headers }` so the response
|
|
77
|
+
// decision below is uniform for every legal return shape.
|
|
78
|
+
const hasResHeaders = procedure.config.schema?.res?.headers != null;
|
|
79
|
+
const { body, headers } = hasResHeaders
|
|
80
|
+
? (result ?? {})
|
|
81
|
+
: { body: result, headers: undefined };
|
|
82
|
+
if (headers && typeof headers === 'object') {
|
|
83
|
+
for (const [k, v] of Object.entries(headers))
|
|
88
84
|
c.header(k, v);
|
|
89
|
-
}
|
|
90
|
-
return c.body(null, successStatus);
|
|
91
85
|
}
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
// No body to send — a 204 status, or an `undefined` return (a `void`
|
|
87
|
+
// handler, or a headers-only `res: { headers }` envelope). Emit a clean
|
|
88
|
+
// bodyless response instead of `c.json(undefined)`, which would send an
|
|
89
|
+
// empty, unparseable body under an `application/json` content-type.
|
|
90
|
+
if (successStatus === 204 || body === undefined) {
|
|
91
|
+
return c.body(null, successStatus);
|
|
94
92
|
}
|
|
95
|
-
if (headers)
|
|
96
|
-
for (const [k, v] of Object.entries(headers))
|
|
97
|
-
c.header(k, v);
|
|
98
93
|
return c.json(body, successStatus);
|
|
99
94
|
}
|
|
100
95
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../../../../src/implementations/http/hono/handlers/http.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../../../../src/implementations/http/hono/handlers/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -108,11 +108,74 @@ describe('installHttpRoute', () => {
|
|
|
108
108
|
P.CreateHttp('Q', {
|
|
109
109
|
path: '/q', method: 'get',
|
|
110
110
|
schema: { req: { query: Type.Any() }, res: { body: Type.Any() } },
|
|
111
|
-
}, async (_ctx, { query }) =>
|
|
111
|
+
}, async (_ctx, { query }) => query);
|
|
112
112
|
}, { api: { queryParser } });
|
|
113
113
|
const res = await app.request('/q?a=1&b=2');
|
|
114
114
|
expect(queryParser).toHaveBeenCalledWith('a=1&b=2');
|
|
115
115
|
expect(await res.json()).toEqual({ parsed: true, raw: 'a=1&b=2' });
|
|
116
116
|
});
|
|
117
|
+
test('domain object with a top-level "body" field serializes whole (no res.headers)', async () => {
|
|
118
|
+
const { app } = buildApp((P) => {
|
|
119
|
+
P.CreateHttp('GetMessage', {
|
|
120
|
+
path: '/msg', method: 'get',
|
|
121
|
+
schema: { res: { body: Type.Object({ id: Type.String(), body: Type.String() }) } },
|
|
122
|
+
}, async () => ({ id: '1', body: 'hello text' }));
|
|
123
|
+
});
|
|
124
|
+
const res = await app.request('/msg');
|
|
125
|
+
expect(res.status).toBe(200);
|
|
126
|
+
expect(await res.json()).toEqual({ id: '1', body: 'hello text' });
|
|
127
|
+
});
|
|
128
|
+
test('envelope unwraps when res.headers is declared', async () => {
|
|
129
|
+
const { app } = buildApp((P) => {
|
|
130
|
+
P.CreateHttp('Enveloped', {
|
|
131
|
+
path: '/env', method: 'get',
|
|
132
|
+
schema: { res: { body: Type.Object({ ok: Type.Boolean() }), headers: Type.Object({}) } },
|
|
133
|
+
}, async () => ({ body: { ok: true }, headers: { 'x-trace': 'z' } }));
|
|
134
|
+
});
|
|
135
|
+
const res = await app.request('/env');
|
|
136
|
+
expect(res.status).toBe(200);
|
|
137
|
+
expect(res.headers.get('x-trace')).toBe('z');
|
|
138
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
139
|
+
});
|
|
140
|
+
// Headers-only response shape — `res: { headers }` with no `res.body`. The
|
|
141
|
+
// handler returns `{ headers }` (no body key). The response must be bodyless,
|
|
142
|
+
// not an empty `application/json` payload that a client can't parse.
|
|
143
|
+
test('headers-only res schema returns a clean bodyless response', async () => {
|
|
144
|
+
const { app } = buildApp((P) => {
|
|
145
|
+
P.CreateHttp('HeadOnly', {
|
|
146
|
+
path: '/ho', method: 'get',
|
|
147
|
+
schema: { res: { headers: Type.Object({}) } },
|
|
148
|
+
}, async () => ({ headers: { 'x-trace': 'h1' } }));
|
|
149
|
+
});
|
|
150
|
+
const res = await app.request('/ho');
|
|
151
|
+
expect(res.status).toBe(200);
|
|
152
|
+
expect(res.headers.get('x-trace')).toBe('h1');
|
|
153
|
+
expect(res.headers.get('content-type')).toBeNull();
|
|
154
|
+
expect(await res.text()).toBe('');
|
|
155
|
+
});
|
|
156
|
+
// A void handler on a non-204 status sends a clean bodyless response rather
|
|
157
|
+
// than an empty `application/json` payload — uniform with the headers-only and
|
|
158
|
+
// 204 paths (an undefined body is always bodyless).
|
|
159
|
+
test('void handler on a 200 status returns a clean bodyless response', async () => {
|
|
160
|
+
const { app } = buildApp((P) => {
|
|
161
|
+
P.CreateHttp('Void', { path: '/void', method: 'get', schema: {} }, async () => undefined);
|
|
162
|
+
});
|
|
163
|
+
const res = await app.request('/void');
|
|
164
|
+
expect(res.status).toBe(200);
|
|
165
|
+
expect(res.headers.get('content-type')).toBeNull();
|
|
166
|
+
expect(await res.text()).toBe('');
|
|
167
|
+
});
|
|
168
|
+
test('res.headers declared with a 204 status applies headers and no body', async () => {
|
|
169
|
+
const { app } = buildApp((P) => {
|
|
170
|
+
P.CreateHttp('NoContent', {
|
|
171
|
+
path: '/nc', method: 'delete',
|
|
172
|
+
schema: { res: { headers: Type.Object({}) } },
|
|
173
|
+
}, async () => ({ headers: { 'x-trace': 'd1' } }));
|
|
174
|
+
});
|
|
175
|
+
const res = await app.request('/nc', { method: 'DELETE' });
|
|
176
|
+
expect(res.status).toBe(204);
|
|
177
|
+
expect(res.headers.get('x-trace')).toBe('d1');
|
|
178
|
+
expect(await res.text()).toBe('');
|
|
179
|
+
});
|
|
117
180
|
});
|
|
118
181
|
//# sourceMappingURL=http.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.test.js","sourceRoot":"","sources":["../../../../../src/implementations/http/hono/handlers/http.test.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http.test.js","sourceRoot":"","sources":["../../../../../src/implementations/http/hono/handlers/http.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
ts-procedures can generate type-safe client SDKs directly from your server's `DocRegistry` output. Generated files include TypeScript types and callable functions for every registered procedure, organized by scope — no manual type duplication required.
|
|
6
6
|
|
|
7
|
+
## Setup / prerequisites
|
|
8
|
+
|
|
9
|
+
> **ESM only.** ts-procedures and its generated output are ESM-only — a `tsconfig.json` with `"module": "commonjs"` fails immediately. In your consuming project set `"type": "module"` in `package.json`, and in `tsconfig.json` use `"module": "ESNext"` with `"moduleResolution": "Bundler"` (or `"NodeNext"`). Run scripts with [`tsx`](https://github.com/privatenumber/tsx) during development (e.g. `tsx src/index.ts`).
|
|
10
|
+
|
|
7
11
|
## Quick Start
|
|
8
12
|
|
|
9
13
|
**Step 1 — Serve your docs endpoint** (see [DocRegistry](./http-integrations.md#docregistry--composing-docs-from-multiple-builders) for setup):
|
|
@@ -35,7 +39,11 @@ const api = createApiClient({
|
|
|
35
39
|
},
|
|
36
40
|
},
|
|
37
41
|
})
|
|
42
|
+
```
|
|
38
43
|
|
|
44
|
+
> **Note:** `basePath` must be the **origin only** (e.g. `http://localhost:3000`), not `http://localhost:3000/api`. Generated client paths already include the server's `pathPrefix` (e.g. `/api/...`), so putting the prefix in `basePath` too doubles it up to `/api/api/...`.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
39
47
|
try {
|
|
40
48
|
const user = await api.users.GetUser({ pathParams: { id: '123' } })
|
|
41
49
|
} catch (err) {
|
package/docs/core.md
CHANGED
|
@@ -214,6 +214,8 @@ schema: { params: Type.Object({ title: Type.String() }) }
|
|
|
214
214
|
|
|
215
215
|
TypeBox schemas are valid JSON Schema and work directly with AJV for runtime validation.
|
|
216
216
|
|
|
217
|
+
> **Composing schemas:** the bundled typebox has no `Type.Composite`. Compose with a flat object spread instead — `Type.Object({ ...Base.properties, extra: Type.String() })` — which also keeps the emitted JSON Schema a single `object` rather than an `allOf`.
|
|
218
|
+
|
|
217
219
|
### Validation Behavior
|
|
218
220
|
|
|
219
221
|
AJV is configured with:
|
|
@@ -134,6 +134,10 @@ new HonoAppBuilder({
|
|
|
134
134
|
|
|
135
135
|
For streaming procedures the taxonomy covers the pre-stream path only; mid-stream errors are handled via `stream.onMidStreamError`.
|
|
136
136
|
|
|
137
|
+
> **Typed client classes — when you get them for free.** A taxonomy entry declared with just `{ class, statusCode }` is self-describing: its default body is `{ name, message }`, so codegen emits a typed client error class and registry entry automatically — `catch (e) { if (e instanceof ApiErrors.NotFound) ... }` works with zero extra ceremony. The framework can only do this when it knows the wire shape. Two cases where it can't, and the client falls back to the untyped `ClientHttpError` until you help it:
|
|
138
|
+
> - **Custom `toResponse` without a `schema`.** Once you shape the body yourself, the framework won't guess it. Add an explicit `schema` matching your `toResponse` to restore the typed class.
|
|
139
|
+
> - **Raw `ErrorDoc`s** added via `DocRegistry.documentError(...)` or a `config.errors` array (rather than a taxonomy). These carry no body contract — give them a `schema` to make them typed on the client.
|
|
140
|
+
|
|
137
141
|
### Imperative — the `onError` callback
|
|
138
142
|
|
|
139
143
|
For apps that don't need typed client dispatch or declarative docs, configure `onError` directly and handle every error in one place:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-procedures",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
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",
|
|
@@ -10,6 +10,11 @@ import { tmpdir } from 'node:os'
|
|
|
10
10
|
import { join } from 'node:path'
|
|
11
11
|
import { execSync } from 'node:child_process'
|
|
12
12
|
import { generateClient } from './index.js'
|
|
13
|
+
import { emitErrorsFile } from './emit-errors.js'
|
|
14
|
+
import {
|
|
15
|
+
defineErrorTaxonomy,
|
|
16
|
+
taxonomyToErrorDocs,
|
|
17
|
+
} from '../implementations/http/error-taxonomy.js'
|
|
13
18
|
import type { DocEnvelope } from '../implementations/types.js'
|
|
14
19
|
|
|
15
20
|
describe('generated _errors.ts — runtime behavior', () => {
|
|
@@ -180,4 +185,25 @@ describe('generated _errors.ts — runtime behavior', () => {
|
|
|
180
185
|
rmSync(outDir, { recursive: true, force: true })
|
|
181
186
|
}
|
|
182
187
|
}, 30000)
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Taxonomy-derived envelope: a class+statusCode-only entry must produce a
|
|
191
|
+
// typed client error class + registry entry (previously schema-less → skipped).
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
it('emits a typed class + registry entry for a class+statusCode-only taxonomy entry', async () => {
|
|
194
|
+
const taxonomy = defineErrorTaxonomy({
|
|
195
|
+
// Only { class, statusCode } — the common case. No schema, no toResponse.
|
|
196
|
+
AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
|
|
197
|
+
})
|
|
198
|
+
const errorDocs = taxonomyToErrorDocs(taxonomy)
|
|
199
|
+
|
|
200
|
+
// The synthesized schema is what makes codegen emit a class for it.
|
|
201
|
+
const result = await emitErrorsFile(errorDocs)
|
|
202
|
+
expect(result).toBeDefined()
|
|
203
|
+
expect(result).toContain('export class AuthError')
|
|
204
|
+
expect(result).toContain('export const ErrorRegistry = {')
|
|
205
|
+
// Precise registry membership — `^\s*AuthError,$` avoids matching a
|
|
206
|
+
// substring like `MyAuthError,` or an occurrence in a comment.
|
|
207
|
+
expect(result).toMatch(/^\s*AuthError,$/m)
|
|
208
|
+
})
|
|
183
209
|
})
|
|
@@ -5,10 +5,14 @@ import {
|
|
|
5
5
|
ProcedureYieldValidationError,
|
|
6
6
|
} from '../../errors.js'
|
|
7
7
|
import type { TProcedureRegistration } from '../../index.js'
|
|
8
|
+
import * as AJV from 'ajv'
|
|
8
9
|
import {
|
|
9
10
|
defineErrorTaxonomy,
|
|
10
11
|
resolveErrorResponse,
|
|
11
12
|
defaultErrorTaxonomy,
|
|
13
|
+
taxonomyToErrorDocs,
|
|
14
|
+
defaultErrorSchema,
|
|
15
|
+
defaultErrorBody,
|
|
12
16
|
} from './error-taxonomy.js'
|
|
13
17
|
|
|
14
18
|
class UseCaseError extends Error {
|
|
@@ -436,3 +440,110 @@ describe('resolveErrorResponse', () => {
|
|
|
436
440
|
expect(Object.keys(taxonomy)).toEqual(['B', 'A'])
|
|
437
441
|
})
|
|
438
442
|
})
|
|
443
|
+
|
|
444
|
+
describe('taxonomyToErrorDocs', () => {
|
|
445
|
+
test('synthesizes a { name const, message } schema for class+statusCode-only entries', () => {
|
|
446
|
+
const taxonomy = defineErrorTaxonomy({
|
|
447
|
+
AuthError: { class: AuthError, statusCode: 401 },
|
|
448
|
+
})
|
|
449
|
+
const docs = taxonomyToErrorDocs(taxonomy)
|
|
450
|
+
const auth = docs.find((d) => d.name === 'AuthError')
|
|
451
|
+
expect(auth?.statusCode).toBe(401)
|
|
452
|
+
expect(auth?.schema).toEqual({
|
|
453
|
+
type: 'object',
|
|
454
|
+
properties: {
|
|
455
|
+
name: { type: 'string', const: 'AuthError' },
|
|
456
|
+
message: { type: 'string' },
|
|
457
|
+
},
|
|
458
|
+
required: ['name', 'message'],
|
|
459
|
+
})
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
test('does NOT synthesize a schema when a custom toResponse is present', () => {
|
|
463
|
+
const taxonomy = defineErrorTaxonomy({
|
|
464
|
+
UseCaseError: {
|
|
465
|
+
class: UseCaseError,
|
|
466
|
+
statusCode: 422,
|
|
467
|
+
toResponse: (err) => ({ name: 'UseCaseError', message: err.externalMsg }),
|
|
468
|
+
},
|
|
469
|
+
})
|
|
470
|
+
const docs = taxonomyToErrorDocs(taxonomy)
|
|
471
|
+
const useCase = docs.find((d) => d.name === 'UseCaseError')
|
|
472
|
+
expect(useCase?.schema).toBeUndefined()
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('preserves an explicit schema untouched', () => {
|
|
476
|
+
const explicitSchema = {
|
|
477
|
+
type: 'object',
|
|
478
|
+
properties: {
|
|
479
|
+
name: { type: 'string', const: 'UseCaseError' },
|
|
480
|
+
reason: { type: 'string' },
|
|
481
|
+
},
|
|
482
|
+
required: ['name', 'reason'],
|
|
483
|
+
}
|
|
484
|
+
const taxonomy = defineErrorTaxonomy({
|
|
485
|
+
UseCaseError: {
|
|
486
|
+
class: UseCaseError,
|
|
487
|
+
statusCode: 422,
|
|
488
|
+
schema: explicitSchema,
|
|
489
|
+
},
|
|
490
|
+
})
|
|
491
|
+
const docs = taxonomyToErrorDocs(taxonomy)
|
|
492
|
+
const useCase = docs.find((d) => d.name === 'UseCaseError')
|
|
493
|
+
expect(useCase?.schema).toBe(explicitSchema)
|
|
494
|
+
})
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
describe('defaultErrorSchema', () => {
|
|
498
|
+
test('synthesizes the { name, message } envelope for a bare entry', () => {
|
|
499
|
+
expect(defaultErrorSchema('AuthError', { class: AuthError, statusCode: 401 })).toEqual({
|
|
500
|
+
type: 'object',
|
|
501
|
+
properties: {
|
|
502
|
+
name: { type: 'string', const: 'AuthError' },
|
|
503
|
+
message: { type: 'string' },
|
|
504
|
+
},
|
|
505
|
+
required: ['name', 'message'],
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
test('returns undefined when a custom toResponse is present (shape unknown)', () => {
|
|
510
|
+
expect(
|
|
511
|
+
defaultErrorSchema('UseCaseError', {
|
|
512
|
+
class: UseCaseError,
|
|
513
|
+
statusCode: 422,
|
|
514
|
+
toResponse: () => ({ name: 'UseCaseError' }),
|
|
515
|
+
})
|
|
516
|
+
).toBeUndefined()
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
test('returns the explicit schema when one is set', () => {
|
|
520
|
+
const schema = { type: 'object', properties: {} }
|
|
521
|
+
expect(
|
|
522
|
+
defaultErrorSchema('UseCaseError', { class: UseCaseError, statusCode: 422, schema })
|
|
523
|
+
).toBe(schema)
|
|
524
|
+
})
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
// The synthesized schema and the runtime body share one source (defaultErrorBody).
|
|
528
|
+
// This locks the invariant: whatever the default branch serializes must validate
|
|
529
|
+
// against the schema codegen turns into the client error class. If either side
|
|
530
|
+
// changes shape, this fails before consumers see a mismatch.
|
|
531
|
+
describe('defaultErrorBody / defaultErrorSchema invariant', () => {
|
|
532
|
+
const ajv = new AJV.Ajv()
|
|
533
|
+
|
|
534
|
+
test('default body validates against the synthesized schema', () => {
|
|
535
|
+
const schema = defaultErrorSchema('AuthError', { class: AuthError, statusCode: 401 })
|
|
536
|
+
const validate = ajv.compile(schema!)
|
|
537
|
+
|
|
538
|
+
expect(validate(defaultErrorBody('AuthError', new Error('nope')))).toBe(true)
|
|
539
|
+
// A non-Error throw stringifies to a message — still valid.
|
|
540
|
+
expect(validate(defaultErrorBody('AuthError', 'plain string'))).toBe(true)
|
|
541
|
+
// Wrong discriminator name is rejected by the `const` — proves the schema
|
|
542
|
+
// actually constrains the wire shape the client dispatcher keys on.
|
|
543
|
+
expect(validate({ name: 'SomethingElse', message: 'x' })).toBe(false)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
test('default body carries exactly the schema-described keys', () => {
|
|
547
|
+
expect(Object.keys(defaultErrorBody('X', new Error('m'))).sort()).toEqual(['message', 'name'])
|
|
548
|
+
})
|
|
549
|
+
})
|
|
@@ -204,9 +204,67 @@ export const PROCEDURE_REGISTRATION_ERROR_DOC: ErrorDoc = {
|
|
|
204
204
|
},
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* The default response body for an entry without a custom `toResponse`:
|
|
209
|
+
* `{ name: <key>, message }`. This is the single source of truth for the default
|
|
210
|
+
* wire shape — `resolveErrorResponse` serializes with it and
|
|
211
|
+
* {@link defaultErrorSchema} describes it. Keeping both derived from one place
|
|
212
|
+
* means the synthesized schema can never drift from what the runtime emits.
|
|
213
|
+
*/
|
|
214
|
+
export function defaultErrorBody(key: string, err: unknown): { name: string; message: string } {
|
|
215
|
+
return {
|
|
216
|
+
name: key,
|
|
217
|
+
message: err instanceof Error ? err.message : String(err),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Synthesizes the response-body JSON Schema for a taxonomy entry that ships
|
|
223
|
+
* neither an explicit `schema` nor a custom `toResponse`.
|
|
224
|
+
*
|
|
225
|
+
* The common case for `defineErrorTaxonomy` is `{ class, statusCode }` only.
|
|
226
|
+
* For those entries the default `toResponse` (see `resolveErrorResponse`) emits
|
|
227
|
+
* exactly `{ name: <key>, message }`. Without a schema, `taxonomyToErrorDocs`
|
|
228
|
+
* produced a schema-less `ErrorDoc`, and codegen (`emit-errors.ts`) only emits a
|
|
229
|
+
* typed client error class for docs that carry a schema — so these entries
|
|
230
|
+
* silently fell back to the untyped `ClientHttpError`, while framework errors
|
|
231
|
+
* (which ship schemas) worked. That mismatch was confusing.
|
|
232
|
+
*
|
|
233
|
+
* By describing the default envelope here, the entry becomes self-describing and
|
|
234
|
+
* codegen emits a typed client error class with zero ceremony from the consumer.
|
|
235
|
+
*
|
|
236
|
+
* Rules:
|
|
237
|
+
* - Entry has an explicit `schema` → caller keeps it (this is not consulted).
|
|
238
|
+
* - Entry has a custom `toResponse` but no `schema` → returns `undefined`; the
|
|
239
|
+
* body shape is unknown and we never guess it.
|
|
240
|
+
* - Entry has neither → returns the `{ name: const <key>, message }` schema that
|
|
241
|
+
* describes {@link defaultErrorBody} — the exact body the runtime serializes.
|
|
242
|
+
* (An invariant test keeps the two from drifting.)
|
|
243
|
+
*/
|
|
244
|
+
export function defaultErrorSchema(
|
|
245
|
+
key: string,
|
|
246
|
+
entry: ErrorTaxonomyEntry
|
|
247
|
+
): Record<string, unknown> | undefined {
|
|
248
|
+
if (entry.schema) return entry.schema
|
|
249
|
+
if (entry.toResponse) return undefined
|
|
250
|
+
return {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {
|
|
253
|
+
name: { type: 'string', const: key },
|
|
254
|
+
message: { type: 'string' },
|
|
255
|
+
},
|
|
256
|
+
required: ['name', 'message'],
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
207
260
|
/**
|
|
208
261
|
* Converts a taxonomy into {@link ErrorDoc} objects suitable for a DocEnvelope.
|
|
209
262
|
*
|
|
263
|
+
* For entries that supply neither a `schema` nor a custom `toResponse`, the
|
|
264
|
+
* schema of the default `{ name, message }` envelope is synthesized (see
|
|
265
|
+
* {@link defaultErrorSchema}) so client codegen emits a typed error class for
|
|
266
|
+
* them too — matching the behavior of the schema-carrying framework defaults.
|
|
267
|
+
*
|
|
210
268
|
* @internal Used by `DocRegistry` to merge taxonomy entries into the envelope.
|
|
211
269
|
* Consumers should pass their taxonomy directly to `new DocRegistry({ errors: taxonomy })`
|
|
212
270
|
* rather than calling this helper — the constructor handles the conversion.
|
|
@@ -216,7 +274,7 @@ export function taxonomyToErrorDocs(taxonomy: ErrorTaxonomy): ErrorDoc[] {
|
|
|
216
274
|
name: key,
|
|
217
275
|
statusCode: entry.statusCode,
|
|
218
276
|
description: entry.description ?? '',
|
|
219
|
-
schema: entry
|
|
277
|
+
schema: defaultErrorSchema(key, entry),
|
|
220
278
|
}))
|
|
221
279
|
}
|
|
222
280
|
|
|
@@ -314,10 +372,7 @@ export function resolveErrorResponse(params: {
|
|
|
314
372
|
|
|
315
373
|
const rawBody = entry.toResponse
|
|
316
374
|
? entry.toResponse(candidate as any, { key })
|
|
317
|
-
:
|
|
318
|
-
name: key,
|
|
319
|
-
message: candidate instanceof Error ? candidate.message : String(candidate),
|
|
320
|
-
}
|
|
375
|
+
: defaultErrorBody(key, candidate)
|
|
321
376
|
const body = ensureName(rawBody, key)
|
|
322
377
|
|
|
323
378
|
return {
|
|
@@ -120,11 +120,79 @@ describe('installHttpRoute', () => {
|
|
|
120
120
|
path: '/q', method: 'get',
|
|
121
121
|
schema: { req: { query: Type.Any() }, res: { body: Type.Any() } },
|
|
122
122
|
},
|
|
123
|
-
async (_ctx, { query }) =>
|
|
123
|
+
async (_ctx, { query }) => query)
|
|
124
124
|
}, { api: { queryParser } })
|
|
125
125
|
|
|
126
126
|
const res = await app.request('/q?a=1&b=2')
|
|
127
127
|
expect(queryParser).toHaveBeenCalledWith('a=1&b=2')
|
|
128
128
|
expect(await res.json()).toEqual({ parsed: true, raw: 'a=1&b=2' })
|
|
129
129
|
})
|
|
130
|
+
|
|
131
|
+
test('domain object with a top-level "body" field serializes whole (no res.headers)', async () => {
|
|
132
|
+
const { app } = buildApp((P) => {
|
|
133
|
+
P.CreateHttp('GetMessage', {
|
|
134
|
+
path: '/msg', method: 'get',
|
|
135
|
+
schema: { res: { body: Type.Object({ id: Type.String(), body: Type.String() }) } },
|
|
136
|
+
}, async () => ({ id: '1', body: 'hello text' }))
|
|
137
|
+
})
|
|
138
|
+
const res = await app.request('/msg')
|
|
139
|
+
expect(res.status).toBe(200)
|
|
140
|
+
expect(await res.json()).toEqual({ id: '1', body: 'hello text' })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('envelope unwraps when res.headers is declared', async () => {
|
|
144
|
+
const { app } = buildApp((P) => {
|
|
145
|
+
P.CreateHttp('Enveloped', {
|
|
146
|
+
path: '/env', method: 'get',
|
|
147
|
+
schema: { res: { body: Type.Object({ ok: Type.Boolean() }), headers: Type.Object({}) } },
|
|
148
|
+
}, async () => ({ body: { ok: true }, headers: { 'x-trace': 'z' } }))
|
|
149
|
+
})
|
|
150
|
+
const res = await app.request('/env')
|
|
151
|
+
expect(res.status).toBe(200)
|
|
152
|
+
expect(res.headers.get('x-trace')).toBe('z')
|
|
153
|
+
expect(await res.json()).toEqual({ ok: true })
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Headers-only response shape — `res: { headers }` with no `res.body`. The
|
|
157
|
+
// handler returns `{ headers }` (no body key). The response must be bodyless,
|
|
158
|
+
// not an empty `application/json` payload that a client can't parse.
|
|
159
|
+
test('headers-only res schema returns a clean bodyless response', async () => {
|
|
160
|
+
const { app } = buildApp((P) => {
|
|
161
|
+
P.CreateHttp('HeadOnly', {
|
|
162
|
+
path: '/ho', method: 'get',
|
|
163
|
+
schema: { res: { headers: Type.Object({}) } },
|
|
164
|
+
}, async () => ({ headers: { 'x-trace': 'h1' } }))
|
|
165
|
+
})
|
|
166
|
+
const res = await app.request('/ho')
|
|
167
|
+
expect(res.status).toBe(200)
|
|
168
|
+
expect(res.headers.get('x-trace')).toBe('h1')
|
|
169
|
+
expect(res.headers.get('content-type')).toBeNull()
|
|
170
|
+
expect(await res.text()).toBe('')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// A void handler on a non-204 status sends a clean bodyless response rather
|
|
174
|
+
// than an empty `application/json` payload — uniform with the headers-only and
|
|
175
|
+
// 204 paths (an undefined body is always bodyless).
|
|
176
|
+
test('void handler on a 200 status returns a clean bodyless response', async () => {
|
|
177
|
+
const { app } = buildApp((P) => {
|
|
178
|
+
P.CreateHttp('Void', { path: '/void', method: 'get', schema: {} }, async () => undefined)
|
|
179
|
+
})
|
|
180
|
+
const res = await app.request('/void')
|
|
181
|
+
expect(res.status).toBe(200)
|
|
182
|
+
expect(res.headers.get('content-type')).toBeNull()
|
|
183
|
+
expect(await res.text()).toBe('')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('res.headers declared with a 204 status applies headers and no body', async () => {
|
|
187
|
+
const { app } = buildApp((P) => {
|
|
188
|
+
P.CreateHttp('NoContent', {
|
|
189
|
+
path: '/nc', method: 'delete',
|
|
190
|
+
schema: { res: { headers: Type.Object({}) } },
|
|
191
|
+
}, async () => ({ headers: { 'x-trace': 'd1' } }))
|
|
192
|
+
})
|
|
193
|
+
const res = await app.request('/nc', { method: 'DELETE' })
|
|
194
|
+
expect(res.status).toBe(204)
|
|
195
|
+
expect(res.headers.get('x-trace')).toBe('d1')
|
|
196
|
+
expect(await res.text()).toBe('')
|
|
197
|
+
})
|
|
130
198
|
})
|
|
@@ -103,32 +103,30 @@ export function installHttpRoute(params: {
|
|
|
103
103
|
|
|
104
104
|
cfg.api?.onSuccess?.(procedure, c)
|
|
105
105
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
// The `{ body, headers }` envelope is OPT-IN via `schema.res.headers`: when
|
|
107
|
+
// the route declares response headers, the handler returns `{ body, headers }`;
|
|
108
|
+
// otherwise its return value IS the body. Gating on the schema (rather than
|
|
109
|
+
// duck-typing the result for a `body`/`headers` key) lets a domain object
|
|
110
|
+
// safely carry its own `body`/`headers` field without being unwrapped.
|
|
111
|
+
// Normalize both shapes to a single `{ body, headers }` so the response
|
|
112
|
+
// decision below is uniform for every legal return shape.
|
|
113
|
+
const hasResHeaders = procedure.config.schema?.res?.headers != null
|
|
114
|
+
const { body, headers } = hasResHeaders
|
|
115
|
+
? ((result ?? {}) as { body?: unknown; headers?: unknown })
|
|
116
|
+
: { body: result, headers: undefined }
|
|
117
|
+
|
|
118
|
+
if (headers && typeof headers === 'object') {
|
|
119
|
+
for (const [k, v] of Object.entries(headers as Record<string, string>)) c.header(k, v)
|
|
114
120
|
}
|
|
115
121
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
headers = (result as any).headers as Record<string, string>
|
|
122
|
-
} else if (result && typeof result === 'object' && 'headers' in result && !('body' in result)) {
|
|
123
|
-
for (const [k, v] of Object.entries((result as any).headers as Record<string, string>)) {
|
|
124
|
-
c.header(k, v)
|
|
125
|
-
}
|
|
122
|
+
// No body to send — a 204 status, or an `undefined` return (a `void`
|
|
123
|
+
// handler, or a headers-only `res: { headers }` envelope). Emit a clean
|
|
124
|
+
// bodyless response instead of `c.json(undefined)`, which would send an
|
|
125
|
+
// empty, unparseable body under an `application/json` content-type.
|
|
126
|
+
if (successStatus === 204 || body === undefined) {
|
|
126
127
|
return c.body(null, successStatus as any)
|
|
127
|
-
} else if (result && typeof result === 'object' && 'body' in result && !('headers' in result)) {
|
|
128
|
-
body = (result as any).body
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
if (headers) for (const [k, v] of Object.entries(headers)) c.header(k, v)
|
|
132
130
|
return c.json(body, successStatus as any)
|
|
133
131
|
} catch (error) {
|
|
134
132
|
return dispatchPreStreamError({
|