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.
Files changed (40) hide show
  1. package/build/implementations/http/express-rpc/index.d.ts +40 -39
  2. package/build/implementations/http/express-rpc/index.js +40 -20
  3. package/build/implementations/http/express-rpc/index.js.map +1 -1
  4. package/build/implementations/http/express-rpc/index.test.js +138 -58
  5. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  6. package/build/implementations/http/express-rpc/types.d.ts +1 -1
  7. package/build/implementations/types.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/src/implementations/http/express-rpc/README.md +49 -25
  10. package/src/implementations/http/express-rpc/index.test.ts +170 -58
  11. package/src/implementations/http/express-rpc/index.ts +74 -45
  12. package/src/implementations/http/express-rpc/types.ts +6 -2
  13. package/src/implementations/types.ts +1 -1
  14. package/build/implementations/http/client/index.d.ts +0 -1
  15. package/build/implementations/http/client/index.js +0 -2
  16. package/build/implementations/http/client/index.js.map +0 -1
  17. package/build/implementations/http/express/example/factories.d.ts +0 -97
  18. package/build/implementations/http/express/example/factories.js +0 -4
  19. package/build/implementations/http/express/example/factories.js.map +0 -1
  20. package/build/implementations/http/express/example/procedures/auth.d.ts +0 -1
  21. package/build/implementations/http/express/example/procedures/auth.js +0 -22
  22. package/build/implementations/http/express/example/procedures/auth.js.map +0 -1
  23. package/build/implementations/http/express/example/procedures/users.d.ts +0 -1
  24. package/build/implementations/http/express/example/procedures/users.js +0 -30
  25. package/build/implementations/http/express/example/procedures/users.js.map +0 -1
  26. package/build/implementations/http/express/example/server.d.ts +0 -3
  27. package/build/implementations/http/express/example/server.js +0 -49
  28. package/build/implementations/http/express/example/server.js.map +0 -1
  29. package/build/implementations/http/express/example/server.test.d.ts +0 -1
  30. package/build/implementations/http/express/example/server.test.js +0 -110
  31. package/build/implementations/http/express/example/server.test.js.map +0 -1
  32. package/build/implementations/http/express/index.d.ts +0 -35
  33. package/build/implementations/http/express/index.js +0 -75
  34. package/build/implementations/http/express/index.js.map +0 -1
  35. package/build/implementations/http/express/index.test.d.ts +0 -1
  36. package/build/implementations/http/express/index.test.js +0 -329
  37. package/build/implementations/http/express/index.test.js.map +0 -1
  38. package/build/implementations/http/express/types.d.ts +0 -17
  39. package/build/implementations/http/express/types.js +0 -2
  40. 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 `/rpc/{name}/{version}` paths.
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('/rpc/echo/1').send({ message: 'hello' })
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('/rpc/echo/1')
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('/rpc/test/1').send({})
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', '/rpc/test/1')
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('/rpc/test/1').send({})
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('/rpc/test/1').send({})
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('/rpc/test/1').send({})
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('/rpc/test/1').send({})
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('/rpc/test/1').send({})
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('/rpc/test/1').send({})
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('/rpc/test/1').send({})
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('/rpc/public/1').send({})
280
- const privateRes = await request(app).post('/rpc/private/1').send({})
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 resolver receives Express request object', async () => {
287
- const contextResolver = vi.fn((req) => ({
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, contextResolver)
407
+ builder.register(RPC, factoryContext)
299
408
  const app = builder.build()
300
409
 
301
- await request(app).post('/rpc/get-auth/1').set('Authorization', 'Bearer token123').send({})
410
+ await request(app).post('/get-auth/1').set('Authorization', 'Bearer token123').send({})
302
411
 
303
- expect(contextResolver).toHaveBeenCalledTimes(1)
304
- expect(contextResolver.mock.calls[0]![0]).toHaveProperty('headers')
305
- expect(contextResolver.mock.calls[0]![0].headers).toHaveProperty(
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('/rpc/method-one/1').send({})
327
- const res2 = await request(app).post('/rpc/method-two/2').send({})
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('/rpc/method-one/1')
357
- expect(builder.docs[1]!.path).toBe('/rpc/nested/method/2')
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('/rpc/echo/1').send({ data: 'test-data' })
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('/rpc/test/1')
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' → /rpc/users/1", () => {
512
+ test("simple string: 'users' → /users/1", () => {
404
513
  const path = builder.makeRPCHttpRoutePath({ name: 'users', version: 1 })
405
- expect(path).toBe('/rpc/users/1')
514
+ expect(path).toBe('/users/1')
406
515
  })
407
516
 
408
- test("array name: ['users', 'get-by-id'] → /rpc/users/get-by-id/1", () => {
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('/rpc/users/get-by-id/1')
519
+ expect(path).toBe('/users/get-by-id/1')
411
520
  })
412
521
 
413
- test("camelCase: 'getUserById' → /rpc/get-user-by-id/1", () => {
522
+ test("camelCase: 'getUserById' → /get-user-by-id/1", () => {
414
523
  const path = builder.makeRPCHttpRoutePath({ name: 'getUserById', version: 1 })
415
- expect(path).toBe('/rpc/get-user-by-id/1')
524
+ expect(path).toBe('/get-user-by-id/1')
416
525
  })
417
526
 
418
- test("PascalCase: 'GetUserById' → /rpc/get-user-by-id/1", () => {
527
+ test("PascalCase: 'GetUserById' → /get-user-by-id/1", () => {
419
528
  const path = builder.makeRPCHttpRoutePath({ name: 'GetUserById', version: 1 })
420
- expect(path).toBe('/rpc/get-user-by-id/1')
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('/rpc/test/1')
429
- expect(pathV2).toBe('/rpc/test/2')
430
- expect(pathV99).toBe('/rpc/test/99')
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('/rpc/user-module/get-active-users/1')
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
- const { info } = RPC.Create(
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
- const procedure = RPC.getProcedures()[0]!
464
- const doc = builder.buildRpcHttpRouteDoc(procedure)
572
+ builder.register(RPC, () => ({}))
573
+ builder.build()
465
574
 
466
- expect(doc.path).toBe('/rpc/users/1')
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
- const procedure = RPC.getProcedures()[0]!
477
- const doc = builder.buildRpcHttpRouteDoc(procedure)
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
- const procedure = RPC.getProcedures()[0]!
491
- const doc = builder.buildRpcHttpRouteDoc(procedure)
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
- const procedures = RPC.getProcedures()
614
+ builder.register(RPC, () => ({}))
615
+ builder.build()
503
616
 
504
- procedures.forEach((proc) => {
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('/rpc/system/version/1').send({})
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('/rpc/health/1').send({})
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('/rpc/users/profile/1')
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('/rpc/users/profile/2')
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('/rpc/system/version/1')
601
- expect(paths).toContain('/rpc/health/1')
602
- expect(paths).toContain('/rpc/users/profile/1')
603
- expect(paths).toContain('/rpc/users/profile/2')
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 resolver.
92
- * @param factory
93
- * @param contextResolver
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
- contextResolver: (req: express.Request) => ExtractContext<TFactory>
129
+ factoryContext:
130
+ | ExtractContext<TFactory>
131
+ | ((req: express.Request) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
98
132
  ): this {
99
- this.factories.push({ factory, contextResolver } as ExpressFactoryItem<any>)
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, contextResolver }) => {
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
- res.json(await procedure.handler(contextResolver(req), req.body))
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 = this.makeRPCHttpRoutePath(config)
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
- } ? TContext : never
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
- contextResolver: (req: express.Request) => ExtractContext<TFactory>
30
+ factoryContext:
31
+ | ExtractContext<TFactory>
32
+ | ((req: express.Request) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
29
33
  }
@@ -7,7 +7,7 @@ export interface RPCConfig {
7
7
 
8
8
  export type FactoryItem<C> = {
9
9
  factory: ReturnType<typeof Procedures<C, RPCConfig>>
10
- contextResolver: (req: Request) => C
10
+ factoryContext: (req: Request) => C
11
11
  }
12
12
 
13
13
  export interface RPCHttpRouteDoc {
@@ -1 +0,0 @@
1
- export {};
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/implementations/http/client/index.ts"],"names":[],"mappings":""}