ts-procedures 5.4.0 → 5.5.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/README.md +21 -0
- package/agent_config/claude-code/skills/guide/SKILL.md +1 -0
- package/agent_config/claude-code/skills/guide/anti-patterns.md +37 -4
- package/agent_config/claude-code/skills/guide/api-reference.md +86 -0
- package/agent_config/claude-code/skills/guide/patterns.md +33 -0
- package/agent_config/claude-code/skills/review/checklist.md +2 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +1 -1
- package/agent_config/copilot/copilot-instructions.md +4 -0
- package/agent_config/cursor/cursorrules +4 -0
- package/build/implementations/http/doc-registry.d.ts +12 -0
- package/build/implementations/http/doc-registry.js +114 -0
- package/build/implementations/http/doc-registry.js.map +1 -0
- package/build/implementations/http/doc-registry.test.d.ts +1 -0
- package/build/implementations/http/doc-registry.test.js +321 -0
- package/build/implementations/http/doc-registry.test.js.map +1 -0
- package/build/implementations/types.d.ts +31 -0
- package/package.json +5 -2
- package/src/errors.test.ts +0 -163
- package/src/errors.ts +0 -107
- package/src/exports.ts +0 -7
- package/src/implementations/http/README.md +0 -260
- package/src/implementations/http/express-rpc/README.md +0 -281
- package/src/implementations/http/express-rpc/index.test.ts +0 -957
- package/src/implementations/http/express-rpc/index.ts +0 -265
- package/src/implementations/http/express-rpc/types.ts +0 -16
- package/src/implementations/http/hono-api/index.test.ts +0 -1328
- package/src/implementations/http/hono-api/index.ts +0 -461
- package/src/implementations/http/hono-api/types.ts +0 -16
- package/src/implementations/http/hono-rpc/README.md +0 -358
- package/src/implementations/http/hono-rpc/index.test.ts +0 -1075
- package/src/implementations/http/hono-rpc/index.ts +0 -237
- package/src/implementations/http/hono-rpc/types.ts +0 -16
- package/src/implementations/http/hono-stream/README.md +0 -526
- package/src/implementations/http/hono-stream/index.test.ts +0 -1676
- package/src/implementations/http/hono-stream/index.ts +0 -435
- package/src/implementations/http/hono-stream/types.ts +0 -29
- package/src/implementations/types.ts +0 -127
- package/src/index.test.ts +0 -1194
- package/src/index.ts +0 -512
- package/src/schema/compute-schema.test.ts +0 -128
- package/src/schema/compute-schema.ts +0 -88
- package/src/schema/extract-json-schema.test.ts +0 -25
- package/src/schema/extract-json-schema.ts +0 -15
- package/src/schema/parser.test.ts +0 -182
- package/src/schema/parser.ts +0 -215
- package/src/schema/resolve-schema-lib.test.ts +0 -19
- package/src/schema/resolve-schema-lib.ts +0 -29
- package/src/schema/types.ts +0 -20
- package/src/stack-utils.test.ts +0 -94
- package/src/stack-utils.ts +0 -129
package/src/errors.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { TSchemaValidationError } from './schema/parser.js'
|
|
2
|
-
import { DefinitionInfo, DefinitionLocation, formatDefinitionInfo } from './stack-utils.js'
|
|
3
|
-
import { kebabCase } from 'es-toolkit/string'
|
|
4
|
-
|
|
5
|
-
export class ProcedureError extends Error {
|
|
6
|
-
cause?: unknown
|
|
7
|
-
readonly definedAt?: DefinitionLocation
|
|
8
|
-
readonly definitionStack?: string
|
|
9
|
-
|
|
10
|
-
constructor(
|
|
11
|
-
readonly procedureName: string,
|
|
12
|
-
readonly message: string,
|
|
13
|
-
readonly meta?: object,
|
|
14
|
-
// Used for error stack trace details
|
|
15
|
-
definitionInfo?: DefinitionInfo
|
|
16
|
-
) {
|
|
17
|
-
super(message)
|
|
18
|
-
this.name = 'ProcedureError'
|
|
19
|
-
|
|
20
|
-
if (definitionInfo) {
|
|
21
|
-
this.definedAt = definitionInfo.definedAt
|
|
22
|
-
this.definitionStack = definitionInfo.definitionStack
|
|
23
|
-
this.enhanceStack()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
27
|
-
Object.setPrototypeOf(this, ProcedureError.prototype)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Returns a formatted string showing where the procedure was defined.
|
|
32
|
-
*/
|
|
33
|
-
getDefinitionLocation(): string | undefined {
|
|
34
|
-
if (!this.definedAt) {
|
|
35
|
-
return undefined
|
|
36
|
-
}
|
|
37
|
-
return `${this.definedAt.file}:${this.definedAt.line}:${this.definedAt.column}`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Enhances the error stack with definition location information.
|
|
42
|
-
*/
|
|
43
|
-
private enhanceStack(): void {
|
|
44
|
-
if (!this.stack || !this.definedAt) {
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const definitionSection = formatDefinitionInfo(
|
|
49
|
-
{ definedAt: this.definedAt, definitionStack: this.definitionStack },
|
|
50
|
-
this.procedureName
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
if (definitionSection) {
|
|
54
|
-
this.stack = this.stack + definitionSection
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export class ProcedureValidationError extends ProcedureError {
|
|
60
|
-
constructor(
|
|
61
|
-
readonly procedureName: string,
|
|
62
|
-
message: string,
|
|
63
|
-
readonly errors?: TSchemaValidationError[],
|
|
64
|
-
// Used for error stack trace details
|
|
65
|
-
definitionInfo?: DefinitionInfo
|
|
66
|
-
) {
|
|
67
|
-
const readableErrors = errors
|
|
68
|
-
?.map((err) => `- ${kebabCase(err.instancePath).replace('-', '.')} ${err.message}`)
|
|
69
|
-
.join(', ')
|
|
70
|
-
super(procedureName, message + ' ' + readableErrors, { errors }, definitionInfo)
|
|
71
|
-
this.name = 'ProcedureValidationError'
|
|
72
|
-
|
|
73
|
-
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
74
|
-
Object.setPrototypeOf(this, ProcedureValidationError.prototype)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export class ProcedureRegistrationError extends ProcedureError {
|
|
79
|
-
constructor(
|
|
80
|
-
readonly procedureName: string,
|
|
81
|
-
message: string,
|
|
82
|
-
// Used for error stack trace details
|
|
83
|
-
definitionInfo?: DefinitionInfo
|
|
84
|
-
) {
|
|
85
|
-
super(procedureName, message, undefined, definitionInfo)
|
|
86
|
-
this.name = 'ProcedureRegistrationError'
|
|
87
|
-
|
|
88
|
-
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
89
|
-
Object.setPrototypeOf(this, ProcedureRegistrationError.prototype)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export class ProcedureYieldValidationError extends ProcedureError {
|
|
94
|
-
constructor(
|
|
95
|
-
readonly procedureName: string,
|
|
96
|
-
message: string,
|
|
97
|
-
readonly errors?: TSchemaValidationError[],
|
|
98
|
-
// Used for error stack trace details
|
|
99
|
-
definitionInfo?: DefinitionInfo
|
|
100
|
-
) {
|
|
101
|
-
super(procedureName, message, undefined, definitionInfo)
|
|
102
|
-
this.name = 'ProcedureYieldValidationError'
|
|
103
|
-
|
|
104
|
-
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
105
|
-
Object.setPrototypeOf(this, ProcedureYieldValidationError.prototype)
|
|
106
|
-
}
|
|
107
|
-
}
|
package/src/exports.ts
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
# HTTP Implementations
|
|
2
|
-
|
|
3
|
-
HTTP implementation builders for `ts-procedures` that create type-safe, versioned endpoints with automatic path generation, schema-based validation, and route documentation.
|
|
4
|
-
|
|
5
|
-
## Available Implementations
|
|
6
|
-
|
|
7
|
-
### RPC (Request/Response)
|
|
8
|
-
|
|
9
|
-
For procedures created with `Create()` - standard request/response pattern using POST.
|
|
10
|
-
|
|
11
|
-
| Framework | Package | Description |
|
|
12
|
-
|-----------|---------|-------------|
|
|
13
|
-
| [Express RPC](./express-rpc/README.md) | `express-rpc` | Express.js integration |
|
|
14
|
-
| [Hono RPC](./hono-rpc/README.md) | `hono-rpc` | Hono integration (Bun, Deno, Cloudflare Workers, Node.js) |
|
|
15
|
-
|
|
16
|
-
### Streaming
|
|
17
|
-
|
|
18
|
-
For procedures created with `CreateStream()` - server-sent events and streaming responses.
|
|
19
|
-
|
|
20
|
-
| Framework | Package | Description |
|
|
21
|
-
|-----------|---------|-------------|
|
|
22
|
-
| [Hono Stream](./hono-stream/README.md) | `hono-stream` | SSE and text streaming for async generators |
|
|
23
|
-
|
|
24
|
-
### API (REST-style)
|
|
25
|
-
|
|
26
|
-
For procedures using `schema.input` — per-channel input validation with standard HTTP methods.
|
|
27
|
-
|
|
28
|
-
| Framework | Package | Description |
|
|
29
|
-
|-----------|---------|-------------|
|
|
30
|
-
| [Hono API](./hono-api/) | `hono-api` | REST-style routing by HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD) |
|
|
31
|
-
|
|
32
|
-
## Procedure Types
|
|
33
|
-
|
|
34
|
-
| Type | Created With | Handler Return | HTTP Methods | Use Case |
|
|
35
|
-
|------|--------------|----------------|--------------|----------|
|
|
36
|
-
| RPC | `Create()` | `Promise<T>` | POST | Standard request/response |
|
|
37
|
-
| Stream | `CreateStream()` | `AsyncGenerator<T>` | GET, POST | Real-time updates, SSE |
|
|
38
|
-
| API | `Create()` | `Promise<T>` | GET, POST, PUT, DELETE, PATCH, HEAD | REST-style endpoints with per-channel input |
|
|
39
|
-
|
|
40
|
-
## Core Concepts
|
|
41
|
-
|
|
42
|
-
### Config Interface
|
|
43
|
-
|
|
44
|
-
All HTTP implementations use a shared configuration interface:
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
interface RPCConfig {
|
|
48
|
-
scope: string | string[] // Route path segment(s)
|
|
49
|
-
version: number // API version number
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
#### APIConfig (REST-style)
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
interface APIConfig {
|
|
57
|
-
path: string // Route path with Hono params (e.g., '/users/:id')
|
|
58
|
-
method: HttpMethod // 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'
|
|
59
|
-
successStatus?: number // Default: POST→201, DELETE→204, others→200
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
API routes use developer-defined paths — no auto-generation from scope/version.
|
|
64
|
-
|
|
65
|
-
### Path Generation
|
|
66
|
-
|
|
67
|
-
Routes are generated using kebab-case conversion:
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
/{pathPrefix}/{scope...}/{procedureName}/{version}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
**Examples:**
|
|
74
|
-
|
|
75
|
-
| Scope | Procedure Name | Version | Generated Path |
|
|
76
|
-
|-------|----------------|---------|----------------|
|
|
77
|
-
| `'users'` | `'Create'` | `1` | `/users/create/1` |
|
|
78
|
-
| `'users'` | `'GetById'` | `1` | `/users/get-by-id/1` |
|
|
79
|
-
| `['users', 'admin']` | `'List'` | `1` | `/users/admin/list/1` |
|
|
80
|
-
| `['UserModule', 'permissions']` | `'Update'` | `2` | `/user-module/permissions/update/2` |
|
|
81
|
-
|
|
82
|
-
**With pathPrefix `/api/v1`:**
|
|
83
|
-
|
|
84
|
-
| Scope | Procedure Name | Version | Generated Path |
|
|
85
|
-
|-------|----------------|---------|----------------|
|
|
86
|
-
| `'users'` | `'Create'` | `1` | `/api/v1/users/create/1` |
|
|
87
|
-
| `['users', 'admin']` | `'Delete'` | `2` | `/api/v1/users/admin/delete/2` |
|
|
88
|
-
|
|
89
|
-
### Context Resolution
|
|
90
|
-
|
|
91
|
-
The `factoryContext` parameter supports three patterns:
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
// 1. Static object
|
|
95
|
-
builder.register(Factory, { userId: 'static-123' })
|
|
96
|
-
|
|
97
|
-
// 2. Sync function
|
|
98
|
-
builder.register(Factory, (c) => ({
|
|
99
|
-
userId: c.req.header('x-user-id')
|
|
100
|
-
}))
|
|
101
|
-
|
|
102
|
-
// 3. Async function
|
|
103
|
-
builder.register(Factory, async (c) => {
|
|
104
|
-
const user = await validateToken(c.req.header('authorization'))
|
|
105
|
-
return { userId: user.id }
|
|
106
|
-
})
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Abort Signal
|
|
110
|
-
|
|
111
|
-
All HTTP implementations automatically inject an `AbortSignal` into the handler context as `ctx.signal`. This signal aborts when the client disconnects, enabling handlers to cancel in-flight work (fetch calls, database queries, etc.).
|
|
112
|
-
|
|
113
|
-
| Framework | Signal Source | Behavior |
|
|
114
|
-
|-----------|-------------|----------|
|
|
115
|
-
| Hono RPC | `c.req.raw.signal` | Web standard Request signal |
|
|
116
|
-
| Hono Stream | `c.req.raw.signal` | Combined with internal stream AbortController via `AbortSignal.any()` |
|
|
117
|
-
| Express RPC | Lazy `AbortController` | Created on first `ctx.signal` access, wired to `req.on('close')` |
|
|
118
|
-
|
|
119
|
-
For streaming procedures, `signal.reason` is `'stream-completed'` on normal completion, allowing handlers to distinguish from client disconnection.
|
|
120
|
-
|
|
121
|
-
### Lifecycle Hooks
|
|
122
|
-
|
|
123
|
-
**RPC Implementations:**
|
|
124
|
-
|
|
125
|
-
```
|
|
126
|
-
onRequestStart → handler → onSuccess → onRequestEnd
|
|
127
|
-
↓
|
|
128
|
-
(on error)
|
|
129
|
-
↓
|
|
130
|
-
onError handler
|
|
131
|
-
↓
|
|
132
|
-
onRequestEnd
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
**Stream Implementations:**
|
|
136
|
-
|
|
137
|
-
```
|
|
138
|
-
onRequestStart → onStreamStart → [yields...] → onStreamEnd → onRequestEnd
|
|
139
|
-
↓
|
|
140
|
-
(on error)
|
|
141
|
-
↓
|
|
142
|
-
error in stream
|
|
143
|
-
↓
|
|
144
|
-
onStreamEnd
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
| Hook | Available In | Trigger |
|
|
148
|
-
|------|--------------|---------|
|
|
149
|
-
| `onRequestStart` | Both | Before route handler |
|
|
150
|
-
| `onRequestEnd` | Both | After response sent |
|
|
151
|
-
| `onSuccess` | RPC, API | After successful handler |
|
|
152
|
-
| `onError` | RPC, API | On handler error |
|
|
153
|
-
| `onStreamStart` | Stream | Before first yield |
|
|
154
|
-
| `onStreamEnd` | Stream | After stream completes |
|
|
155
|
-
| `onPreStreamError` | Stream | On pre-stream error (validation, auth) |
|
|
156
|
-
| `onMidStreamError` | Stream | On mid-stream error (generator throws) |
|
|
157
|
-
|
|
158
|
-
### Route Documentation
|
|
159
|
-
|
|
160
|
-
Each registered procedure generates documentation accessible via `builder.docs`.
|
|
161
|
-
|
|
162
|
-
**RPC Documentation (`RPCHttpRouteDoc`):**
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
interface RPCHttpRouteDoc {
|
|
166
|
-
name: string
|
|
167
|
-
path: string
|
|
168
|
-
method: 'post'
|
|
169
|
-
scope: string | string[]
|
|
170
|
-
version: number
|
|
171
|
-
jsonSchema: {
|
|
172
|
-
body?: object // From schema.params
|
|
173
|
-
response?: object // From schema.returnType
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
**Stream Documentation (`StreamHttpRouteDoc`):**
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
interface StreamHttpRouteDoc {
|
|
182
|
-
name: string
|
|
183
|
-
path: string
|
|
184
|
-
methods: ('get' | 'post')[]
|
|
185
|
-
streamMode: 'sse' | 'text'
|
|
186
|
-
scope: string | string[]
|
|
187
|
-
version: number
|
|
188
|
-
jsonSchema: {
|
|
189
|
-
params?: object // From schema.params
|
|
190
|
-
yieldType?: object // From schema.yieldType
|
|
191
|
-
returnType?: object // From schema.returnType
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
**API Documentation (`APIHttpRouteDoc`):**
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
interface APIHttpRouteDoc {
|
|
200
|
-
name: string
|
|
201
|
-
path: string
|
|
202
|
-
method: HttpMethod
|
|
203
|
-
fullPath: string
|
|
204
|
-
successStatus?: number
|
|
205
|
-
jsonSchema: {
|
|
206
|
-
pathParams?: object // From schema.input.pathParams
|
|
207
|
-
query?: object // From schema.input.query
|
|
208
|
-
body?: object // From schema.input.body
|
|
209
|
-
headers?: object // From schema.input.headers
|
|
210
|
-
response?: object // From schema.returnType
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Builder Pattern
|
|
216
|
-
|
|
217
|
-
All implementations follow the same builder pattern:
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
const builder = new AppBuilder(config)
|
|
221
|
-
.register(PublicFactory, publicContextResolver)
|
|
222
|
-
.register(ProtectedFactory, protectedContextResolver)
|
|
223
|
-
|
|
224
|
-
const app = builder.build()
|
|
225
|
-
const docs = builder.docs
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
**Note:** `HonoAPIAppBuilder.build()` is async (resolves query parser on first call).
|
|
229
|
-
|
|
230
|
-
**Key methods:**
|
|
231
|
-
|
|
232
|
-
| Method | Returns | Description |
|
|
233
|
-
|--------|---------|-------------|
|
|
234
|
-
| `register(factory, context, options?)` | `this` | Register a procedure factory |
|
|
235
|
-
| `build()` | Framework app | Create routes and return the application |
|
|
236
|
-
|
|
237
|
-
**Properties:**
|
|
238
|
-
|
|
239
|
-
| Property | Type | Description |
|
|
240
|
-
|----------|------|-------------|
|
|
241
|
-
| `app` | Framework app | The underlying framework application |
|
|
242
|
-
| `docs` | Route doc array | Route documentation (populated after `build()`) |
|
|
243
|
-
|
|
244
|
-
## Framework Comparison
|
|
245
|
-
|
|
246
|
-
| Aspect | Express | Hono |
|
|
247
|
-
|--------|---------|------|
|
|
248
|
-
| Context param | `req: express.Request` | `c: Context` |
|
|
249
|
-
| Error handler return | `void` (mutates res) | `Response` |
|
|
250
|
-
| Body access | `req.body` | `await c.req.json()` |
|
|
251
|
-
| Header access | `req.headers['x-id']` | `c.req.header('x-id')` |
|
|
252
|
-
| JSON middleware | Auto-added (or manual) | Built-in |
|
|
253
|
-
| Streaming support | Not yet | `hono-stream` |
|
|
254
|
-
|
|
255
|
-
## TypeScript Types
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
import { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/implementations/types'
|
|
259
|
-
import type { APIConfig, APIHttpRouteDoc, APIInput, HttpMethod } from 'ts-procedures/http'
|
|
260
|
-
```
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
# ExpressRPCAppBuilder
|
|
2
|
-
|
|
3
|
-
Express.js integration for `ts-procedures` that creates type-safe RPC endpoints as POST routes.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install ts-procedures express
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import express from 'express'
|
|
15
|
-
import { Procedures } from 'ts-procedures'
|
|
16
|
-
import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/express-rpc'
|
|
17
|
-
import { v } from 'suretype'
|
|
18
|
-
|
|
19
|
-
// Define your context type
|
|
20
|
-
type AppContext = { userId: string }
|
|
21
|
-
|
|
22
|
-
// Create a procedure factory
|
|
23
|
-
const RPC = Procedures<AppContext, RPCConfig>()
|
|
24
|
-
|
|
25
|
-
// Define procedures
|
|
26
|
-
RPC.Create(
|
|
27
|
-
'GetUser',
|
|
28
|
-
{
|
|
29
|
-
scope: ['users', 'profile'],
|
|
30
|
-
version: 1,
|
|
31
|
-
schema: {
|
|
32
|
-
params: v.object({ id: v.string() }),
|
|
33
|
-
returnType: v.object({ id: v.string(), name: v.string() })
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
async (ctx, params) => {
|
|
37
|
-
return { id: params.id, name: 'John Doe' }
|
|
38
|
-
}
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
// Build the Express app
|
|
42
|
-
const builder = new ExpressRPCAppBuilder({ pathPrefix: '/rpc' })
|
|
43
|
-
.register(RPC, (req) => ({
|
|
44
|
-
userId: req.headers['x-user-id'] as string
|
|
45
|
-
}))
|
|
46
|
-
|
|
47
|
-
const app = builder.build()
|
|
48
|
-
app.listen(3000)
|
|
49
|
-
|
|
50
|
-
// POST /rpc/users/profile/get-user/1 → { id: "123", name: "John Doe" }
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Configuration
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
type ExpressRPCAppBuilderConfig = {
|
|
57
|
-
app?: express.Express // Existing Express app (optional)
|
|
58
|
-
pathPrefix?: string // Prefix for all routes (e.g., '/rpc/v1')
|
|
59
|
-
onRequestStart?: (req: express.Request) => void
|
|
60
|
-
onRequestEnd?: (req: express.Request, res: express.Response) => void
|
|
61
|
-
onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void
|
|
62
|
-
error?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
| Option | Type | Description |
|
|
67
|
-
|--------|------|------------------------------------------------------|
|
|
68
|
-
| `app` | `express.Express` | Use existing Express app instead of creating new one |
|
|
69
|
-
| `pathPrefix` | `string` | Prefix all routes (e.g., `/rpc/v1`) |
|
|
70
|
-
| `onRequestStart` | `(req) => void` | Called at start of each request |
|
|
71
|
-
| `onRequestEnd` | `(req, res) => void` | Called after response finishes |
|
|
72
|
-
| `onSuccess` | `(proc, req, res) => void` | Called on successful handler execution |
|
|
73
|
-
| `error` | `(proc, req, res, err) => void` | Custom error handler |
|
|
74
|
-
|
|
75
|
-
## Context Resolution
|
|
76
|
-
|
|
77
|
-
The context resolver receives the Express `Request` object:
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
builder.register(RPC, (req: express.Request) => ({
|
|
81
|
-
userId: req.headers['x-user-id'] as string,
|
|
82
|
-
sessionId: req.cookies?.sessionId,
|
|
83
|
-
ip: req.ip
|
|
84
|
-
}))
|
|
85
|
-
|
|
86
|
-
// Async context resolution
|
|
87
|
-
builder.register(RPC, async (req) => {
|
|
88
|
-
const token = req.headers.authorization?.replace('Bearer ', '')
|
|
89
|
-
const user = await verifyToken(token)
|
|
90
|
-
return { userId: user.id, roles: user.roles }
|
|
91
|
-
})
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Abort Signal
|
|
95
|
-
|
|
96
|
-
`ExpressRPCAppBuilder` provides a lazy `AbortSignal` on `ctx.signal`. The underlying `AbortController` and `req.on('close')` listener are only created when `ctx.signal` is first accessed, so handlers that don't use it pay no overhead.
|
|
97
|
-
|
|
98
|
-
The signal aborts when the client disconnects before the response finishes (premature close). Normal response completion does not trigger an abort.
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
RPC.Create(
|
|
102
|
-
'SlowQuery',
|
|
103
|
-
{ scope: 'data', version: 1 },
|
|
104
|
-
async (ctx, params) => {
|
|
105
|
-
// Automatically cancelled if client disconnects
|
|
106
|
-
const response = await fetch('https://slow-api.example.com/data', {
|
|
107
|
-
signal: ctx.signal,
|
|
108
|
-
})
|
|
109
|
-
return response.json()
|
|
110
|
-
}
|
|
111
|
-
)
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
To use `ctx.signal` with type safety, include `signal: AbortSignal` in your context type:
|
|
115
|
-
|
|
116
|
-
```typescript
|
|
117
|
-
type AppContext = { userId: string; signal: AbortSignal }
|
|
118
|
-
const RPC = Procedures<AppContext, RPCConfig>()
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## Error Handling
|
|
122
|
-
|
|
123
|
-
Custom error handler receives the procedure, request, response, and error:
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
const builder = new ExpressRPCAppBuilder({
|
|
127
|
-
onError: (procedure, req, res, error) => {
|
|
128
|
-
console.error(`Error in ${procedure.name}:`, error)
|
|
129
|
-
|
|
130
|
-
if (error instanceof ValidationError) {
|
|
131
|
-
res.status(400).json({ error: error.message, code: 'VALIDATION_ERROR' })
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (error instanceof AuthError) {
|
|
136
|
-
res.status(401).json({ error: 'Unauthorized', code: 'AUTH_ERROR' })
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
res.status(500).json({ error: 'Internal server error' })
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**Default error handling:** Returns `{ error: message }` with status 500.
|
|
146
|
-
|
|
147
|
-
## Using Existing Express App
|
|
148
|
-
|
|
149
|
-
When providing an existing Express app, **you must set up JSON parsing middleware**:
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
const app = express()
|
|
153
|
-
app.use(express.json()) // Required!
|
|
154
|
-
app.use(cors())
|
|
155
|
-
app.use(helmet())
|
|
156
|
-
|
|
157
|
-
const builder = new ExpressRPCAppBuilder({ app })
|
|
158
|
-
.register(RPC, contextResolver)
|
|
159
|
-
|
|
160
|
-
builder.build() // Adds RPC routes to existing app
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
When no `app` is provided, `express.json()` middleware is added automatically.
|
|
164
|
-
|
|
165
|
-
## API Reference
|
|
166
|
-
|
|
167
|
-
### Constructor
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
new ExpressRPCAppBuilder(config?: ExpressRPCAppBuilderConfig)
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Methods
|
|
174
|
-
|
|
175
|
-
| Method | Signature | Description |
|
|
176
|
-
|--------|-----------|-------------|
|
|
177
|
-
| `register` | `register<T>(factory, context): this` | Register procedure factory with context |
|
|
178
|
-
| `build` | `build(): express.Application` | Build routes and return app |
|
|
179
|
-
| `makeRPCHttpRoutePath` | `makeRPCHttpRoutePath(config: RPCConfig): string` | Generate route path |
|
|
180
|
-
|
|
181
|
-
### Static Methods
|
|
182
|
-
|
|
183
|
-
| Method | Signature | Description |
|
|
184
|
-
|--------|-----------|-------------|
|
|
185
|
-
| `makeRPCHttpRoutePath` | `static makeRPCHttpRoutePath({ config, prefix }): string` | Generate route path with custom prefix |
|
|
186
|
-
|
|
187
|
-
### Properties
|
|
188
|
-
|
|
189
|
-
| Property | Type | Description |
|
|
190
|
-
|----------|------|-------------|
|
|
191
|
-
| `app` | `express.Express` | The Express application instance |
|
|
192
|
-
| `docs` | `RPCHttpRouteDoc[]` | Route documentation (after `build()`) |
|
|
193
|
-
| `config` | `ExpressRPCAppBuilderConfig` | The configuration object |
|
|
194
|
-
|
|
195
|
-
## TypeScript Types
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
import {
|
|
199
|
-
ExpressRPCAppBuilder,
|
|
200
|
-
ExpressRPCAppBuilderConfig,
|
|
201
|
-
RPCConfig,
|
|
202
|
-
RPCHttpRouteDoc
|
|
203
|
-
} from 'ts-procedures/express-rpc'
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## Full Example
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
import express from 'express'
|
|
210
|
-
import { Procedures } from 'ts-procedures'
|
|
211
|
-
import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/express-rpc'
|
|
212
|
-
import { v } from 'suretype'
|
|
213
|
-
|
|
214
|
-
// Context types
|
|
215
|
-
type PublicContext = { source: 'public' }
|
|
216
|
-
type AuthContext = { source: 'auth'; userId: string }
|
|
217
|
-
|
|
218
|
-
// Create factories
|
|
219
|
-
const PublicRPC = Procedures<PublicContext, RPCConfig>()
|
|
220
|
-
const AuthRPC = Procedures<AuthContext, RPCConfig>()
|
|
221
|
-
|
|
222
|
-
// Public procedures
|
|
223
|
-
PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
|
|
224
|
-
status: 'ok'
|
|
225
|
-
}))
|
|
226
|
-
|
|
227
|
-
PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
|
|
228
|
-
version: '1.0.0'
|
|
229
|
-
}))
|
|
230
|
-
|
|
231
|
-
// Authenticated procedures
|
|
232
|
-
AuthRPC.Create(
|
|
233
|
-
'GetProfile',
|
|
234
|
-
{
|
|
235
|
-
scope: ['users', 'profile'],
|
|
236
|
-
version: 1,
|
|
237
|
-
schema: { returnType: v.object({ userId: v.string() }) }
|
|
238
|
-
},
|
|
239
|
-
async (ctx) => ({ userId: ctx.userId })
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
AuthRPC.Create(
|
|
243
|
-
'UpdateProfile',
|
|
244
|
-
{
|
|
245
|
-
scope: ['users', 'profile'],
|
|
246
|
-
version: 2,
|
|
247
|
-
schema: { params: v.object({ name: v.string() }) }
|
|
248
|
-
},
|
|
249
|
-
async (ctx, params) => ({ userId: ctx.userId, name: params.name })
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
// Build app
|
|
253
|
-
const builder = new ExpressRPCAppBuilder({
|
|
254
|
-
pathPrefix: '/rpc',
|
|
255
|
-
onRequestStart: (req) => console.log(`→ ${req.method} ${req.path}`),
|
|
256
|
-
onRequestEnd: (req, res) => console.log(`← ${res.statusCode}`),
|
|
257
|
-
onSuccess: (proc) => console.log(`✓ ${proc.name}`),
|
|
258
|
-
onError: (proc, req, res, err) => {
|
|
259
|
-
console.error(`✗ ${proc.name}:`, err.message)
|
|
260
|
-
res.status(500).json({ error: err.message })
|
|
261
|
-
}
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
builder
|
|
265
|
-
.register(PublicRPC, () => ({ source: 'public' as const }))
|
|
266
|
-
.register(AuthRPC, (req) => ({
|
|
267
|
-
source: 'auth' as const,
|
|
268
|
-
userId: req.headers['x-user-id'] as string || 'anonymous'
|
|
269
|
-
}))
|
|
270
|
-
|
|
271
|
-
const app = builder.build()
|
|
272
|
-
|
|
273
|
-
// Generated routes:
|
|
274
|
-
// POST /rpc/health/1
|
|
275
|
-
// POST /rpc/system/version/get-version/1
|
|
276
|
-
// POST /rpc/users/profile/get-user/1
|
|
277
|
-
// POST /rpc/users/profile/get-user/2
|
|
278
|
-
|
|
279
|
-
console.log('Routes:', builder.docs.map(d => d.path))
|
|
280
|
-
app.listen(3000)
|
|
281
|
-
```
|