ts-procedures 2.1.0 → 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 +35 -35
- package/build/implementations/http/express-rpc/index.js +29 -13
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +108 -54
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/package.json +1 -1
- package/src/implementations/http/express-rpc/README.md +27 -13
- package/src/implementations/http/express-rpc/index.test.ts +128 -54
- package/src/implementations/http/express-rpc/index.ts +59 -38
- 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,8 +347,8 @@ 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 })
|
|
@@ -296,7 +367,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
296
367
|
builder.register(RPC, factoryContext)
|
|
297
368
|
const app = builder.build()
|
|
298
369
|
|
|
299
|
-
const res = await request(app).post('/
|
|
370
|
+
const res = await request(app).post('/get-request-id/1').send({})
|
|
300
371
|
|
|
301
372
|
expect(res.body).toEqual({ id: 'req-123' })
|
|
302
373
|
})
|
|
@@ -316,7 +387,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
316
387
|
builder.register(RPC, factoryContext)
|
|
317
388
|
const app = builder.build()
|
|
318
389
|
|
|
319
|
-
await request(app).post('/
|
|
390
|
+
await request(app).post('/get-request-id/1').send({})
|
|
320
391
|
|
|
321
392
|
expect(factoryContext).toHaveBeenCalledTimes(1)
|
|
322
393
|
})
|
|
@@ -336,7 +407,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
336
407
|
builder.register(RPC, factoryContext)
|
|
337
408
|
const app = builder.build()
|
|
338
409
|
|
|
339
|
-
await request(app).post('/
|
|
410
|
+
await request(app).post('/get-auth/1').set('Authorization', 'Bearer token123').send({})
|
|
340
411
|
|
|
341
412
|
expect(factoryContext).toHaveBeenCalledTimes(1)
|
|
342
413
|
expect(factoryContext.mock.calls[0]![0]).toHaveProperty('headers')
|
|
@@ -361,8 +432,8 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
361
432
|
builder.register(RPC, () => ({}))
|
|
362
433
|
const app = builder.build()
|
|
363
434
|
|
|
364
|
-
const res1 = await request(app).post('/
|
|
365
|
-
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({})
|
|
366
437
|
|
|
367
438
|
expect(res1.status).toBe(200)
|
|
368
439
|
expect(res2.status).toBe(200)
|
|
@@ -391,8 +462,8 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
391
462
|
builder.build()
|
|
392
463
|
|
|
393
464
|
expect(builder.docs).toHaveLength(2)
|
|
394
|
-
expect(builder.docs[0]!.path).toBe('/
|
|
395
|
-
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')
|
|
396
467
|
})
|
|
397
468
|
|
|
398
469
|
test('passes request body to handler as params', async () => {
|
|
@@ -408,7 +479,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
408
479
|
builder.register(RPC, () => ({}))
|
|
409
480
|
const app = builder.build()
|
|
410
481
|
|
|
411
|
-
const res = await request(app).post('/
|
|
482
|
+
const res = await request(app).post('/echo/1').send({ data: 'test-data' })
|
|
412
483
|
|
|
413
484
|
expect(res.body).toEqual({ received: 'test-data' })
|
|
414
485
|
})
|
|
@@ -422,7 +493,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
422
493
|
builder.register(RPC, () => ({}))
|
|
423
494
|
const app = builder.build()
|
|
424
495
|
|
|
425
|
-
const res = await request(app).get('/
|
|
496
|
+
const res = await request(app).get('/test/1')
|
|
426
497
|
|
|
427
498
|
expect(res.status).toBe(404)
|
|
428
499
|
})
|
|
@@ -438,24 +509,24 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
438
509
|
builder = new ExpressRPCAppBuilder()
|
|
439
510
|
})
|
|
440
511
|
|
|
441
|
-
test("simple string: 'users' → /
|
|
512
|
+
test("simple string: 'users' → /users/1", () => {
|
|
442
513
|
const path = builder.makeRPCHttpRoutePath({ name: 'users', version: 1 })
|
|
443
|
-
expect(path).toBe('/
|
|
514
|
+
expect(path).toBe('/users/1')
|
|
444
515
|
})
|
|
445
516
|
|
|
446
|
-
test("array name: ['users', 'get-by-id'] → /
|
|
517
|
+
test("array name: ['users', 'get-by-id'] → /users/get-by-id/1", () => {
|
|
447
518
|
const path = builder.makeRPCHttpRoutePath({ name: ['users', 'get-by-id'], version: 1 })
|
|
448
|
-
expect(path).toBe('/
|
|
519
|
+
expect(path).toBe('/users/get-by-id/1')
|
|
449
520
|
})
|
|
450
521
|
|
|
451
|
-
test("camelCase: 'getUserById' → /
|
|
522
|
+
test("camelCase: 'getUserById' → /get-user-by-id/1", () => {
|
|
452
523
|
const path = builder.makeRPCHttpRoutePath({ name: 'getUserById', version: 1 })
|
|
453
|
-
expect(path).toBe('/
|
|
524
|
+
expect(path).toBe('/get-user-by-id/1')
|
|
454
525
|
})
|
|
455
526
|
|
|
456
|
-
test("PascalCase: 'GetUserById' → /
|
|
527
|
+
test("PascalCase: 'GetUserById' → /get-user-by-id/1", () => {
|
|
457
528
|
const path = builder.makeRPCHttpRoutePath({ name: 'GetUserById', version: 1 })
|
|
458
|
-
expect(path).toBe('/
|
|
529
|
+
expect(path).toBe('/get-user-by-id/1')
|
|
459
530
|
})
|
|
460
531
|
|
|
461
532
|
test('version number included in path', () => {
|
|
@@ -463,9 +534,9 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
463
534
|
const pathV2 = builder.makeRPCHttpRoutePath({ name: 'test', version: 2 })
|
|
464
535
|
const pathV99 = builder.makeRPCHttpRoutePath({ name: 'test', version: 99 })
|
|
465
536
|
|
|
466
|
-
expect(pathV1).toBe('/
|
|
467
|
-
expect(pathV2).toBe('/
|
|
468
|
-
expect(pathV99).toBe('/
|
|
537
|
+
expect(pathV1).toBe('/test/1')
|
|
538
|
+
expect(pathV2).toBe('/test/2')
|
|
539
|
+
expect(pathV99).toBe('/test/99')
|
|
469
540
|
})
|
|
470
541
|
|
|
471
542
|
test('handles mixed case in array segments', () => {
|
|
@@ -473,7 +544,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
473
544
|
name: ['UserModule', 'getActiveUsers'],
|
|
474
545
|
version: 1,
|
|
475
546
|
})
|
|
476
|
-
expect(path).toBe('/
|
|
547
|
+
expect(path).toBe('/user-module/get-active-users/1')
|
|
477
548
|
})
|
|
478
549
|
})
|
|
479
550
|
|
|
@@ -492,16 +563,17 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
492
563
|
const returnSchema = v.object({ name: v.string() })
|
|
493
564
|
|
|
494
565
|
const RPC = Procedures<{}, RPCConfig>()
|
|
495
|
-
|
|
566
|
+
RPC.Create(
|
|
496
567
|
'GetUser',
|
|
497
568
|
{ name: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } },
|
|
498
569
|
async () => ({ name: 'test' })
|
|
499
570
|
)
|
|
500
571
|
|
|
501
|
-
|
|
502
|
-
|
|
572
|
+
builder.register(RPC, () => ({}))
|
|
573
|
+
builder.build()
|
|
503
574
|
|
|
504
|
-
|
|
575
|
+
const doc = builder.docs[0]!
|
|
576
|
+
expect(doc.path).toBe('/users/1')
|
|
505
577
|
expect(doc.method).toBe('post')
|
|
506
578
|
expect(doc.jsonSchema.body).toBeDefined()
|
|
507
579
|
expect(doc.jsonSchema.response).toBeDefined()
|
|
@@ -511,9 +583,10 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
511
583
|
const RPC = Procedures<{}, RPCConfig>()
|
|
512
584
|
RPC.Create('NoParams', { name: 'no-params', version: 1 }, async () => ({ ok: true }))
|
|
513
585
|
|
|
514
|
-
|
|
515
|
-
|
|
586
|
+
builder.register(RPC, () => ({}))
|
|
587
|
+
builder.build()
|
|
516
588
|
|
|
589
|
+
const doc = builder.docs[0]!
|
|
517
590
|
expect(doc.jsonSchema.body).toBeUndefined()
|
|
518
591
|
})
|
|
519
592
|
|
|
@@ -525,9 +598,10 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
525
598
|
async () => ({})
|
|
526
599
|
)
|
|
527
600
|
|
|
528
|
-
|
|
529
|
-
|
|
601
|
+
builder.register(RPC, () => ({}))
|
|
602
|
+
builder.build()
|
|
530
603
|
|
|
604
|
+
const doc = builder.docs[0]!
|
|
531
605
|
expect(doc.jsonSchema.body).toBeDefined()
|
|
532
606
|
expect(doc.jsonSchema.response).toBeUndefined()
|
|
533
607
|
})
|
|
@@ -537,10 +611,10 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
537
611
|
RPC.Create('Test1', { name: 't1', version: 1 }, async () => ({}))
|
|
538
612
|
RPC.Create('Test2', { name: 't2', version: 2 }, async () => ({}))
|
|
539
613
|
|
|
540
|
-
|
|
614
|
+
builder.register(RPC, () => ({}))
|
|
615
|
+
builder.build()
|
|
541
616
|
|
|
542
|
-
|
|
543
|
-
const doc = builder.buildRpcHttpRouteDoc(proc)
|
|
617
|
+
builder.docs.forEach((doc) => {
|
|
544
618
|
expect(doc.method).toBe('post')
|
|
545
619
|
})
|
|
546
620
|
})
|
|
@@ -608,24 +682,24 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
608
682
|
const app = builder.build()
|
|
609
683
|
|
|
610
684
|
// Test public endpoints
|
|
611
|
-
const versionRes = await request(app).post('/
|
|
685
|
+
const versionRes = await request(app).post('/system/version/1').send({})
|
|
612
686
|
expect(versionRes.status).toBe(200)
|
|
613
687
|
expect(versionRes.body).toEqual({ version: '1.0.0' })
|
|
614
688
|
|
|
615
|
-
const healthRes = await request(app).post('/
|
|
689
|
+
const healthRes = await request(app).post('/health/1').send({})
|
|
616
690
|
expect(healthRes.status).toBe(200)
|
|
617
691
|
expect(healthRes.body).toEqual({ status: 'ok' })
|
|
618
692
|
|
|
619
693
|
// Test authenticated endpoints
|
|
620
694
|
const profileRes = await request(app)
|
|
621
|
-
.post('/
|
|
695
|
+
.post('/users/profile/1')
|
|
622
696
|
.set('X-User-Id', 'user-123')
|
|
623
697
|
.send({})
|
|
624
698
|
expect(profileRes.status).toBe(200)
|
|
625
699
|
expect(profileRes.body).toEqual({ userId: 'user-123', source: 'auth' })
|
|
626
700
|
|
|
627
701
|
const updateRes = await request(app)
|
|
628
|
-
.post('/
|
|
702
|
+
.post('/users/profile/2')
|
|
629
703
|
.set('X-User-Id', 'user-456')
|
|
630
704
|
.send({ name: 'John Doe' })
|
|
631
705
|
expect(updateRes.status).toBe(200)
|
|
@@ -635,10 +709,10 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
635
709
|
expect(builder.docs).toHaveLength(4)
|
|
636
710
|
|
|
637
711
|
const paths = builder.docs.map((d) => d.path)
|
|
638
|
-
expect(paths).toContain('/
|
|
639
|
-
expect(paths).toContain('/
|
|
640
|
-
expect(paths).toContain('/
|
|
641
|
-
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')
|
|
642
716
|
|
|
643
717
|
// Verify hooks were called
|
|
644
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()
|
|
@@ -153,9 +184,12 @@ export class ExpressRPCAppBuilder {
|
|
|
153
184
|
* Generates the RPC HTTP route for the given procedure.
|
|
154
185
|
* @param procedure
|
|
155
186
|
*/
|
|
156
|
-
buildRpcHttpRouteDoc(procedure: TProcedureRegistration<any, RPCConfig>): RPCHttpRouteDoc {
|
|
187
|
+
private buildRpcHttpRouteDoc(procedure: TProcedureRegistration<any, RPCConfig>): RPCHttpRouteDoc {
|
|
157
188
|
const { config } = procedure
|
|
158
|
-
const path =
|
|
189
|
+
const path = ExpressRPCAppBuilder.makeRPCHttpRoutePath({
|
|
190
|
+
config,
|
|
191
|
+
prefix: this.config?.pathPrefix,
|
|
192
|
+
})
|
|
159
193
|
const method = 'post' // RPCs use POST method
|
|
160
194
|
const jsonSchema: { body?: object; response?: object } = {}
|
|
161
195
|
|
|
@@ -172,17 +206,4 @@ export class ExpressRPCAppBuilder {
|
|
|
172
206
|
jsonSchema,
|
|
173
207
|
}
|
|
174
208
|
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Generates the RPC route path based on the RPC configuration.
|
|
178
|
-
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
179
|
-
*
|
|
180
|
-
* Example
|
|
181
|
-
* name: ['string', 'string-string', 'string']
|
|
182
|
-
* path: /rpc/string/string-string/string/version
|
|
183
|
-
* @param config
|
|
184
|
-
*/
|
|
185
|
-
makeRPCHttpRoutePath(config: RPCConfig) {
|
|
186
|
-
return `/rpc/${castArray(config.name).map(kebabCase).join('/')}/${String(config.version).trim()}`
|
|
187
|
-
}
|
|
188
209
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/implementations/http/client/index.ts"],"names":[],"mappings":""}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
export interface ProceduresContext {
|
|
2
|
-
headers: Record<string, string>;
|
|
3
|
-
ipAddress: string;
|
|
4
|
-
requestId: string;
|
|
5
|
-
}
|
|
6
|
-
export interface ProtectedContext extends ProceduresContext {
|
|
7
|
-
userId: string;
|
|
8
|
-
}
|
|
9
|
-
export interface PublicContext extends ProceduresContext {
|
|
10
|
-
userId?: undefined;
|
|
11
|
-
}
|
|
12
|
-
export declare const ProtectedFactory: {
|
|
13
|
-
getProcedures: () => import("../../../../index.js").TProcedureRegistration<ProtectedContext, HTTPRouteConfig>[];
|
|
14
|
-
Create: <TName extends string, TParams, TReturnType>(name: TName, config: {
|
|
15
|
-
description?: string;
|
|
16
|
-
schema?: {
|
|
17
|
-
params?: TParams | undefined;
|
|
18
|
-
returnType?: TReturnType | undefined;
|
|
19
|
-
} | undefined;
|
|
20
|
-
} & HTTPRouteConfig, handler: (ctx: {
|
|
21
|
-
userId: string;
|
|
22
|
-
headers: Record<string, string>;
|
|
23
|
-
ipAddress: string;
|
|
24
|
-
requestId: string;
|
|
25
|
-
error: (message: string, meta?: object) => import("../../../../errors.js").ProcedureError;
|
|
26
|
-
}, params: import("../../../../exports.js").TSchemaLib<TParams>) => Promise<import("../../../../exports.js").TSchemaLib<TReturnType>>) => { [K in TName]: (ctx: {
|
|
27
|
-
userId: string;
|
|
28
|
-
headers: Record<string, string>;
|
|
29
|
-
ipAddress: string;
|
|
30
|
-
requestId: string;
|
|
31
|
-
}, params: import("../../../../exports.js").TSchemaLib<TParams>) => Promise<import("../../../../exports.js").TSchemaLib<TReturnType>>; } & {
|
|
32
|
-
procedure: (ctx: {
|
|
33
|
-
userId: string;
|
|
34
|
-
headers: Record<string, string>;
|
|
35
|
-
ipAddress: string;
|
|
36
|
-
requestId: string;
|
|
37
|
-
}, params: import("../../../../exports.js").TSchemaLib<TParams>) => Promise<import("../../../../exports.js").TSchemaLib<TReturnType>>;
|
|
38
|
-
info: {
|
|
39
|
-
name: TName;
|
|
40
|
-
description?: string;
|
|
41
|
-
schema: {
|
|
42
|
-
params?: TParams | undefined;
|
|
43
|
-
returnType?: TReturnType | undefined;
|
|
44
|
-
};
|
|
45
|
-
validation?: {
|
|
46
|
-
params?: ((params: any) => {
|
|
47
|
-
errors?: any[];
|
|
48
|
-
}) | undefined;
|
|
49
|
-
} | undefined;
|
|
50
|
-
} & HTTPRouteConfig;
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
export declare const PublicFactory: {
|
|
54
|
-
getProcedures: () => import("../../../../index.js").TProcedureRegistration<PublicContext, HTTPRouteConfig>[];
|
|
55
|
-
Create: <TName extends string, TParams, TReturnType>(name: TName, config: {
|
|
56
|
-
description?: string;
|
|
57
|
-
schema?: {
|
|
58
|
-
params?: TParams | undefined;
|
|
59
|
-
returnType?: TReturnType | undefined;
|
|
60
|
-
} | undefined;
|
|
61
|
-
} & HTTPRouteConfig, handler: (ctx: {
|
|
62
|
-
userId?: undefined | undefined;
|
|
63
|
-
headers: Record<string, string>;
|
|
64
|
-
ipAddress: string;
|
|
65
|
-
requestId: string;
|
|
66
|
-
error: (message: string, meta?: object) => import("../../../../errors.js").ProcedureError;
|
|
67
|
-
}, params: import("../../../../exports.js").TSchemaLib<TParams>) => Promise<import("../../../../exports.js").TSchemaLib<TReturnType>>) => { [K in TName]: (ctx: {
|
|
68
|
-
userId?: undefined | undefined;
|
|
69
|
-
headers: Record<string, string>;
|
|
70
|
-
ipAddress: string;
|
|
71
|
-
requestId: string;
|
|
72
|
-
}, params: import("../../../../exports.js").TSchemaLib<TParams>) => Promise<import("../../../../exports.js").TSchemaLib<TReturnType>>; } & {
|
|
73
|
-
procedure: (ctx: {
|
|
74
|
-
userId?: undefined | undefined;
|
|
75
|
-
headers: Record<string, string>;
|
|
76
|
-
ipAddress: string;
|
|
77
|
-
requestId: string;
|
|
78
|
-
}, params: import("../../../../exports.js").TSchemaLib<TParams>) => Promise<import("../../../../exports.js").TSchemaLib<TReturnType>>;
|
|
79
|
-
info: {
|
|
80
|
-
name: TName;
|
|
81
|
-
description?: string;
|
|
82
|
-
schema: {
|
|
83
|
-
params?: TParams | undefined;
|
|
84
|
-
returnType?: TReturnType | undefined;
|
|
85
|
-
};
|
|
86
|
-
validation?: {
|
|
87
|
-
params?: ((params: any) => {
|
|
88
|
-
errors?: any[];
|
|
89
|
-
}) | undefined;
|
|
90
|
-
} | undefined;
|
|
91
|
-
} & HTTPRouteConfig;
|
|
92
|
-
};
|
|
93
|
-
};
|
|
94
|
-
export interface HTTPRouteConfig {
|
|
95
|
-
path: string;
|
|
96
|
-
method: 'get' | 'post' | 'put' | 'delete' | 'patch';
|
|
97
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"factories.js","sourceRoot":"","sources":["../../../../../src/implementations/http/express/example/factories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAiBjD,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,EAAoC,CAAA;AAE9E,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,EAAiC,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|