ts-procedures 1.1.0 → 2.0.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 +3 -3
- package/build/implementations/http/client/index.d.ts +1 -0
- package/build/implementations/http/client/index.js +2 -0
- package/build/implementations/http/client/index.js.map +1 -0
- package/build/implementations/http/express/index.d.ts +2 -1
- package/build/implementations/http/express/index.js.map +1 -1
- package/build/implementations/http/express/types.d.ts +17 -0
- package/build/implementations/http/express/types.js +2 -0
- package/build/implementations/http/express/types.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +82 -0
- package/build/implementations/http/express-rpc/index.js +140 -0
- package/build/implementations/http/express-rpc/index.js.map +1 -0
- package/build/implementations/http/express-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/index.test.js +445 -0
- package/build/implementations/http/express-rpc/index.test.js.map +1 -0
- package/build/implementations/http/express-rpc/types.d.ts +28 -0
- package/build/implementations/http/express-rpc/types.js +2 -0
- package/build/implementations/http/express-rpc/types.js.map +1 -0
- package/build/implementations/types.d.ts +17 -0
- package/build/implementations/types.js +2 -0
- package/build/implementations/types.js.map +1 -0
- package/build/schema/parser.js +2 -1
- package/build/schema/parser.js.map +1 -1
- package/package.json +13 -7
- package/src/implementations/http/express-rpc/README.md +321 -0
- package/src/implementations/http/express-rpc/index.test.ts +614 -0
- package/src/implementations/http/express-rpc/index.ts +180 -0
- package/src/implementations/http/express-rpc/types.ts +29 -0
- package/src/implementations/types.ts +20 -0
- package/src/schema/parser.ts +5 -4
- package/src/schema/types.ts +0 -1
- package/src/implementations/http/express/README.md +0 -351
- package/src/implementations/http/express/example/factories.ts +0 -25
- package/src/implementations/http/express/example/procedures/auth.ts +0 -24
- package/src/implementations/http/express/example/procedures/users.ts +0 -32
- package/src/implementations/http/express/example/server.test.ts +0 -133
- package/src/implementations/http/express/example/server.ts +0 -67
- package/src/implementations/http/express/index.test.ts +0 -526
- package/src/implementations/http/express/index.ts +0 -108
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Express RPC Integration
|
|
2
|
+
|
|
3
|
+
RPC-style HTTP integration for `ts-procedures` using Express. Creates POST routes at `/rpc/{name}/{version}` paths with automatic JSON schema documentation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install ts-procedures express
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { Procedures } from 'ts-procedures'
|
|
21
|
+
import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/express-rpc'
|
|
22
|
+
import { v } from 'suretype'
|
|
23
|
+
|
|
24
|
+
// Define your context type
|
|
25
|
+
type AppContext = { userId: string }
|
|
26
|
+
|
|
27
|
+
// RPC config type
|
|
28
|
+
// type RPCConfig = { name: string | string[]; version: number }
|
|
29
|
+
|
|
30
|
+
// Create a procedure factory
|
|
31
|
+
const RPC = Procedures<AppContext, RPCConfig>()
|
|
32
|
+
|
|
33
|
+
// Define procedures
|
|
34
|
+
RPC.Create(
|
|
35
|
+
'GetUser',
|
|
36
|
+
{
|
|
37
|
+
name: ['users', 'get'],
|
|
38
|
+
version: 1,
|
|
39
|
+
schema: {
|
|
40
|
+
params: v.object({ id: v.string() }),
|
|
41
|
+
returnType: v.object({ id: v.string(), name: v.string() }),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
async (ctx, params) => {
|
|
45
|
+
return { id: params.id, name: 'John Doe' }
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Build the Express app
|
|
50
|
+
const builder = new ExpressRPCAppBuilder()
|
|
51
|
+
.register(RPC, (req) => ({ userId: req.headers['x-user-id'] as string }))
|
|
52
|
+
.build()
|
|
53
|
+
|
|
54
|
+
// Start the server
|
|
55
|
+
builder.listen(3000, () => {
|
|
56
|
+
console.log('RPC server running on http://localhost:3000')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Route created: POST /rpc/users/get/1
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### `ExpressRPCAppBuilder`
|
|
65
|
+
|
|
66
|
+
Builder class for creating an Express application with RPC routes.
|
|
67
|
+
|
|
68
|
+
#### Constructor
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
new ExpressRPCAppBuilder(config?: {
|
|
72
|
+
app?: express.Express
|
|
73
|
+
onRequestStart?: (req: express.Request) => void
|
|
74
|
+
onRequestEnd?: (req: express.Request, res: express.Response) => void
|
|
75
|
+
onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void
|
|
76
|
+
error?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Option | Type | Description |
|
|
81
|
+
|--------|------|-------------|
|
|
82
|
+
| `app` | `express.Express` | Existing Express app to use. When provided, you must configure middleware (e.g., `express.json()`) yourself. If omitted, a new app with JSON middleware is created. |
|
|
83
|
+
| `onRequestStart` | `(req) => void` | Called at the start of each request. |
|
|
84
|
+
| `onRequestEnd` | `(req, res) => void` | Called after the response finishes (via `res.on('finish')`). |
|
|
85
|
+
| `onSuccess` | `(procedure, req, res) => void` | Called after successful procedure execution. |
|
|
86
|
+
| `error` | `(procedure, req, res, error) => void` | Custom error handler. When provided, you control the response. |
|
|
87
|
+
|
|
88
|
+
#### Methods
|
|
89
|
+
|
|
90
|
+
##### `register<C>(factory, contextResolver): this`
|
|
91
|
+
|
|
92
|
+
Registers a procedure factory with its context resolver.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
builder.register(
|
|
96
|
+
RPC, // Procedure factory
|
|
97
|
+
(req) => ({ userId: req.user.id }) // Context resolver
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- **factory**: The procedure factory created by `Procedures<Context, RPCConfig>()`
|
|
102
|
+
- **contextResolver**: Synchronous function `(req: express.Request) => Context`
|
|
103
|
+
- **Returns**: `this` for method chaining
|
|
104
|
+
|
|
105
|
+
##### `build(): express.Application`
|
|
106
|
+
|
|
107
|
+
Builds and returns the Express application with all registered RPC routes.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const app = builder.build()
|
|
111
|
+
app.listen(3000)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Properties
|
|
115
|
+
|
|
116
|
+
##### `app: express.Express`
|
|
117
|
+
|
|
118
|
+
The underlying Express application instance.
|
|
119
|
+
|
|
120
|
+
##### `docs: RPCHttpRouteDoc[]`
|
|
121
|
+
|
|
122
|
+
Array of route documentation objects, populated after `build()` is called.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface RPCHttpRouteDoc {
|
|
126
|
+
path: string // e.g., '/rpc/users/get/1'
|
|
127
|
+
method: 'post'
|
|
128
|
+
jsonSchema: {
|
|
129
|
+
body?: object // JSON Schema for request params
|
|
130
|
+
response?: object // JSON Schema for return type
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `makeRPCHttpRoutePath(config: RPCConfig): string`
|
|
136
|
+
|
|
137
|
+
Generates the RPC route path from config. Exposed for testing/utilities.
|
|
138
|
+
|
|
139
|
+
### `buildRpcHttpRouteDoc(procedure): RPCHttpRouteDoc`
|
|
140
|
+
|
|
141
|
+
Generates route documentation for a procedure. Exposed for testing/utilities.
|
|
142
|
+
|
|
143
|
+
## Route Path Generation
|
|
144
|
+
|
|
145
|
+
Routes are generated at `/rpc/{name-segments}/{version}`:
|
|
146
|
+
|
|
147
|
+
| Config Name | Version | Generated Path |
|
|
148
|
+
|-------------|---------|----------------|
|
|
149
|
+
| `'users'` | `1` | `/rpc/users/1` |
|
|
150
|
+
| `['users', 'get-by-id']` | `1` | `/rpc/users/get-by-id/1` |
|
|
151
|
+
| `'getUserById'` | `2` | `/rpc/get-user-by-id/2` |
|
|
152
|
+
| `'GetUserById'` | `1` | `/rpc/get-user-by-id/1` |
|
|
153
|
+
|
|
154
|
+
- Names are converted to kebab-case
|
|
155
|
+
- Array names create nested path segments
|
|
156
|
+
- Version is appended as the final segment (raw number, not `v1`)
|
|
157
|
+
|
|
158
|
+
## Multiple Factories
|
|
159
|
+
|
|
160
|
+
Register multiple factories with different contexts:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
type PublicContext = { source: 'public' }
|
|
164
|
+
type AuthContext = { source: 'auth'; userId: string }
|
|
165
|
+
|
|
166
|
+
const PublicRPC = Procedures<PublicContext, RPCConfig>()
|
|
167
|
+
const AuthRPC = Procedures<AuthContext, RPCConfig>()
|
|
168
|
+
|
|
169
|
+
// Define public procedures
|
|
170
|
+
PublicRPC.Create('HealthCheck', { name: 'health', version: 1 }, async () => ({ status: 'ok' }))
|
|
171
|
+
|
|
172
|
+
// Define authenticated procedures
|
|
173
|
+
AuthRPC.Create('GetProfile', { name: ['users', 'profile'], version: 1 }, async (ctx) => ({
|
|
174
|
+
userId: ctx.userId,
|
|
175
|
+
}))
|
|
176
|
+
|
|
177
|
+
// Build with different context resolvers
|
|
178
|
+
const app = new ExpressRPCAppBuilder()
|
|
179
|
+
.register(PublicRPC, () => ({ source: 'public' }))
|
|
180
|
+
.register(AuthRPC, (req) => ({
|
|
181
|
+
source: 'auth',
|
|
182
|
+
userId: req.headers['x-user-id'] as string,
|
|
183
|
+
}))
|
|
184
|
+
.build()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Lifecycle Hooks
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const app = new ExpressRPCAppBuilder({
|
|
191
|
+
onRequestStart: (req) => {
|
|
192
|
+
console.log(`[${req.method}] ${req.path}`)
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
onRequestEnd: (req, res) => {
|
|
196
|
+
console.log(`Response: ${res.statusCode}`)
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
onSuccess: (procedure, req, res) => {
|
|
200
|
+
console.log(`Procedure ${procedure.name} succeeded`)
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Execution order:** `onRequestStart` → handler → `onSuccess` → `onRequestEnd`
|
|
206
|
+
|
|
207
|
+
Note: `onSuccess` is only called on successful execution. It is NOT called when the handler throws.
|
|
208
|
+
|
|
209
|
+
## Error Handling
|
|
210
|
+
|
|
211
|
+
### Custom Error Handler
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const app = new ExpressRPCAppBuilder({
|
|
215
|
+
error: (procedure, req, res, error) => {
|
|
216
|
+
console.error(`Error in ${procedure.name}:`, error.message)
|
|
217
|
+
|
|
218
|
+
if (error instanceof ProcedureValidationError) {
|
|
219
|
+
res.status(400).json({ error: 'Validation failed', details: error.errors })
|
|
220
|
+
} else if (error instanceof ProcedureError) {
|
|
221
|
+
res.status(422).json({ error: error.message })
|
|
222
|
+
} else {
|
|
223
|
+
res.status(500).json({ error: 'Internal server error' })
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Default Error Handling
|
|
230
|
+
|
|
231
|
+
Without a custom error handler, errors return a JSON response with the error message:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{ "error": "Error message here" }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Route Documentation
|
|
238
|
+
|
|
239
|
+
Access generated documentation after building:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
const builder = new ExpressRPCAppBuilder()
|
|
243
|
+
builder.register(RPC, contextResolver)
|
|
244
|
+
builder.build()
|
|
245
|
+
|
|
246
|
+
// Documentation is now available
|
|
247
|
+
console.log(builder.docs)
|
|
248
|
+
// [
|
|
249
|
+
// {
|
|
250
|
+
// path: '/rpc/users/get/1',
|
|
251
|
+
// method: 'post',
|
|
252
|
+
// jsonSchema: {
|
|
253
|
+
// body: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
254
|
+
// response: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } } }
|
|
255
|
+
// }
|
|
256
|
+
// }
|
|
257
|
+
// ]
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Use `docs` to generate OpenAPI specs, API documentation, or client SDKs.
|
|
261
|
+
|
|
262
|
+
## Using an Existing Express App
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import express from 'express'
|
|
266
|
+
|
|
267
|
+
const app = express()
|
|
268
|
+
|
|
269
|
+
// Add your own middleware
|
|
270
|
+
app.use(express.json())
|
|
271
|
+
app.use(cors())
|
|
272
|
+
app.use(helmet())
|
|
273
|
+
|
|
274
|
+
// Mount RPC routes
|
|
275
|
+
const rpcApp = new ExpressRPCAppBuilder({ app })
|
|
276
|
+
.register(RPC, contextResolver)
|
|
277
|
+
.build()
|
|
278
|
+
|
|
279
|
+
// Add other routes
|
|
280
|
+
app.get('/health', (req, res) => res.json({ status: 'ok' }))
|
|
281
|
+
|
|
282
|
+
app.listen(3000)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Important:** When providing your own Express app, you must set up middleware like `express.json()` yourself.
|
|
286
|
+
|
|
287
|
+
## Types
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import type { RPCConfig, RPCHttpRouteDoc } from 'ts-procedures/express-rpc'
|
|
291
|
+
|
|
292
|
+
// RPCConfig - Required config shape for procedures
|
|
293
|
+
interface RPCConfig {
|
|
294
|
+
name: string | string[]
|
|
295
|
+
version: number
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// RPCHttpRouteDoc - Route documentation
|
|
299
|
+
interface RPCHttpRouteDoc {
|
|
300
|
+
path: string
|
|
301
|
+
method: 'post'
|
|
302
|
+
jsonSchema: {
|
|
303
|
+
body?: object
|
|
304
|
+
response?: object
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## HTTP Method
|
|
310
|
+
|
|
311
|
+
All RPC routes use **POST** method only. GET requests to RPC paths return 404.
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# Works
|
|
315
|
+
curl -X POST http://localhost:3000/rpc/users/get/1 \
|
|
316
|
+
-H "Content-Type: application/json" \
|
|
317
|
+
-d '{"id": "123"}'
|
|
318
|
+
|
|
319
|
+
# Returns 404
|
|
320
|
+
curl http://localhost:3000/rpc/users/get/1
|
|
321
|
+
```
|