ts-procedures 10.0.0 → 10.1.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/CHANGELOG.md +29 -0
- package/build/adapters/hono/envelope-parity.test.js +17 -6
- package/build/adapters/hono/envelope-parity.test.js.map +1 -1
- package/build/codegen/constants.js +2 -2
- package/build/codegen/constants.js.map +1 -1
- package/build/exports.d.ts +1 -0
- package/build/exports.js +4 -0
- package/build/exports.js.map +1 -1
- package/build/server/doc-registry.js +3 -0
- package/build/server/doc-registry.js.map +1 -1
- package/build/server/doc-registry.test.js +31 -0
- package/build/server/doc-registry.test.js.map +1 -1
- package/build/server/index.d.ts +1 -0
- package/build/server/index.js +1 -0
- package/build/server/index.js.map +1 -1
- package/build/server/spec-version.d.ts +24 -0
- package/build/server/spec-version.js +26 -0
- package/build/server/spec-version.js.map +1 -0
- package/build/server/types.d.ts +15 -0
- package/build/version.d.ts +9 -0
- package/build/version.js +11 -0
- package/build/version.js.map +1 -0
- package/docs/doc-envelope-spec.md +291 -0
- package/docs/doc-envelope.schema.json +253 -0
- package/docs/http-integrations.md +27 -1
- package/package.json +1 -1
- package/src/adapters/hono/envelope-parity.test.ts +18 -6
- package/src/codegen/constants.ts +2 -2
- package/src/exports.ts +4 -0
- package/src/server/doc-registry.test.ts +34 -0
- package/src/server/doc-registry.ts +3 -0
- package/src/server/index.ts +1 -0
- package/src/server/spec-version.ts +27 -0
- package/src/server/types.ts +15 -0
- package/src/version.ts +11 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# ts-procedures DocEnvelope — Consumer Spec
|
|
2
|
+
|
|
3
|
+
A spec for a GUI that fetches a `DocEnvelope` JSON document from a ts-procedures
|
|
4
|
+
server URL and renders it. This is the **stable wire contract** between a
|
|
5
|
+
ts-procedures server and any documentation/codegen consumer. It is a *frozen*
|
|
6
|
+
contract in the framework (changing field names/nesting breaks codegen goldens),
|
|
7
|
+
so you can build against it confidently.
|
|
8
|
+
|
|
9
|
+
Companion file: `doc-envelope.schema.json` (JSON Schema Draft-07) — validate
|
|
10
|
+
fetched documents against it.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. How you get the document
|
|
15
|
+
|
|
16
|
+
There is **no built-in endpoint**. A server author calls
|
|
17
|
+
`builder.toDocEnvelope()` (or `DocRegistry.toJSON()`) and exposes the resulting
|
|
18
|
+
JSON at a route of their choosing (e.g. `GET /api/docs`, `/openapi`, a static
|
|
19
|
+
`.json` file, etc.).
|
|
20
|
+
|
|
21
|
+
So your website's "paste a URL" flow should:
|
|
22
|
+
|
|
23
|
+
1. `fetch(url)` the user-supplied URL.
|
|
24
|
+
2. Expect `Content-Type` ~ JSON; `JSON.parse` the body.
|
|
25
|
+
3. Validate that it is an object with a `routes` **array**. That single check is
|
|
26
|
+
what the official codegen uses as its "is this an envelope?" gate. If
|
|
27
|
+
`routes` is missing/not-an-array, show a clear "not a ts-procedures docs
|
|
28
|
+
document" error.
|
|
29
|
+
4. Optionally validate fully against `doc-envelope.schema.json`.
|
|
30
|
+
|
|
31
|
+
CORS: the server must allow your origin, or you'll need a proxy. Worth surfacing
|
|
32
|
+
as a distinct error from "bad JSON".
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. Top-level shape
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface DocEnvelope {
|
|
40
|
+
specVersion?: number // envelope wire-format version; absent ⇒ treat as 1
|
|
41
|
+
generatorVersion?: string // producing ts-procedures version, e.g. "10.0.0" (informational)
|
|
42
|
+
basePath: string // e.g. "/api" (may be "")
|
|
43
|
+
headers: HeaderDoc[] // global headers (often [])
|
|
44
|
+
errors: ErrorDoc[] // the error catalog
|
|
45
|
+
routes: AnyRouteDoc[] // every procedure/route
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The authoritative producer (`DocRegistry.toJSON`) emits these six keys.
|
|
50
|
+
Resilience notes:
|
|
51
|
+
|
|
52
|
+
- **`specVersion`** is your compatibility signal — a small monotonic integer
|
|
53
|
+
that increments **only** when the envelope format changes (never on routine
|
|
54
|
+
package releases). **Branch your parser on this.** Treat a **missing**
|
|
55
|
+
`specVersion` as `1` (documents predating the field). Refuse / warn on a
|
|
56
|
+
`specVersion` higher than your GUI knows about. Current value: `1`.
|
|
57
|
+
- **`generatorVersion`** is the producing ts-procedures package version (e.g.
|
|
58
|
+
`"10.0.0"`). **Informational only** — show it in a footer/tooltip, but do
|
|
59
|
+
**not** use it to decide how to parse; it changes every release regardless of
|
|
60
|
+
format. (Note: one internal codegen *input* fixture carries an unrelated
|
|
61
|
+
top-level `"version"` string and uppercase methods — that's a loose
|
|
62
|
+
hand-authored fixture, not server output; ignore `version` if you ever see it.)
|
|
63
|
+
- Be tolerant of **extra top-level keys** — treat unknown keys as opaque.
|
|
64
|
+
- `headers` and `errors` are commonly empty arrays. Render gracefully.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 3. The error catalog (`errors`)
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
interface ErrorDoc {
|
|
72
|
+
name: string // unique; routes reference it by this name
|
|
73
|
+
statusCode: number // HTTP status, e.g. 400, 404, 418, 500
|
|
74
|
+
description: string
|
|
75
|
+
schema?: Record<string, unknown> // JSON Schema of the error body (optional)
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Framework defaults are auto-merged and always present unless opted out:
|
|
80
|
+
`ProcedureValidationError` (400), `ProcedureYieldValidationError` (500),
|
|
81
|
+
`ProcedureError` (500), `ProcedureRegistrationError` (500). User taxonomy errors
|
|
82
|
+
(e.g. `NotFound` 404, `Teapot` 418) appear alongside them.
|
|
83
|
+
|
|
84
|
+
**GUI tip:** build a `Map<name, ErrorDoc>`. Each route's `errors: string[]` is a
|
|
85
|
+
list of these names — render them as links/badges that resolve to the catalog
|
|
86
|
+
entry (status code + description + body schema).
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 4. Global headers (`headers`)
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
interface HeaderDoc {
|
|
94
|
+
name: string
|
|
95
|
+
description?: string
|
|
96
|
+
required?: boolean
|
|
97
|
+
example?: string
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Simple list; render as a small table. Often empty.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 5. Routes — discriminated union on `kind`
|
|
106
|
+
|
|
107
|
+
`routes[]` mixes four kinds. **Always switch on `route.kind`.** Every route has a
|
|
108
|
+
`name` and a `jsonSchema` object; the rest varies by kind.
|
|
109
|
+
|
|
110
|
+
| `kind` | Transport | Path field(s) | Method field | Payload schema keys |
|
|
111
|
+
|-----------------|----------------------|----------------------|---------------------|----------------------------------------------|
|
|
112
|
+
| `rpc` | JSON-RPC (POST) | `path` | `method: "post"` | `body`, `response` |
|
|
113
|
+
| `stream` | RPC stream (SSE/text)| `path` | `methods: string[]` | `params`, `yieldType`, `returnType` |
|
|
114
|
+
| `api` | REST | `path?`, `fullPath` | `method` | `req.{pathParams,query,body,headers}`, `res.{body,headers}` |
|
|
115
|
+
| `http-stream` | REST stream (SSE) | `path?`, `fullPath` | `method` | `req.{...}`, `res.{headers}`, `yield`, `returnType` |
|
|
116
|
+
|
|
117
|
+
Common fields: `name` (string), `errors?` (string[] → catalog names),
|
|
118
|
+
`scope` (grouping; `string | string[]` for rpc/stream, `string` for api/http-stream).
|
|
119
|
+
|
|
120
|
+
> **No per-route `description`.** A `description` passed at procedure-definition
|
|
121
|
+
> time is **not** carried into the route doc. Don't build UI that depends on it.
|
|
122
|
+
|
|
123
|
+
### 5.1 `rpc`
|
|
124
|
+
|
|
125
|
+
```jsonc
|
|
126
|
+
{
|
|
127
|
+
"kind": "rpc",
|
|
128
|
+
"name": "GetUser",
|
|
129
|
+
"version": 1,
|
|
130
|
+
"scope": "users",
|
|
131
|
+
"path": "/api/users/get-user/1", // full path; embeds scope + version
|
|
132
|
+
"method": "post",
|
|
133
|
+
"jsonSchema": {
|
|
134
|
+
"body": { "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } } },
|
|
135
|
+
"response": { "type": "object", "required": ["id","name"], "properties": { "id":{"type":"string"}, "name":{"type":"string"} } }
|
|
136
|
+
},
|
|
137
|
+
"errors": ["Teapot"]
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- `jsonSchema.body` = request input (runtime-validated).
|
|
142
|
+
- `jsonSchema.response` = response shape (documentation only).
|
|
143
|
+
- Both are optional (a no-arg/no-return procedure yields `jsonSchema: {}`).
|
|
144
|
+
|
|
145
|
+
### 5.2 `stream` (RPC stream)
|
|
146
|
+
|
|
147
|
+
```jsonc
|
|
148
|
+
{
|
|
149
|
+
"kind": "stream",
|
|
150
|
+
"name": "WatchUser",
|
|
151
|
+
"version": 2,
|
|
152
|
+
"scope": ["users", "live"],
|
|
153
|
+
"path": "/api/users/live/watch-user/2",
|
|
154
|
+
"methods": ["post", "get"],
|
|
155
|
+
"streamMode": "sse", // "sse" | "text"
|
|
156
|
+
"jsonSchema": {
|
|
157
|
+
"params": { "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } } },
|
|
158
|
+
"yieldType": { /* see note */ },
|
|
159
|
+
"returnType": { "type": "object", "required": ["total"], "properties": { "total": { "type": "number" } } }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- `params` = input (query params for GET, body for POST).
|
|
165
|
+
- **`yieldType` in `sse` mode is the SSE envelope**, not the bare value:
|
|
166
|
+
```jsonc
|
|
167
|
+
{
|
|
168
|
+
"type": "object",
|
|
169
|
+
"description": "SSE message envelope. The data field contains the procedure yield value.",
|
|
170
|
+
"required": ["data", "event", "id"],
|
|
171
|
+
"properties": {
|
|
172
|
+
"data": { /* the actual per-yield value schema */ },
|
|
173
|
+
"event": { "type": "string" },
|
|
174
|
+
"id": { "type": "string" },
|
|
175
|
+
"retry": { "type": "number" }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
In `text` mode `yieldType` is the bare value schema. **GUI tip:** to show "what
|
|
180
|
+
each event carries", detect the SSE envelope (object with `data`/`event`/`id`)
|
|
181
|
+
and surface `properties.data`; otherwise show `yieldType` directly.
|
|
182
|
+
- `returnType` = value delivered on stream completion (carried by the `return`
|
|
183
|
+
SSE event). Optional.
|
|
184
|
+
- `errors` here are pre-stream errors only.
|
|
185
|
+
|
|
186
|
+
### 5.3 `api` (REST)
|
|
187
|
+
|
|
188
|
+
```jsonc
|
|
189
|
+
{
|
|
190
|
+
"kind": "api",
|
|
191
|
+
"name": "UpdateUser",
|
|
192
|
+
"scope": "users",
|
|
193
|
+
"path": "/users/:id", // as registered (may have :params)
|
|
194
|
+
"method": "put",
|
|
195
|
+
"fullPath": "/api/users/:id", // resolved, incl. basePath — prefer this for display
|
|
196
|
+
"successStatus": 200, // optional; defaults POST→201, DELETE→204, else 200
|
|
197
|
+
"jsonSchema": {
|
|
198
|
+
"req": {
|
|
199
|
+
"pathParams": { "type":"object", "required":["id"], "properties":{"id":{"type":"string"}} },
|
|
200
|
+
"query": { "type":"object", "properties":{"dryRun":{"type":"boolean"}} },
|
|
201
|
+
"body": { "type":"object", "required":["name"], "properties":{"name":{"type":"string"}} }
|
|
202
|
+
// "headers" also possible
|
|
203
|
+
},
|
|
204
|
+
"res": {
|
|
205
|
+
"body": { "type":"object", "required":["id","name"], "properties":{"id":{"type":"string"},"name":{"type":"string"}} },
|
|
206
|
+
"headers": { "type":"object", "required":["x-rev"], "properties":{"x-rev":{"type":"string"}} }
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
"errors": ["Teapot"]
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- `req` channels: any of `pathParams`, `query`, `body`, `headers` (all optional).
|
|
214
|
+
- `res` channels: `body`, `headers` (both optional).
|
|
215
|
+
- `jsonSchema.req`/`res` may be `{}` or absent for trivial endpoints.
|
|
216
|
+
- Prefer `fullPath` for the displayed URL; `path` may be omitted in some
|
|
217
|
+
envelopes.
|
|
218
|
+
|
|
219
|
+
### 5.4 `http-stream` (REST stream)
|
|
220
|
+
|
|
221
|
+
```jsonc
|
|
222
|
+
{
|
|
223
|
+
"kind": "http-stream",
|
|
224
|
+
"name": "TailUser",
|
|
225
|
+
"scope": "users",
|
|
226
|
+
"path": "/users/:id/tail",
|
|
227
|
+
"method": "get",
|
|
228
|
+
"fullPath": "/api/users/:id/tail",
|
|
229
|
+
"streamMode": "sse",
|
|
230
|
+
"jsonSchema": {
|
|
231
|
+
"req": { "pathParams": { "type":"object", "required":["id"], "properties":{"id":{"type":"string"}} } },
|
|
232
|
+
"res": { "headers": { /* optional response headers schema */ } },
|
|
233
|
+
"yield": { "type":"object", "required":["line"], "properties":{"line":{"type":"string"}} },
|
|
234
|
+
"returnType": { "type":"object", "required":["lines"], "properties":{"lines":{"type":"number"}} }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- Same `req` channels as `api`.
|
|
240
|
+
- `res` has **`headers` only** (no `body` — streaming responses have no single body).
|
|
241
|
+
- `yield` = per-event value schema, **raw** (NOT wrapped in an SSE envelope,
|
|
242
|
+
unlike `kind: "stream"`).
|
|
243
|
+
- `returnType` = completion value. Optional.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 6. Embedded payload schemas
|
|
248
|
+
|
|
249
|
+
Every `jsonSchema.*` leaf (`body`, `response`, `params`, `yield`, `req.query`,
|
|
250
|
+
error `schema`, …) is a plain **JSON Schema (Draft-07)** object as produced by
|
|
251
|
+
the server's schema layer (TypeBox → JSON Schema). Expect standard keywords:
|
|
252
|
+
`type`, `properties`, `required`, `items`, `enum`, `const`, `format`, `oneOf`,
|
|
253
|
+
nested objects/arrays, etc.
|
|
254
|
+
|
|
255
|
+
**Render these with a generic JSON-Schema viewer.** Don't hard-code field lists.
|
|
256
|
+
Useful libraries: `@apidevtools/json-schema-ref-parser` (deref `$ref` if any),
|
|
257
|
+
or any JSON-Schema-to-table/form renderer. They are validation-grade schemas, so
|
|
258
|
+
you can also drive a "try it" form or example generator from them.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 7. Suggested rendering model
|
|
263
|
+
|
|
264
|
+
1. **Header:** `basePath`, route count, kind breakdown.
|
|
265
|
+
2. **Sidebar:** group routes by `scope` (join array scopes with `/`), then by
|
|
266
|
+
`kind`. Label each with `name` + method/path.
|
|
267
|
+
3. **Route detail:**
|
|
268
|
+
- Title: `name`; badge: `kind`; method(s); display path (`fullPath` ?? `path`).
|
|
269
|
+
- For `rpc`/`stream`: also show `version`.
|
|
270
|
+
- **Request** section from `body`/`params`/`req.*`.
|
|
271
|
+
- **Response** section from `response`/`res.*`; for streams, a **Stream**
|
|
272
|
+
section from `yield`/`yieldType` (+ `returnType`).
|
|
273
|
+
- **Errors** section: resolve each `errors[]` name against the catalog and
|
|
274
|
+
show status + description + body schema.
|
|
275
|
+
4. **Errors page:** full `errors[]` catalog.
|
|
276
|
+
5. **Headers page:** `headers[]` table.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 8. Defensive checklist for the GUI
|
|
281
|
+
|
|
282
|
+
- Validate `routes` is an array before anything else; otherwise it's not an envelope.
|
|
283
|
+
- Switch on `kind`; render unknown `kind` values generically (future-proofing).
|
|
284
|
+
- Treat HTTP methods case-insensitively (canonical output is lowercase, but
|
|
285
|
+
normalize anyway).
|
|
286
|
+
- Tolerate empty/absent `headers`, `errors`, `jsonSchema`, `req`, `res`.
|
|
287
|
+
- Tolerate extra/unknown properties at every level.
|
|
288
|
+
- Read `specVersion` first (default 1 if absent) and branch parsing on it; warn on unknown-higher.
|
|
289
|
+
- Don't expect a per-route `description`. `generatorVersion` is display-only, never a parse signal.
|
|
290
|
+
- Prefer `fullPath` over `path` for `api`/`http-stream` display.
|
|
291
|
+
- Unwrap the SSE envelope for `stream` `yieldType.data` when showing "event payload".
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://ts-procedures.dev/schemas/doc-envelope.schema.json",
|
|
4
|
+
"title": "ts-procedures DocEnvelope",
|
|
5
|
+
"description": "The JSON document a ts-procedures server exposes (via builder.toDocEnvelope() / DocRegistry.toJSON()). Describes every procedure/route the server serves, the global error catalog, and global headers. Payload schemas embedded under each route's `jsonSchema` are arbitrary JSON Schema (Draft-07) objects and are intentionally left unconstrained here.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["basePath", "headers", "errors", "routes"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"specVersion": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"minimum": 1,
|
|
12
|
+
"description": "Wire-format version of the envelope. A small monotonic integer, bumped ONLY when the envelope shape changes — not on every package release. Branch on this to pick a parser. ABSENT means treat as 1 (envelopes predating this field). Emitted by ts-procedures servers; optional here so older/hand-built documents still validate."
|
|
13
|
+
},
|
|
14
|
+
"generatorVersion": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "ts-procedures package version that produced the envelope, e.g. \"10.0.0\". Informational only (telemetry/debugging) — do NOT use it for parse-compatibility decisions; use specVersion."
|
|
17
|
+
},
|
|
18
|
+
"basePath": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Path prefix shared by all routes, e.g. \"/api\". May be the empty string."
|
|
21
|
+
},
|
|
22
|
+
"headers": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"description": "Global HTTP headers documented for the whole API.",
|
|
25
|
+
"items": { "$ref": "#/definitions/HeaderDoc" }
|
|
26
|
+
},
|
|
27
|
+
"errors": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"description": "Catalog of every error the API may return (framework defaults + user-defined taxonomy). Routes reference these by name via their `errors` array.",
|
|
30
|
+
"items": { "$ref": "#/definitions/ErrorDoc" }
|
|
31
|
+
},
|
|
32
|
+
"routes": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"description": "All procedures/routes. Discriminated union on `kind`.",
|
|
35
|
+
"items": { "$ref": "#/definitions/AnyRouteDoc" }
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": true,
|
|
39
|
+
"definitions": {
|
|
40
|
+
"JsonSchema": {
|
|
41
|
+
"description": "An embedded JSON Schema (Draft-07) object describing a payload. Left unconstrained on purpose — render it generically.",
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": true
|
|
44
|
+
},
|
|
45
|
+
"HeaderDoc": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": ["name"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"name": { "type": "string" },
|
|
50
|
+
"description": { "type": "string" },
|
|
51
|
+
"required": { "type": "boolean" },
|
|
52
|
+
"example": { "type": "string" }
|
|
53
|
+
},
|
|
54
|
+
"additionalProperties": true
|
|
55
|
+
},
|
|
56
|
+
"ErrorDoc": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": ["name", "statusCode", "description"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"name": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Unique error name. Routes link to it via their `errors` array."
|
|
63
|
+
},
|
|
64
|
+
"statusCode": {
|
|
65
|
+
"type": "integer",
|
|
66
|
+
"description": "HTTP status code returned for this error.",
|
|
67
|
+
"minimum": 100,
|
|
68
|
+
"maximum": 599
|
|
69
|
+
},
|
|
70
|
+
"description": { "type": "string" },
|
|
71
|
+
"schema": {
|
|
72
|
+
"$ref": "#/definitions/JsonSchema",
|
|
73
|
+
"description": "JSON Schema of the error response body. Optional."
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"additionalProperties": true
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
"AnyRouteDoc": {
|
|
80
|
+
"oneOf": [
|
|
81
|
+
{ "$ref": "#/definitions/RPCRouteDoc" },
|
|
82
|
+
{ "$ref": "#/definitions/StreamRouteDoc" },
|
|
83
|
+
{ "$ref": "#/definitions/APIRouteDoc" },
|
|
84
|
+
{ "$ref": "#/definitions/HttpStreamRouteDoc" }
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
"HttpMethod": {
|
|
89
|
+
"description": "HTTP method. Canonical wire form is lowercase; treat case-insensitively when rendering.",
|
|
90
|
+
"type": "string",
|
|
91
|
+
"enum": ["get", "post", "put", "delete", "patch", "head", "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"]
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
"StreamMode": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"enum": ["sse", "text"],
|
|
97
|
+
"description": "Stream transport. `sse` = Server-Sent Events; `text` = raw text chunks."
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
"Scope": {
|
|
101
|
+
"description": "Logical grouping. A single segment or an array of segments.",
|
|
102
|
+
"oneOf": [
|
|
103
|
+
{ "type": "string" },
|
|
104
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
"RPCRouteDoc": {
|
|
109
|
+
"title": "RPC route (kind = rpc)",
|
|
110
|
+
"description": "JSON-RPC-style procedure. Always POST. Path embeds scope + version.",
|
|
111
|
+
"type": "object",
|
|
112
|
+
"required": ["kind", "name", "path", "method", "scope", "version", "jsonSchema"],
|
|
113
|
+
"properties": {
|
|
114
|
+
"kind": { "const": "rpc" },
|
|
115
|
+
"name": { "type": "string" },
|
|
116
|
+
"path": { "type": "string", "description": "Full path incl. basePath + version, e.g. /api/users/get-user/1" },
|
|
117
|
+
"method": { "const": "post" },
|
|
118
|
+
"scope": { "$ref": "#/definitions/Scope" },
|
|
119
|
+
"version": { "type": "number" },
|
|
120
|
+
"errors": { "type": "array", "items": { "type": "string" } },
|
|
121
|
+
"jsonSchema": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"body": { "$ref": "#/definitions/JsonSchema", "description": "Request body schema (validated)." },
|
|
125
|
+
"response": { "$ref": "#/definitions/JsonSchema", "description": "Response body schema (doc only)." }
|
|
126
|
+
},
|
|
127
|
+
"additionalProperties": true
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"additionalProperties": true
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
"StreamRouteDoc": {
|
|
134
|
+
"title": "RPC stream route (kind = stream)",
|
|
135
|
+
"description": "Streaming RPC procedure. Served over both POST and GET. Path embeds scope + version.",
|
|
136
|
+
"type": "object",
|
|
137
|
+
"required": ["kind", "name", "path", "methods", "streamMode", "scope", "version", "jsonSchema"],
|
|
138
|
+
"properties": {
|
|
139
|
+
"kind": { "const": "stream" },
|
|
140
|
+
"name": { "type": "string" },
|
|
141
|
+
"path": { "type": "string" },
|
|
142
|
+
"methods": {
|
|
143
|
+
"type": "array",
|
|
144
|
+
"description": "Allowed methods, conventionally [\"post\", \"get\"].",
|
|
145
|
+
"items": { "type": "string", "enum": ["get", "post"] }
|
|
146
|
+
},
|
|
147
|
+
"streamMode": { "$ref": "#/definitions/StreamMode" },
|
|
148
|
+
"scope": { "$ref": "#/definitions/Scope" },
|
|
149
|
+
"version": { "type": "number" },
|
|
150
|
+
"errors": { "type": "array", "items": { "type": "string" }, "description": "Pre-stream errors only." },
|
|
151
|
+
"jsonSchema": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {
|
|
154
|
+
"params": { "$ref": "#/definitions/JsonSchema", "description": "Query params (GET) or body (POST)." },
|
|
155
|
+
"yieldType": {
|
|
156
|
+
"$ref": "#/definitions/JsonSchema",
|
|
157
|
+
"description": "Schema for each streamed value. In `sse` mode this is the SSE envelope { data, event, id, retry } whose `data` holds the user's yield schema; in `text` mode it is the raw yield schema."
|
|
158
|
+
},
|
|
159
|
+
"returnType": { "$ref": "#/definitions/JsonSchema", "description": "Final return value schema." }
|
|
160
|
+
},
|
|
161
|
+
"additionalProperties": true
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"additionalProperties": true
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
"APIRouteDoc": {
|
|
168
|
+
"title": "REST route (kind = api)",
|
|
169
|
+
"description": "REST-style HTTP endpoint with explicit method + path.",
|
|
170
|
+
"type": "object",
|
|
171
|
+
"required": ["kind", "name", "method", "fullPath", "jsonSchema"],
|
|
172
|
+
"properties": {
|
|
173
|
+
"kind": { "const": "api" },
|
|
174
|
+
"name": { "type": "string" },
|
|
175
|
+
"scope": { "type": "string" },
|
|
176
|
+
"path": { "type": "string", "description": "Path as registered (may include :params), e.g. /users/:id. May be omitted in some envelopes; prefer fullPath." },
|
|
177
|
+
"method": { "$ref": "#/definitions/HttpMethod" },
|
|
178
|
+
"fullPath": { "type": "string", "description": "Resolved path incl. basePath prefix, e.g. /api/users/:id." },
|
|
179
|
+
"successStatus": { "type": "integer", "description": "Status on success. Defaults: POST→201, DELETE→204, else 200." },
|
|
180
|
+
"errors": { "type": "array", "items": { "type": "string" } },
|
|
181
|
+
"jsonSchema": {
|
|
182
|
+
"type": "object",
|
|
183
|
+
"properties": {
|
|
184
|
+
"req": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"properties": {
|
|
187
|
+
"pathParams": { "$ref": "#/definitions/JsonSchema" },
|
|
188
|
+
"query": { "$ref": "#/definitions/JsonSchema" },
|
|
189
|
+
"body": { "$ref": "#/definitions/JsonSchema" },
|
|
190
|
+
"headers": { "$ref": "#/definitions/JsonSchema" }
|
|
191
|
+
},
|
|
192
|
+
"additionalProperties": true
|
|
193
|
+
},
|
|
194
|
+
"res": {
|
|
195
|
+
"type": "object",
|
|
196
|
+
"properties": {
|
|
197
|
+
"body": { "$ref": "#/definitions/JsonSchema" },
|
|
198
|
+
"headers": { "$ref": "#/definitions/JsonSchema" }
|
|
199
|
+
},
|
|
200
|
+
"additionalProperties": true
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"additionalProperties": true
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"additionalProperties": true
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
"HttpStreamRouteDoc": {
|
|
210
|
+
"title": "REST stream route (kind = http-stream)",
|
|
211
|
+
"description": "REST-style streaming endpoint. Response has no single body; values arrive via `yield`. Currently always SSE.",
|
|
212
|
+
"type": "object",
|
|
213
|
+
"required": ["kind", "name", "method", "fullPath", "streamMode", "jsonSchema"],
|
|
214
|
+
"properties": {
|
|
215
|
+
"kind": { "const": "http-stream" },
|
|
216
|
+
"name": { "type": "string" },
|
|
217
|
+
"scope": { "type": "string" },
|
|
218
|
+
"path": { "type": "string" },
|
|
219
|
+
"method": { "$ref": "#/definitions/HttpMethod" },
|
|
220
|
+
"fullPath": { "type": "string" },
|
|
221
|
+
"streamMode": { "$ref": "#/definitions/StreamMode" },
|
|
222
|
+
"errors": { "type": "array", "items": { "type": "string" } },
|
|
223
|
+
"jsonSchema": {
|
|
224
|
+
"type": "object",
|
|
225
|
+
"properties": {
|
|
226
|
+
"req": {
|
|
227
|
+
"type": "object",
|
|
228
|
+
"properties": {
|
|
229
|
+
"pathParams": { "$ref": "#/definitions/JsonSchema" },
|
|
230
|
+
"query": { "$ref": "#/definitions/JsonSchema" },
|
|
231
|
+
"body": { "$ref": "#/definitions/JsonSchema" },
|
|
232
|
+
"headers": { "$ref": "#/definitions/JsonSchema" }
|
|
233
|
+
},
|
|
234
|
+
"additionalProperties": true
|
|
235
|
+
},
|
|
236
|
+
"res": {
|
|
237
|
+
"type": "object",
|
|
238
|
+
"description": "Response headers only — streaming responses have no single body.",
|
|
239
|
+
"properties": {
|
|
240
|
+
"headers": { "$ref": "#/definitions/JsonSchema" }
|
|
241
|
+
},
|
|
242
|
+
"additionalProperties": true
|
|
243
|
+
},
|
|
244
|
+
"yield": { "$ref": "#/definitions/JsonSchema", "description": "Schema for each streamed value. NOT wrapped in an SSE envelope (unlike kind=stream)." },
|
|
245
|
+
"returnType": { "$ref": "#/definitions/JsonSchema", "description": "Final return value schema." }
|
|
246
|
+
},
|
|
247
|
+
"additionalProperties": true
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"additionalProperties": true
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -311,7 +311,33 @@ const docs = new DocRegistry({ errors: appErrors, basePath: '/api' })
|
|
|
311
311
|
|
|
312
312
|
`http-stream` route docs include the route's `yield` and `res.headers` schemas. (Through v8, live envelopes silently dropped these for `http-stream` routes, so generated clients lost those types unless the envelope was hand-authored; v9 fixed this.)
|
|
313
313
|
|
|
314
|
-
|
|
314
|
+
### Envelope versioning
|
|
315
|
+
|
|
316
|
+
Every envelope carries two top-level metadata fields so downstream consumers (docs viewers, codegen, dashboards) can reason about format compatibility:
|
|
317
|
+
|
|
318
|
+
```jsonc
|
|
319
|
+
{
|
|
320
|
+
"specVersion": 1, // wire-format version of the envelope itself
|
|
321
|
+
"generatorVersion": "10.0.0", // ts-procedures package version that produced it
|
|
322
|
+
"basePath": "/api",
|
|
323
|
+
"headers": [],
|
|
324
|
+
"errors": [ /* ... */ ],
|
|
325
|
+
"routes": [ /* ... */ ]
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- **`specVersion`** is a small monotonic integer, bumped **only** when the envelope shape changes in a way parsers must react to — never on ordinary package releases. Branch on it. An envelope with no `specVersion` predates the field and must be treated as `1`. The current value is exported as `DOC_ENVELOPE_SPEC_VERSION` from `ts-procedures` and `ts-procedures/server`.
|
|
330
|
+
- **`generatorVersion`** is the producing package version (`DOC_ENVELOPE_GENERATOR_VERSION`, exported from `ts-procedures/server`). It is informational only — for telemetry and "which release served this?" — and must **not** drive parse-compatibility decisions, since it churns on every release regardless of the envelope format.
|
|
331
|
+
|
|
332
|
+
If you'd rather not advertise the exact package version on a public docs endpoint, strip it with the `transform` hook:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
app.get('/docs', (c) => c.json(
|
|
336
|
+
builder.toDocEnvelope({ transform: ({ generatorVersion, ...env }) => env }),
|
|
337
|
+
))
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The `DocRegistry` output is the input for [Client Code Generation](./client-and-codegen.md). Building a third-party docs viewer or other tool that consumes the served envelope? The full wire contract — every field, per-kind shapes, and a validatable JSON Schema — is in [DocEnvelope Spec](./doc-envelope-spec.md) (`doc-envelope.schema.json`).
|
|
315
341
|
|
|
316
342
|
## Type Exports
|
|
317
343
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-procedures",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.1.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",
|
|
@@ -3,8 +3,10 @@ import { dirname, resolve } from 'node:path'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { describe, expect, it } from 'vitest'
|
|
5
5
|
import { Type } from 'typebox'
|
|
6
|
+
import pkg from '../../../package.json' with { type: 'json' }
|
|
6
7
|
import { Procedures } from '../../core/procedures.js'
|
|
7
8
|
import { HonoAppBuilder, defineErrorTaxonomy } from './index.js'
|
|
9
|
+
import { DOC_ENVELOPE_SPEC_VERSION } from '../../server/spec-version.js'
|
|
8
10
|
import type { RPCConfig } from '../../server/types.js'
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -16,12 +18,15 @@ import type { RPCConfig } from '../../server/types.js'
|
|
|
16
18
|
* The envelope is codegen's input; if this test fails, generated client output
|
|
17
19
|
* WILL drift.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
* dropped the `yield` schema (and `res.headers`) from
|
|
21
|
-
* the registration stored `yieldType` while the
|
|
22
|
-
* fixes that, so the fixture's TailUser route
|
|
23
|
-
* `yield` schema the builder now
|
|
24
|
-
*
|
|
21
|
+
* TWO documented deviations from the raw v8 capture:
|
|
22
|
+
* 1. v8's CreateHttpStream dropped the `yield` schema (and `res.headers`) from
|
|
23
|
+
* http-stream route docs — the registration stored `yieldType` while the
|
|
24
|
+
* doc builder read `yield`. v9 fixes that, so the fixture's TailUser route
|
|
25
|
+
* was patched to include the `yield` schema the builder now emits.
|
|
26
|
+
* 2. v9 adds envelope-level `specVersion`/`generatorVersion` metadata that v8
|
|
27
|
+
* never emitted. These are asserted explicitly below, then stripped before
|
|
28
|
+
* the deep-equal so the rest stays byte-for-byte with the v8 capture.
|
|
29
|
+
* Everything else is the untouched v8 capture.
|
|
25
30
|
*/
|
|
26
31
|
class TeapotError extends Error {}
|
|
27
32
|
|
|
@@ -121,6 +126,13 @@ describe('DocEnvelope parity with v8.6.0', () => {
|
|
|
121
126
|
const expected = JSON.parse(await readFile(fixturePath, 'utf-8'))
|
|
122
127
|
const actual = JSON.parse(JSON.stringify(builder.toDocEnvelope()))
|
|
123
128
|
|
|
129
|
+
// Deviation #2: assert the new v9 envelope metadata, then strip it so the
|
|
130
|
+
// remaining shape matches the untouched v8 capture.
|
|
131
|
+
expect(actual.specVersion).toBe(DOC_ENVELOPE_SPEC_VERSION)
|
|
132
|
+
expect(actual.generatorVersion).toBe(pkg.version)
|
|
133
|
+
delete actual.specVersion
|
|
134
|
+
delete actual.generatorVersion
|
|
135
|
+
|
|
124
136
|
expect(actual).toEqual(expected)
|
|
125
137
|
})
|
|
126
138
|
})
|
package/src/codegen/constants.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { PACKAGE_VERSION } from '../version.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Stable, version-agnostic token embedded in every generated file (across all
|
|
@@ -7,4 +7,4 @@ import pkg from '../../package.json' with { type: 'json' }
|
|
|
7
7
|
* the package version (an orphan left by an older release must still match).
|
|
8
8
|
*/
|
|
9
9
|
export const CODEGEN_SIGNATURE = 'ts-procedures-codegen'
|
|
10
|
-
export const CODEGEN_HEADER = `// Auto-generated by ${CODEGEN_SIGNATURE} (v${
|
|
10
|
+
export const CODEGEN_HEADER = `// Auto-generated by ${CODEGEN_SIGNATURE} (v${PACKAGE_VERSION}) — do not edit`
|
package/src/exports.ts
CHANGED
|
@@ -56,5 +56,9 @@ export type { Infer, TSchemaLib, TJSONSchema, Prettify } from './schema/json-sch
|
|
|
56
56
|
|
|
57
57
|
// Doc envelope — offline codegen input
|
|
58
58
|
export { writeDocEnvelope } from './server/doc-envelope.js'
|
|
59
|
+
// `generatorVersion` metadata lives on `ts-procedures/server`
|
|
60
|
+
// (DOC_ENVELOPE_GENERATOR_VERSION) — it's niche telemetry. The spec version is
|
|
61
|
+
// the one consumers branch on, so it gets the prominent top-level export.
|
|
62
|
+
export { DOC_ENVELOPE_SPEC_VERSION } from './server/spec-version.js'
|
|
59
63
|
export type { DocEnvelopeSource } from './server/doc-envelope.js'
|
|
60
64
|
export type { DocEnvelope } from './server/types.js'
|