ts-procedures 2.0.1 → 2.1.1
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/build/implementations/http/express-rpc/index.d.ts +40 -39
- package/build/implementations/http/express-rpc/index.js +40 -20
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +138 -58
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/build/implementations/http/express-rpc/types.d.ts +1 -1
- package/build/implementations/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/implementations/http/express-rpc/README.md +49 -25
- package/src/implementations/http/express-rpc/index.test.ts +170 -58
- package/src/implementations/http/express-rpc/index.ts +74 -45
- package/src/implementations/http/express-rpc/types.ts +6 -2
- package/src/implementations/types.ts +1 -1
- package/build/implementations/http/client/index.d.ts +0 -1
- package/build/implementations/http/client/index.js +0 -2
- package/build/implementations/http/client/index.js.map +0 -1
- package/build/implementations/http/express/example/factories.d.ts +0 -97
- package/build/implementations/http/express/example/factories.js +0 -4
- package/build/implementations/http/express/example/factories.js.map +0 -1
- package/build/implementations/http/express/example/procedures/auth.d.ts +0 -1
- package/build/implementations/http/express/example/procedures/auth.js +0 -22
- package/build/implementations/http/express/example/procedures/auth.js.map +0 -1
- package/build/implementations/http/express/example/procedures/users.d.ts +0 -1
- package/build/implementations/http/express/example/procedures/users.js +0 -30
- package/build/implementations/http/express/example/procedures/users.js.map +0 -1
- package/build/implementations/http/express/example/server.d.ts +0 -3
- package/build/implementations/http/express/example/server.js +0 -49
- package/build/implementations/http/express/example/server.js.map +0 -1
- package/build/implementations/http/express/example/server.test.d.ts +0 -1
- package/build/implementations/http/express/example/server.test.js +0 -110
- package/build/implementations/http/express/example/server.test.js.map +0 -1
- package/build/implementations/http/express/index.d.ts +0 -35
- package/build/implementations/http/express/index.js +0 -75
- package/build/implementations/http/express/index.js.map +0 -1
- package/build/implementations/http/express/index.test.d.ts +0 -1
- package/build/implementations/http/express/index.test.js +0 -329
- package/build/implementations/http/express/index.test.js.map +0 -1
- package/build/implementations/http/express/types.d.ts +0 -17
- package/build/implementations/http/express/types.js +0 -2
- package/build/implementations/http/express/types.js.map +0 -1
|
@@ -10,7 +10,7 @@ import { RPCConfig } from '../../types.js'
|
|
|
10
10
|
* ExpressRPCAppBuilder Test Suite
|
|
11
11
|
*
|
|
12
12
|
* Tests the RPC-style Express integration for ts-procedures.
|
|
13
|
-
* This builder creates POST routes at `/
|
|
13
|
+
* This builder creates POST routes at `/{name}/{version}` paths (with optional pathPrefix).
|
|
14
14
|
*/
|
|
15
15
|
describe('ExpressRPCAppBuilder', () => {
|
|
16
16
|
// --------------------------------------------------------------------------
|
|
@@ -31,7 +31,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
31
31
|
const app = builder.build()
|
|
32
32
|
|
|
33
33
|
// JSON body should be parsed automatically
|
|
34
|
-
const res = await request(app).post('/
|
|
34
|
+
const res = await request(app).post('/echo/1').send({ message: 'hello' })
|
|
35
35
|
|
|
36
36
|
expect(res.status).toBe(200)
|
|
37
37
|
expect(res.body).toEqual({ message: 'hello' })
|
|
@@ -54,7 +54,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
54
54
|
|
|
55
55
|
// Without json middleware, body won't be parsed (req.body is undefined)
|
|
56
56
|
const res = await request(app)
|
|
57
|
-
.post('/
|
|
57
|
+
.post('/echo/1')
|
|
58
58
|
.set('Content-Type', 'application/json')
|
|
59
59
|
.send(JSON.stringify({ message: 'hello' }))
|
|
60
60
|
|
|
@@ -76,6 +76,77 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
76
76
|
})
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
+
// --------------------------------------------------------------------------
|
|
80
|
+
// pathPrefix Option Tests
|
|
81
|
+
// --------------------------------------------------------------------------
|
|
82
|
+
describe('pathPrefix option', () => {
|
|
83
|
+
test('uses custom pathPrefix for all routes', async () => {
|
|
84
|
+
const builder = new ExpressRPCAppBuilder({ pathPrefix: '/api/v1' })
|
|
85
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
86
|
+
|
|
87
|
+
RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }))
|
|
88
|
+
|
|
89
|
+
builder.register(RPC, () => ({}))
|
|
90
|
+
const app = builder.build()
|
|
91
|
+
|
|
92
|
+
const res = await request(app).post('/api/v1/test/1').send({})
|
|
93
|
+
expect(res.status).toBe(200)
|
|
94
|
+
expect(res.body).toEqual({ ok: true })
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('pathPrefix without leading slash gets normalized', async () => {
|
|
98
|
+
const builder = new ExpressRPCAppBuilder({ pathPrefix: 'custom' })
|
|
99
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
100
|
+
|
|
101
|
+
RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }))
|
|
102
|
+
|
|
103
|
+
builder.register(RPC, () => ({}))
|
|
104
|
+
const app = builder.build()
|
|
105
|
+
|
|
106
|
+
const res = await request(app).post('/custom/test/1').send({})
|
|
107
|
+
expect(res.status).toBe(200)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('no prefix when pathPrefix not specified', async () => {
|
|
111
|
+
const builder = new ExpressRPCAppBuilder()
|
|
112
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
113
|
+
|
|
114
|
+
RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }))
|
|
115
|
+
|
|
116
|
+
builder.register(RPC, () => ({}))
|
|
117
|
+
const app = builder.build()
|
|
118
|
+
|
|
119
|
+
const res = await request(app).post('/test/1').send({})
|
|
120
|
+
expect(res.status).toBe(200)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('pathPrefix appears in generated docs', () => {
|
|
124
|
+
const builder = new ExpressRPCAppBuilder({ pathPrefix: '/api' })
|
|
125
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
126
|
+
|
|
127
|
+
RPC.Create('Test', { name: 'test', version: 1 }, async () => ({}))
|
|
128
|
+
|
|
129
|
+
builder.register(RPC, () => ({}))
|
|
130
|
+
builder.build()
|
|
131
|
+
|
|
132
|
+
expect(builder.docs[0]!.path).toBe('/api/test/1')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('pathPrefix /rpc restores original behavior', async () => {
|
|
136
|
+
const builder = new ExpressRPCAppBuilder({ pathPrefix: '/rpc' })
|
|
137
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
138
|
+
|
|
139
|
+
RPC.Create('Users', { name: 'users', version: 1 }, async () => ({ users: [] }))
|
|
140
|
+
|
|
141
|
+
builder.register(RPC, () => ({}))
|
|
142
|
+
const app = builder.build()
|
|
143
|
+
|
|
144
|
+
const res = await request(app).post('/rpc/users/1').send({})
|
|
145
|
+
expect(res.status).toBe(200)
|
|
146
|
+
expect(builder.docs[0]!.path).toBe('/rpc/users/1')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
79
150
|
// --------------------------------------------------------------------------
|
|
80
151
|
// Lifecycle Hooks Tests
|
|
81
152
|
// --------------------------------------------------------------------------
|
|
@@ -90,11 +161,11 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
90
161
|
builder.register(RPC, () => ({}))
|
|
91
162
|
const app = builder.build()
|
|
92
163
|
|
|
93
|
-
await request(app).post('/
|
|
164
|
+
await request(app).post('/test/1').send({})
|
|
94
165
|
|
|
95
166
|
expect(onRequestStart).toHaveBeenCalledTimes(1)
|
|
96
167
|
expect(onRequestStart.mock.calls[0]![0]).toHaveProperty('method', 'POST')
|
|
97
|
-
expect(onRequestStart.mock.calls[0]![0]).toHaveProperty('path', '/
|
|
168
|
+
expect(onRequestStart.mock.calls[0]![0]).toHaveProperty('path', '/test/1')
|
|
98
169
|
})
|
|
99
170
|
|
|
100
171
|
test('onRequestEnd is called after response finishes', async () => {
|
|
@@ -107,7 +178,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
107
178
|
builder.register(RPC, () => ({}))
|
|
108
179
|
const app = builder.build()
|
|
109
180
|
|
|
110
|
-
await request(app).post('/
|
|
181
|
+
await request(app).post('/test/1').send({})
|
|
111
182
|
|
|
112
183
|
expect(onRequestEnd).toHaveBeenCalledTimes(1)
|
|
113
184
|
expect(onRequestEnd.mock.calls[0]![0]).toHaveProperty('method', 'POST')
|
|
@@ -124,7 +195,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
124
195
|
builder.register(RPC, () => ({}))
|
|
125
196
|
const app = builder.build()
|
|
126
197
|
|
|
127
|
-
await request(app).post('/
|
|
198
|
+
await request(app).post('/test/1').send({})
|
|
128
199
|
|
|
129
200
|
expect(onSuccess).toHaveBeenCalledTimes(1)
|
|
130
201
|
expect(onSuccess.mock.calls[0]![0]).toHaveProperty('name', 'Test')
|
|
@@ -142,7 +213,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
142
213
|
builder.register(RPC, () => ({}))
|
|
143
214
|
const app = builder.build()
|
|
144
215
|
|
|
145
|
-
await request(app).post('/
|
|
216
|
+
await request(app).post('/test/1').send({})
|
|
146
217
|
|
|
147
218
|
expect(onSuccess).not.toHaveBeenCalled()
|
|
148
219
|
})
|
|
@@ -165,7 +236,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
165
236
|
builder.register(RPC, () => ({}))
|
|
166
237
|
const app = builder.build()
|
|
167
238
|
|
|
168
|
-
await request(app).post('/
|
|
239
|
+
await request(app).post('/test/1').send({})
|
|
169
240
|
|
|
170
241
|
expect(order).toEqual(['start', 'handler', 'success', 'end'])
|
|
171
242
|
})
|
|
@@ -190,7 +261,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
190
261
|
builder.register(RPC, () => ({}))
|
|
191
262
|
const app = builder.build()
|
|
192
263
|
|
|
193
|
-
const res = await request(app).post('/
|
|
264
|
+
const res = await request(app).post('/test/1').send({})
|
|
194
265
|
|
|
195
266
|
expect(errorHandler).toHaveBeenCalledTimes(1)
|
|
196
267
|
expect(errorHandler.mock.calls[0]![0]).toHaveProperty('name', 'Test')
|
|
@@ -211,7 +282,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
211
282
|
builder.register(RPC, () => ({}))
|
|
212
283
|
const app = builder.build()
|
|
213
284
|
|
|
214
|
-
const res = await request(app).post('/
|
|
285
|
+
const res = await request(app).post('/test/1').send({})
|
|
215
286
|
|
|
216
287
|
// Default error handler returns error message in JSON body
|
|
217
288
|
expect(res.body).toHaveProperty('error')
|
|
@@ -231,7 +302,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
231
302
|
builder.register(RPC, () => ({}))
|
|
232
303
|
const app = builder.build()
|
|
233
304
|
|
|
234
|
-
const res = await request(app).post('/
|
|
305
|
+
const res = await request(app).post('/test/1').send({})
|
|
235
306
|
|
|
236
307
|
// Unhandled exceptions are caught and returned as error response
|
|
237
308
|
expect(res.body).toHaveProperty('error')
|
|
@@ -276,15 +347,53 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
276
347
|
|
|
277
348
|
const app = builder.build()
|
|
278
349
|
|
|
279
|
-
const publicRes = await request(app).post('/
|
|
280
|
-
const privateRes = await request(app).post('/
|
|
350
|
+
const publicRes = await request(app).post('/public/1').send({})
|
|
351
|
+
const privateRes = await request(app).post('/private/1').send({})
|
|
281
352
|
|
|
282
353
|
expect(publicRes.body).toEqual({ isPublic: true })
|
|
283
354
|
expect(privateRes.body).toEqual({ isPrivate: true })
|
|
284
355
|
})
|
|
285
356
|
|
|
286
|
-
test('context
|
|
287
|
-
const
|
|
357
|
+
test('context can be a static object', async () => {
|
|
358
|
+
const factoryContext = { requestId: 'req-123' }
|
|
359
|
+
|
|
360
|
+
const builder = new ExpressRPCAppBuilder()
|
|
361
|
+
const RPC = Procedures<{ requestId: string }, RPCConfig>()
|
|
362
|
+
|
|
363
|
+
RPC.Create('GetRequestId', { name: 'get-request-id', version: 1 }, async (ctx) => ({
|
|
364
|
+
id: ctx.requestId,
|
|
365
|
+
}))
|
|
366
|
+
|
|
367
|
+
builder.register(RPC, factoryContext)
|
|
368
|
+
const app = builder.build()
|
|
369
|
+
|
|
370
|
+
const res = await request(app).post('/get-request-id/1').send({})
|
|
371
|
+
|
|
372
|
+
expect(res.body).toEqual({ id: 'req-123' })
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('factoryContext can be async function', async () => {
|
|
376
|
+
const factoryContext = vi.fn(async () => {
|
|
377
|
+
return { requestId: 'req-456' }
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
const builder = new ExpressRPCAppBuilder()
|
|
381
|
+
const RPC = Procedures<{ requestId: string }, RPCConfig>()
|
|
382
|
+
|
|
383
|
+
RPC.Create('GetRequestId', { name: 'get-request-id', version: 1 }, async (ctx) => ({
|
|
384
|
+
id: ctx.requestId,
|
|
385
|
+
}))
|
|
386
|
+
|
|
387
|
+
builder.register(RPC, factoryContext)
|
|
388
|
+
const app = builder.build()
|
|
389
|
+
|
|
390
|
+
await request(app).post('/get-request-id/1').send({})
|
|
391
|
+
|
|
392
|
+
expect(factoryContext).toHaveBeenCalledTimes(1)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
test('factoryContext function receives Express request object', async () => {
|
|
396
|
+
const factoryContext = vi.fn((req) => ({
|
|
288
397
|
authHeader: req.headers.authorization,
|
|
289
398
|
}))
|
|
290
399
|
|
|
@@ -295,14 +404,14 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
295
404
|
auth: ctx.authHeader,
|
|
296
405
|
}))
|
|
297
406
|
|
|
298
|
-
builder.register(RPC,
|
|
407
|
+
builder.register(RPC, factoryContext)
|
|
299
408
|
const app = builder.build()
|
|
300
409
|
|
|
301
|
-
await request(app).post('/
|
|
410
|
+
await request(app).post('/get-auth/1').set('Authorization', 'Bearer token123').send({})
|
|
302
411
|
|
|
303
|
-
expect(
|
|
304
|
-
expect(
|
|
305
|
-
expect(
|
|
412
|
+
expect(factoryContext).toHaveBeenCalledTimes(1)
|
|
413
|
+
expect(factoryContext.mock.calls[0]![0]).toHaveProperty('headers')
|
|
414
|
+
expect(factoryContext.mock.calls[0]![0].headers).toHaveProperty(
|
|
306
415
|
'authorization',
|
|
307
416
|
'Bearer token123'
|
|
308
417
|
)
|
|
@@ -323,8 +432,8 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
323
432
|
builder.register(RPC, () => ({}))
|
|
324
433
|
const app = builder.build()
|
|
325
434
|
|
|
326
|
-
const res1 = await request(app).post('/
|
|
327
|
-
const res2 = await request(app).post('/
|
|
435
|
+
const res1 = await request(app).post('/method-one/1').send({})
|
|
436
|
+
const res2 = await request(app).post('/method-two/2').send({})
|
|
328
437
|
|
|
329
438
|
expect(res1.status).toBe(200)
|
|
330
439
|
expect(res2.status).toBe(200)
|
|
@@ -353,8 +462,8 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
353
462
|
builder.build()
|
|
354
463
|
|
|
355
464
|
expect(builder.docs).toHaveLength(2)
|
|
356
|
-
expect(builder.docs[0]!.path).toBe('/
|
|
357
|
-
expect(builder.docs[1]!.path).toBe('/
|
|
465
|
+
expect(builder.docs[0]!.path).toBe('/method-one/1')
|
|
466
|
+
expect(builder.docs[1]!.path).toBe('/nested/method/2')
|
|
358
467
|
})
|
|
359
468
|
|
|
360
469
|
test('passes request body to handler as params', async () => {
|
|
@@ -370,7 +479,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
370
479
|
builder.register(RPC, () => ({}))
|
|
371
480
|
const app = builder.build()
|
|
372
481
|
|
|
373
|
-
const res = await request(app).post('/
|
|
482
|
+
const res = await request(app).post('/echo/1').send({ data: 'test-data' })
|
|
374
483
|
|
|
375
484
|
expect(res.body).toEqual({ received: 'test-data' })
|
|
376
485
|
})
|
|
@@ -384,7 +493,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
384
493
|
builder.register(RPC, () => ({}))
|
|
385
494
|
const app = builder.build()
|
|
386
495
|
|
|
387
|
-
const res = await request(app).get('/
|
|
496
|
+
const res = await request(app).get('/test/1')
|
|
388
497
|
|
|
389
498
|
expect(res.status).toBe(404)
|
|
390
499
|
})
|
|
@@ -400,24 +509,24 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
400
509
|
builder = new ExpressRPCAppBuilder()
|
|
401
510
|
})
|
|
402
511
|
|
|
403
|
-
test("simple string: 'users' → /
|
|
512
|
+
test("simple string: 'users' → /users/1", () => {
|
|
404
513
|
const path = builder.makeRPCHttpRoutePath({ name: 'users', version: 1 })
|
|
405
|
-
expect(path).toBe('/
|
|
514
|
+
expect(path).toBe('/users/1')
|
|
406
515
|
})
|
|
407
516
|
|
|
408
|
-
test("array name: ['users', 'get-by-id'] → /
|
|
517
|
+
test("array name: ['users', 'get-by-id'] → /users/get-by-id/1", () => {
|
|
409
518
|
const path = builder.makeRPCHttpRoutePath({ name: ['users', 'get-by-id'], version: 1 })
|
|
410
|
-
expect(path).toBe('/
|
|
519
|
+
expect(path).toBe('/users/get-by-id/1')
|
|
411
520
|
})
|
|
412
521
|
|
|
413
|
-
test("camelCase: 'getUserById' → /
|
|
522
|
+
test("camelCase: 'getUserById' → /get-user-by-id/1", () => {
|
|
414
523
|
const path = builder.makeRPCHttpRoutePath({ name: 'getUserById', version: 1 })
|
|
415
|
-
expect(path).toBe('/
|
|
524
|
+
expect(path).toBe('/get-user-by-id/1')
|
|
416
525
|
})
|
|
417
526
|
|
|
418
|
-
test("PascalCase: 'GetUserById' → /
|
|
527
|
+
test("PascalCase: 'GetUserById' → /get-user-by-id/1", () => {
|
|
419
528
|
const path = builder.makeRPCHttpRoutePath({ name: 'GetUserById', version: 1 })
|
|
420
|
-
expect(path).toBe('/
|
|
529
|
+
expect(path).toBe('/get-user-by-id/1')
|
|
421
530
|
})
|
|
422
531
|
|
|
423
532
|
test('version number included in path', () => {
|
|
@@ -425,9 +534,9 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
425
534
|
const pathV2 = builder.makeRPCHttpRoutePath({ name: 'test', version: 2 })
|
|
426
535
|
const pathV99 = builder.makeRPCHttpRoutePath({ name: 'test', version: 99 })
|
|
427
536
|
|
|
428
|
-
expect(pathV1).toBe('/
|
|
429
|
-
expect(pathV2).toBe('/
|
|
430
|
-
expect(pathV99).toBe('/
|
|
537
|
+
expect(pathV1).toBe('/test/1')
|
|
538
|
+
expect(pathV2).toBe('/test/2')
|
|
539
|
+
expect(pathV99).toBe('/test/99')
|
|
431
540
|
})
|
|
432
541
|
|
|
433
542
|
test('handles mixed case in array segments', () => {
|
|
@@ -435,7 +544,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
435
544
|
name: ['UserModule', 'getActiveUsers'],
|
|
436
545
|
version: 1,
|
|
437
546
|
})
|
|
438
|
-
expect(path).toBe('/
|
|
547
|
+
expect(path).toBe('/user-module/get-active-users/1')
|
|
439
548
|
})
|
|
440
549
|
})
|
|
441
550
|
|
|
@@ -454,16 +563,17 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
454
563
|
const returnSchema = v.object({ name: v.string() })
|
|
455
564
|
|
|
456
565
|
const RPC = Procedures<{}, RPCConfig>()
|
|
457
|
-
|
|
566
|
+
RPC.Create(
|
|
458
567
|
'GetUser',
|
|
459
568
|
{ name: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } },
|
|
460
569
|
async () => ({ name: 'test' })
|
|
461
570
|
)
|
|
462
571
|
|
|
463
|
-
|
|
464
|
-
|
|
572
|
+
builder.register(RPC, () => ({}))
|
|
573
|
+
builder.build()
|
|
465
574
|
|
|
466
|
-
|
|
575
|
+
const doc = builder.docs[0]!
|
|
576
|
+
expect(doc.path).toBe('/users/1')
|
|
467
577
|
expect(doc.method).toBe('post')
|
|
468
578
|
expect(doc.jsonSchema.body).toBeDefined()
|
|
469
579
|
expect(doc.jsonSchema.response).toBeDefined()
|
|
@@ -473,9 +583,10 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
473
583
|
const RPC = Procedures<{}, RPCConfig>()
|
|
474
584
|
RPC.Create('NoParams', { name: 'no-params', version: 1 }, async () => ({ ok: true }))
|
|
475
585
|
|
|
476
|
-
|
|
477
|
-
|
|
586
|
+
builder.register(RPC, () => ({}))
|
|
587
|
+
builder.build()
|
|
478
588
|
|
|
589
|
+
const doc = builder.docs[0]!
|
|
479
590
|
expect(doc.jsonSchema.body).toBeUndefined()
|
|
480
591
|
})
|
|
481
592
|
|
|
@@ -487,9 +598,10 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
487
598
|
async () => ({})
|
|
488
599
|
)
|
|
489
600
|
|
|
490
|
-
|
|
491
|
-
|
|
601
|
+
builder.register(RPC, () => ({}))
|
|
602
|
+
builder.build()
|
|
492
603
|
|
|
604
|
+
const doc = builder.docs[0]!
|
|
493
605
|
expect(doc.jsonSchema.body).toBeDefined()
|
|
494
606
|
expect(doc.jsonSchema.response).toBeUndefined()
|
|
495
607
|
})
|
|
@@ -499,10 +611,10 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
499
611
|
RPC.Create('Test1', { name: 't1', version: 1 }, async () => ({}))
|
|
500
612
|
RPC.Create('Test2', { name: 't2', version: 2 }, async () => ({}))
|
|
501
613
|
|
|
502
|
-
|
|
614
|
+
builder.register(RPC, () => ({}))
|
|
615
|
+
builder.build()
|
|
503
616
|
|
|
504
|
-
|
|
505
|
-
const doc = builder.buildRpcHttpRouteDoc(proc)
|
|
617
|
+
builder.docs.forEach((doc) => {
|
|
506
618
|
expect(doc.method).toBe('post')
|
|
507
619
|
})
|
|
508
620
|
})
|
|
@@ -570,24 +682,24 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
570
682
|
const app = builder.build()
|
|
571
683
|
|
|
572
684
|
// Test public endpoints
|
|
573
|
-
const versionRes = await request(app).post('/
|
|
685
|
+
const versionRes = await request(app).post('/system/version/1').send({})
|
|
574
686
|
expect(versionRes.status).toBe(200)
|
|
575
687
|
expect(versionRes.body).toEqual({ version: '1.0.0' })
|
|
576
688
|
|
|
577
|
-
const healthRes = await request(app).post('/
|
|
689
|
+
const healthRes = await request(app).post('/health/1').send({})
|
|
578
690
|
expect(healthRes.status).toBe(200)
|
|
579
691
|
expect(healthRes.body).toEqual({ status: 'ok' })
|
|
580
692
|
|
|
581
693
|
// Test authenticated endpoints
|
|
582
694
|
const profileRes = await request(app)
|
|
583
|
-
.post('/
|
|
695
|
+
.post('/users/profile/1')
|
|
584
696
|
.set('X-User-Id', 'user-123')
|
|
585
697
|
.send({})
|
|
586
698
|
expect(profileRes.status).toBe(200)
|
|
587
699
|
expect(profileRes.body).toEqual({ userId: 'user-123', source: 'auth' })
|
|
588
700
|
|
|
589
701
|
const updateRes = await request(app)
|
|
590
|
-
.post('/
|
|
702
|
+
.post('/users/profile/2')
|
|
591
703
|
.set('X-User-Id', 'user-456')
|
|
592
704
|
.send({ name: 'John Doe' })
|
|
593
705
|
expect(updateRes.status).toBe(200)
|
|
@@ -597,10 +709,10 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
597
709
|
expect(builder.docs).toHaveLength(4)
|
|
598
710
|
|
|
599
711
|
const paths = builder.docs.map((d) => d.path)
|
|
600
|
-
expect(paths).toContain('/
|
|
601
|
-
expect(paths).toContain('/
|
|
602
|
-
expect(paths).toContain('/
|
|
603
|
-
expect(paths).toContain('/
|
|
712
|
+
expect(paths).toContain('/system/version/1')
|
|
713
|
+
expect(paths).toContain('/health/1')
|
|
714
|
+
expect(paths).toContain('/users/profile/1')
|
|
715
|
+
expect(paths).toContain('/users/profile/2')
|
|
604
716
|
|
|
605
717
|
// Verify hooks were called
|
|
606
718
|
expect(events).toContain('request-start')
|
|
@@ -6,6 +6,31 @@ import { castArray } from 'es-toolkit/compat'
|
|
|
6
6
|
import { ExpressFactoryItem, ExtractContext, ProceduresFactory } from './types.js'
|
|
7
7
|
|
|
8
8
|
export type { RPCConfig, RPCHttpRouteDoc }
|
|
9
|
+
|
|
10
|
+
export type ExpressRPCAppBuilderConfig = {
|
|
11
|
+
/**
|
|
12
|
+
* An existing Express application instance to use.
|
|
13
|
+
* When provided, ensure to set up necessary middleware (e.g., json/body parser) beforehand.
|
|
14
|
+
* If not provided, a new instance will be created.
|
|
15
|
+
*/
|
|
16
|
+
app?: express.Express
|
|
17
|
+
/** Optional path prefix for all RPC routes. */
|
|
18
|
+
pathPrefix?: string
|
|
19
|
+
onRequestStart?: (req: express.Request) => void
|
|
20
|
+
onRequestEnd?: (req: express.Request, res: express.Response) => void
|
|
21
|
+
onSuccess?: (
|
|
22
|
+
procedure: TProcedureRegistration,
|
|
23
|
+
req: express.Request,
|
|
24
|
+
res: express.Response
|
|
25
|
+
) => void
|
|
26
|
+
error?: (
|
|
27
|
+
procedure: TProcedureRegistration,
|
|
28
|
+
req: express.Request,
|
|
29
|
+
res: express.Response,
|
|
30
|
+
error: Error
|
|
31
|
+
) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
9
34
|
/**
|
|
10
35
|
* Builder class for creating an Express application with RPC routes.
|
|
11
36
|
*
|
|
@@ -27,29 +52,7 @@ export class ExpressRPCAppBuilder {
|
|
|
27
52
|
*
|
|
28
53
|
* @param config
|
|
29
54
|
*/
|
|
30
|
-
constructor(
|
|
31
|
-
readonly config?: {
|
|
32
|
-
/**
|
|
33
|
-
* An existing Express application instance to use.
|
|
34
|
-
* When provided, ensure to set up necessary middleware (e.g., json/body parser) beforehand.
|
|
35
|
-
* If not provided, a new instance will be created.
|
|
36
|
-
*/
|
|
37
|
-
app?: express.Express
|
|
38
|
-
onRequestStart?: (req: express.Request) => void
|
|
39
|
-
onRequestEnd?: (req: express.Request, res: express.Response) => void
|
|
40
|
-
onSuccess?: (
|
|
41
|
-
procedure: TProcedureRegistration,
|
|
42
|
-
req: express.Request,
|
|
43
|
-
res: express.Response
|
|
44
|
-
) => void
|
|
45
|
-
error?: (
|
|
46
|
-
procedure: TProcedureRegistration,
|
|
47
|
-
req: express.Request,
|
|
48
|
-
res: express.Response,
|
|
49
|
-
error: Error
|
|
50
|
-
) => void
|
|
51
|
-
}
|
|
52
|
-
) {
|
|
55
|
+
constructor(readonly config?: ExpressRPCAppBuilderConfig) {
|
|
53
56
|
if (config?.app) {
|
|
54
57
|
this._app = config.app
|
|
55
58
|
} else {
|
|
@@ -74,6 +77,34 @@ export class ExpressRPCAppBuilder {
|
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Generates the RPC route path based on the RPC configuration.
|
|
82
|
+
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
83
|
+
*
|
|
84
|
+
* Example
|
|
85
|
+
* name: ['string', 'string-string', 'string']
|
|
86
|
+
* path: /string/string-string/string/version
|
|
87
|
+
* @param config
|
|
88
|
+
*/
|
|
89
|
+
static makeRPCHttpRoutePath({ config, prefix }: { prefix?: string; config: RPCConfig }) {
|
|
90
|
+
const normalizedPrefix = prefix
|
|
91
|
+
? (prefix.startsWith('/') ? prefix : `/${prefix}`)
|
|
92
|
+
: ''
|
|
93
|
+
|
|
94
|
+
return `${normalizedPrefix}/${castArray(config.name).map(kebabCase).join('/')}/${String(config.version).trim()}`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Instance method wrapper for makeRPCHttpRoutePath that uses the builder's pathPrefix.
|
|
99
|
+
* @param config - The RPC configuration
|
|
100
|
+
*/
|
|
101
|
+
makeRPCHttpRoutePath(config: RPCConfig): string {
|
|
102
|
+
return ExpressRPCAppBuilder.makeRPCHttpRoutePath({
|
|
103
|
+
config,
|
|
104
|
+
prefix: this.config?.pathPrefix,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
77
108
|
private factories: ExpressFactoryItem<any>[] = []
|
|
78
109
|
|
|
79
110
|
private _app: express.Express = express()
|
|
@@ -88,15 +119,18 @@ export class ExpressRPCAppBuilder {
|
|
|
88
119
|
}
|
|
89
120
|
|
|
90
121
|
/**
|
|
91
|
-
* Registers a procedure factory with its context
|
|
92
|
-
* @param factory
|
|
93
|
-
* @param
|
|
122
|
+
* Registers a procedure factory with its context.
|
|
123
|
+
* @param factory - The procedure factory created by Procedures<Context, RPCConfig>()
|
|
124
|
+
* @param factoryContext - The context for procedure handlers. Can be a direct value,
|
|
125
|
+
* a sync function (req) => Context, or an async function (req) => Promise<Context>
|
|
94
126
|
*/
|
|
95
127
|
register<TFactory extends ProceduresFactory>(
|
|
96
128
|
factory: TFactory,
|
|
97
|
-
|
|
129
|
+
factoryContext:
|
|
130
|
+
| ExtractContext<TFactory>
|
|
131
|
+
| ((req: express.Request) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
|
|
98
132
|
): this {
|
|
99
|
-
this.factories.push({ factory,
|
|
133
|
+
this.factories.push({ factory, factoryContext } as ExpressFactoryItem<any>)
|
|
100
134
|
return this
|
|
101
135
|
}
|
|
102
136
|
|
|
@@ -105,7 +139,7 @@ export class ExpressRPCAppBuilder {
|
|
|
105
139
|
* @return express.Application
|
|
106
140
|
*/
|
|
107
141
|
build(): express.Application {
|
|
108
|
-
this.factories.forEach(({ factory,
|
|
142
|
+
this.factories.forEach(({ factory, factoryContext }) => {
|
|
109
143
|
factory.getProcedures().map((procedure: TProcedureRegistration<any, RPCConfig>) => {
|
|
110
144
|
const route = this.buildRpcHttpRouteDoc(procedure)
|
|
111
145
|
|
|
@@ -113,7 +147,12 @@ export class ExpressRPCAppBuilder {
|
|
|
113
147
|
|
|
114
148
|
this._app[route.method](route.path, async (req, res) => {
|
|
115
149
|
try {
|
|
116
|
-
|
|
150
|
+
const context =
|
|
151
|
+
typeof factoryContext === 'function'
|
|
152
|
+
? await factoryContext(req)
|
|
153
|
+
: (factoryContext as ExtractContext<typeof factory>)
|
|
154
|
+
|
|
155
|
+
res.json(await procedure.handler(context, req.body))
|
|
117
156
|
if (this.config?.onSuccess) {
|
|
118
157
|
this.config.onSuccess(procedure, req, res)
|
|
119
158
|
}
|
|
@@ -145,9 +184,12 @@ export class ExpressRPCAppBuilder {
|
|
|
145
184
|
* Generates the RPC HTTP route for the given procedure.
|
|
146
185
|
* @param procedure
|
|
147
186
|
*/
|
|
148
|
-
buildRpcHttpRouteDoc(procedure: TProcedureRegistration<any, RPCConfig>): RPCHttpRouteDoc {
|
|
187
|
+
private buildRpcHttpRouteDoc(procedure: TProcedureRegistration<any, RPCConfig>): RPCHttpRouteDoc {
|
|
149
188
|
const { config } = procedure
|
|
150
|
-
const path =
|
|
189
|
+
const path = ExpressRPCAppBuilder.makeRPCHttpRoutePath({
|
|
190
|
+
config,
|
|
191
|
+
prefix: this.config?.pathPrefix,
|
|
192
|
+
})
|
|
151
193
|
const method = 'post' // RPCs use POST method
|
|
152
194
|
const jsonSchema: { body?: object; response?: object } = {}
|
|
153
195
|
|
|
@@ -164,17 +206,4 @@ export class ExpressRPCAppBuilder {
|
|
|
164
206
|
jsonSchema,
|
|
165
207
|
}
|
|
166
208
|
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Generates the RPC route path based on the RPC configuration.
|
|
170
|
-
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
171
|
-
*
|
|
172
|
-
* Example
|
|
173
|
-
* name: ['string', 'string-string', 'string']
|
|
174
|
-
* path: /rpc/string/string-string/string/version
|
|
175
|
-
* @param config
|
|
176
|
-
*/
|
|
177
|
-
makeRPCHttpRoutePath(config: RPCConfig) {
|
|
178
|
-
return `/rpc/${castArray(config.name).map(kebabCase).join('/')}/${String(config.version).trim()}`
|
|
179
|
-
}
|
|
180
209
|
}
|
|
@@ -8,7 +8,9 @@ import express from 'express'
|
|
|
8
8
|
*/
|
|
9
9
|
export type ExtractContext<TFactory> = TFactory extends {
|
|
10
10
|
getProcedures: () => Array<{ handler: (ctx: infer TContext, ...args: any[]) => any }>
|
|
11
|
-
}
|
|
11
|
+
}
|
|
12
|
+
? TContext
|
|
13
|
+
: never
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Minimal structural type for a Procedures factory.
|
|
@@ -25,5 +27,7 @@ export type ProceduresFactory = {
|
|
|
25
27
|
|
|
26
28
|
export type ExpressFactoryItem<TFactory = ReturnType<typeof Procedures<any, RPCConfig>>> = {
|
|
27
29
|
factory: TFactory
|
|
28
|
-
|
|
30
|
+
factoryContext:
|
|
31
|
+
| ExtractContext<TFactory>
|
|
32
|
+
| ((req: express.Request) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
|
|
29
33
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/implementations/http/client/index.ts"],"names":[],"mappings":""}
|