ts-procedures 1.1.1 → 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.
Files changed (32) hide show
  1. package/build/implementations/http/express/index.d.ts +2 -1
  2. package/build/implementations/http/express/index.js.map +1 -1
  3. package/build/implementations/http/express/types.d.ts +17 -0
  4. package/build/implementations/http/express/types.js +2 -0
  5. package/build/implementations/http/express/types.js.map +1 -0
  6. package/build/implementations/http/express-rpc/index.d.ts +82 -0
  7. package/build/implementations/http/express-rpc/index.js +140 -0
  8. package/build/implementations/http/express-rpc/index.js.map +1 -0
  9. package/build/implementations/http/express-rpc/index.test.d.ts +1 -0
  10. package/build/implementations/http/express-rpc/index.test.js +445 -0
  11. package/build/implementations/http/express-rpc/index.test.js.map +1 -0
  12. package/build/implementations/http/express-rpc/types.d.ts +28 -0
  13. package/build/implementations/http/express-rpc/types.js +2 -0
  14. package/build/implementations/http/express-rpc/types.js.map +1 -0
  15. package/build/implementations/types.d.ts +17 -0
  16. package/build/implementations/types.js +2 -0
  17. package/build/implementations/types.js.map +1 -0
  18. package/package.json +13 -8
  19. package/src/implementations/http/express-rpc/README.md +321 -0
  20. package/src/implementations/http/express-rpc/index.test.ts +614 -0
  21. package/src/implementations/http/express-rpc/index.ts +180 -0
  22. package/src/implementations/http/express-rpc/types.ts +29 -0
  23. package/src/implementations/types.ts +20 -0
  24. package/src/implementations/http/client/index.ts +0 -0
  25. package/src/implementations/http/express/README.md +0 -351
  26. package/src/implementations/http/express/example/factories.ts +0 -25
  27. package/src/implementations/http/express/example/procedures/auth.ts +0 -24
  28. package/src/implementations/http/express/example/procedures/users.ts +0 -32
  29. package/src/implementations/http/express/example/server.test.ts +0 -133
  30. package/src/implementations/http/express/example/server.ts +0 -67
  31. package/src/implementations/http/express/index.test.ts +0 -526
  32. 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
+ ```