ts-procedures 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/build/errors.d.ts +2 -1
  2. package/build/errors.js +3 -2
  3. package/build/errors.js.map +1 -1
  4. package/build/errors.test.js +40 -0
  5. package/build/errors.test.js.map +1 -0
  6. package/build/implementations/http/express-rpc/index.d.ts +36 -35
  7. package/build/implementations/http/express-rpc/index.js +29 -13
  8. package/build/implementations/http/express-rpc/index.js.map +1 -1
  9. package/build/implementations/http/express-rpc/index.test.js +146 -92
  10. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  11. package/build/implementations/http/hono-rpc/index.d.ts +83 -0
  12. package/build/implementations/http/hono-rpc/index.js +148 -0
  13. package/build/implementations/http/hono-rpc/index.js.map +1 -0
  14. package/build/implementations/http/hono-rpc/index.test.js +647 -0
  15. package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
  16. package/build/implementations/http/hono-rpc/types.d.ts +28 -0
  17. package/build/implementations/http/hono-rpc/types.js.map +1 -0
  18. package/build/implementations/types.d.ts +1 -1
  19. package/build/index.d.ts +12 -0
  20. package/build/index.js +29 -7
  21. package/build/index.js.map +1 -1
  22. package/build/index.test.js +65 -0
  23. package/build/index.test.js.map +1 -1
  24. package/build/schema/parser.js +3 -0
  25. package/build/schema/parser.js.map +1 -1
  26. package/build/schema/parser.test.js +18 -0
  27. package/build/schema/parser.test.js.map +1 -1
  28. package/package.json +8 -2
  29. package/src/errors.test.ts +53 -0
  30. package/src/errors.ts +4 -2
  31. package/src/implementations/http/README.md +172 -0
  32. package/src/implementations/http/express-rpc/README.md +151 -228
  33. package/src/implementations/http/express-rpc/index.test.ts +167 -93
  34. package/src/implementations/http/express-rpc/index.ts +67 -38
  35. package/src/implementations/http/hono-rpc/README.md +293 -0
  36. package/src/implementations/http/hono-rpc/index.test.ts +847 -0
  37. package/src/implementations/http/hono-rpc/index.ts +202 -0
  38. package/src/implementations/http/hono-rpc/types.ts +33 -0
  39. package/src/implementations/types.ts +2 -1
  40. package/src/index.test.ts +83 -0
  41. package/src/index.ts +34 -8
  42. package/src/schema/parser.test.ts +26 -0
  43. package/src/schema/parser.ts +5 -1
  44. package/build/implementations/http/client/index.js +0 -2
  45. package/build/implementations/http/client/index.js.map +0 -1
  46. package/build/implementations/http/express/example/factories.d.ts +0 -97
  47. package/build/implementations/http/express/example/factories.js +0 -4
  48. package/build/implementations/http/express/example/factories.js.map +0 -1
  49. package/build/implementations/http/express/example/procedures/auth.d.ts +0 -1
  50. package/build/implementations/http/express/example/procedures/auth.js +0 -22
  51. package/build/implementations/http/express/example/procedures/auth.js.map +0 -1
  52. package/build/implementations/http/express/example/procedures/users.d.ts +0 -1
  53. package/build/implementations/http/express/example/procedures/users.js +0 -30
  54. package/build/implementations/http/express/example/procedures/users.js.map +0 -1
  55. package/build/implementations/http/express/example/server.d.ts +0 -3
  56. package/build/implementations/http/express/example/server.js +0 -49
  57. package/build/implementations/http/express/example/server.js.map +0 -1
  58. package/build/implementations/http/express/example/server.test.d.ts +0 -1
  59. package/build/implementations/http/express/example/server.test.js +0 -110
  60. package/build/implementations/http/express/example/server.test.js.map +0 -1
  61. package/build/implementations/http/express/index.d.ts +0 -35
  62. package/build/implementations/http/express/index.js +0 -75
  63. package/build/implementations/http/express/index.js.map +0 -1
  64. package/build/implementations/http/express/index.test.js +0 -329
  65. package/build/implementations/http/express/index.test.js.map +0 -1
  66. package/build/implementations/http/express/types.d.ts +0 -17
  67. package/build/implementations/http/express/types.js.map +0 -1
  68. /package/build/{implementations/http/client/index.d.ts → errors.test.d.ts} +0 -0
  69. /package/build/implementations/http/{express → hono-rpc}/index.test.d.ts +0 -0
  70. /package/build/implementations/http/{express → hono-rpc}/types.js +0 -0
@@ -8,7 +8,7 @@ import { ExpressRPCAppBuilder } from './index.js';
8
8
  * ExpressRPCAppBuilder Test Suite
9
9
  *
10
10
  * Tests the RPC-style Express integration for ts-procedures.
11
- * This builder creates POST routes at `/rpc/{name}/{version}` paths.
11
+ * This builder creates POST routes at `/{name}/{version}` paths (with optional pathPrefix).
12
12
  */
13
13
  describe('ExpressRPCAppBuilder', () => {
14
14
  // --------------------------------------------------------------------------
@@ -18,11 +18,11 @@ describe('ExpressRPCAppBuilder', () => {
18
18
  test('creates default Express app with json middleware', async () => {
19
19
  const builder = new ExpressRPCAppBuilder();
20
20
  const RPC = Procedures();
21
- RPC.Create('Echo', { name: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } }, async (ctx, params) => params);
21
+ RPC.Create('Echo', { scope: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } }, async (ctx, params) => params);
22
22
  builder.register(RPC, () => ({ userId: '123' }));
23
23
  const app = builder.build();
24
24
  // JSON body should be parsed automatically
25
- const res = await request(app).post('/rpc/echo/1').send({ message: 'hello' });
25
+ const res = await request(app).post('/echo/echo/1').send({ message: 'hello' });
26
26
  expect(res.status).toBe(200);
27
27
  expect(res.body).toEqual({ message: 'hello' });
28
28
  });
@@ -31,12 +31,12 @@ describe('ExpressRPCAppBuilder', () => {
31
31
  // Intentionally NOT adding json middleware
32
32
  const builder = new ExpressRPCAppBuilder({ app: customApp });
33
33
  const RPC = Procedures();
34
- RPC.Create('Echo', { name: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } }, async (ctx, params) => ({ received: params }));
34
+ RPC.Create('Echo', { scope: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } }, async (ctx, params) => ({ received: params }));
35
35
  builder.register(RPC, () => ({ userId: '123' }));
36
36
  const app = builder.build();
37
37
  // Without json middleware, body won't be parsed (req.body is undefined)
38
38
  const res = await request(app)
39
- .post('/rpc/echo/1')
39
+ .post('/echo/echo/1')
40
40
  .set('Content-Type', 'application/json')
41
41
  .send(JSON.stringify({ message: 'hello' }));
42
42
  // Request body is undefined since json middleware wasn't added
@@ -55,6 +55,57 @@ describe('ExpressRPCAppBuilder', () => {
55
55
  });
56
56
  });
57
57
  // --------------------------------------------------------------------------
58
+ // pathPrefix Option Tests
59
+ // --------------------------------------------------------------------------
60
+ describe('pathPrefix option', () => {
61
+ test('uses custom pathPrefix for all routes', async () => {
62
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: '/api/v1' });
63
+ const RPC = Procedures();
64
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
65
+ builder.register(RPC, () => ({}));
66
+ const app = builder.build();
67
+ const res = await request(app).post('/api/v1/test/test/1').send({});
68
+ expect(res.status).toBe(200);
69
+ expect(res.body).toEqual({ ok: true });
70
+ });
71
+ test('pathPrefix without leading slash gets normalized', async () => {
72
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: 'custom' });
73
+ const RPC = Procedures();
74
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
75
+ builder.register(RPC, () => ({}));
76
+ const app = builder.build();
77
+ const res = await request(app).post('/custom/test/test/1').send({});
78
+ expect(res.status).toBe(200);
79
+ });
80
+ test('no prefix when pathPrefix not specified', async () => {
81
+ const builder = new ExpressRPCAppBuilder();
82
+ const RPC = Procedures();
83
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
84
+ builder.register(RPC, () => ({}));
85
+ const app = builder.build();
86
+ const res = await request(app).post('/test/test/1').send({});
87
+ expect(res.status).toBe(200);
88
+ });
89
+ test('pathPrefix appears in generated docs', () => {
90
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: '/api' });
91
+ const RPC = Procedures();
92
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}));
93
+ builder.register(RPC, () => ({}));
94
+ builder.build();
95
+ expect(builder.docs[0].path).toBe('/api/test/test/1');
96
+ });
97
+ test('pathPrefix /rpc restores original behavior', async () => {
98
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: '/rpc' });
99
+ const RPC = Procedures();
100
+ RPC.Create('Users', { scope: 'users', version: 1 }, async () => ({ users: [] }));
101
+ builder.register(RPC, () => ({}));
102
+ const app = builder.build();
103
+ const res = await request(app).post('/rpc/users/users/1').send({});
104
+ expect(res.status).toBe(200);
105
+ expect(builder.docs[0].path).toBe('/rpc/users/users/1');
106
+ });
107
+ });
108
+ // --------------------------------------------------------------------------
58
109
  // Lifecycle Hooks Tests
59
110
  // --------------------------------------------------------------------------
60
111
  describe('lifecycle hooks', () => {
@@ -62,22 +113,22 @@ describe('ExpressRPCAppBuilder', () => {
62
113
  const onRequestStart = vi.fn();
63
114
  const builder = new ExpressRPCAppBuilder({ onRequestStart });
64
115
  const RPC = Procedures();
65
- RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }));
116
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
66
117
  builder.register(RPC, () => ({}));
67
118
  const app = builder.build();
68
- await request(app).post('/rpc/test/1').send({});
119
+ await request(app).post('/test/test/1').send({});
69
120
  expect(onRequestStart).toHaveBeenCalledTimes(1);
70
121
  expect(onRequestStart.mock.calls[0][0]).toHaveProperty('method', 'POST');
71
- expect(onRequestStart.mock.calls[0][0]).toHaveProperty('path', '/rpc/test/1');
122
+ expect(onRequestStart.mock.calls[0][0]).toHaveProperty('path', '/test/test/1');
72
123
  });
73
124
  test('onRequestEnd is called after response finishes', async () => {
74
125
  const onRequestEnd = vi.fn();
75
126
  const builder = new ExpressRPCAppBuilder({ onRequestEnd });
76
127
  const RPC = Procedures();
77
- RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }));
128
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
78
129
  builder.register(RPC, () => ({}));
79
130
  const app = builder.build();
80
- await request(app).post('/rpc/test/1').send({});
131
+ await request(app).post('/test/test/1').send({});
81
132
  expect(onRequestEnd).toHaveBeenCalledTimes(1);
82
133
  expect(onRequestEnd.mock.calls[0][0]).toHaveProperty('method', 'POST');
83
134
  expect(onRequestEnd.mock.calls[0][1]).toHaveProperty('statusCode', 200);
@@ -86,10 +137,10 @@ describe('ExpressRPCAppBuilder', () => {
86
137
  const onSuccess = vi.fn();
87
138
  const builder = new ExpressRPCAppBuilder({ onSuccess });
88
139
  const RPC = Procedures();
89
- RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }));
140
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
90
141
  builder.register(RPC, () => ({}));
91
142
  const app = builder.build();
92
- await request(app).post('/rpc/test/1').send({});
143
+ await request(app).post('/test/test/1').send({});
93
144
  expect(onSuccess).toHaveBeenCalledTimes(1);
94
145
  expect(onSuccess.mock.calls[0][0]).toHaveProperty('name', 'Test');
95
146
  });
@@ -97,12 +148,12 @@ describe('ExpressRPCAppBuilder', () => {
97
148
  const onSuccess = vi.fn();
98
149
  const builder = new ExpressRPCAppBuilder({ onSuccess });
99
150
  const RPC = Procedures();
100
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
151
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
101
152
  throw new Error('Handler error');
102
153
  });
103
154
  builder.register(RPC, () => ({}));
104
155
  const app = builder.build();
105
- await request(app).post('/rpc/test/1').send({});
156
+ await request(app).post('/test/test/1').send({});
106
157
  expect(onSuccess).not.toHaveBeenCalled();
107
158
  });
108
159
  test('hooks execute in correct order: start → handler → success → end', async () => {
@@ -113,13 +164,13 @@ describe('ExpressRPCAppBuilder', () => {
113
164
  onSuccess: () => order.push('success'),
114
165
  });
115
166
  const RPC = Procedures();
116
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
167
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
117
168
  order.push('handler');
118
169
  return { ok: true };
119
170
  });
120
171
  builder.register(RPC, () => ({}));
121
172
  const app = builder.build();
122
- await request(app).post('/rpc/test/1').send({});
173
+ await request(app).post('/test/test/1').send({});
123
174
  expect(order).toEqual(['start', 'handler', 'success', 'end']);
124
175
  });
125
176
  });
@@ -133,12 +184,12 @@ describe('ExpressRPCAppBuilder', () => {
133
184
  });
134
185
  const builder = new ExpressRPCAppBuilder({ error: errorHandler });
135
186
  const RPC = Procedures();
136
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
187
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
137
188
  throw new Error('Test error');
138
189
  });
139
190
  builder.register(RPC, () => ({}));
140
191
  const app = builder.build();
141
- const res = await request(app).post('/rpc/test/1').send({});
192
+ const res = await request(app).post('/test/test/1').send({});
142
193
  expect(errorHandler).toHaveBeenCalledTimes(1);
143
194
  expect(errorHandler.mock.calls[0][0]).toHaveProperty('name', 'Test');
144
195
  expect(errorHandler.mock.calls[0][3]).toBeInstanceOf(Error);
@@ -149,12 +200,12 @@ describe('ExpressRPCAppBuilder', () => {
149
200
  test('default error handling returns error message in response', async () => {
150
201
  const builder = new ExpressRPCAppBuilder();
151
202
  const RPC = Procedures();
152
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
203
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
153
204
  throw new Error('Something went wrong');
154
205
  });
155
206
  builder.register(RPC, () => ({}));
156
207
  const app = builder.build();
157
- const res = await request(app).post('/rpc/test/1').send({});
208
+ const res = await request(app).post('/test/test/1').send({});
158
209
  // Default error handler returns error message in JSON body
159
210
  expect(res.body).toHaveProperty('error');
160
211
  expect(res.body.error).toContain('Something went wrong');
@@ -162,14 +213,14 @@ describe('ExpressRPCAppBuilder', () => {
162
213
  test('catches unhandled exceptions in handler', async () => {
163
214
  const builder = new ExpressRPCAppBuilder();
164
215
  const RPC = Procedures();
165
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
216
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
166
217
  // Simulate unhandled exception
167
218
  const obj = null;
168
219
  return obj.property; // This will throw
169
220
  });
170
221
  builder.register(RPC, () => ({}));
171
222
  const app = builder.build();
172
- const res = await request(app).post('/rpc/test/1').send({});
223
+ const res = await request(app).post('/test/test/1').send({});
173
224
  // Unhandled exceptions are caught and returned as error response
174
225
  expect(res.body).toHaveProperty('error');
175
226
  });
@@ -192,18 +243,18 @@ describe('ExpressRPCAppBuilder', () => {
192
243
  const builder = new ExpressRPCAppBuilder();
193
244
  const PublicRPC = Procedures();
194
245
  const PrivateRPC = Procedures();
195
- PublicRPC.Create('PublicMethod', { name: 'public', version: 1 }, async (ctx) => ({
246
+ PublicRPC.Create('PublicMethod', { scope: 'public', version: 1 }, async (ctx) => ({
196
247
  isPublic: ctx.public,
197
248
  }));
198
- PrivateRPC.Create('PrivateMethod', { name: 'private', version: 1 }, async (ctx) => ({
249
+ PrivateRPC.Create('PrivateMethod', { scope: 'private', version: 1 }, async (ctx) => ({
199
250
  isPrivate: ctx.private,
200
251
  }));
201
252
  builder
202
253
  .register(PublicRPC, () => ({ public: true }))
203
254
  .register(PrivateRPC, () => ({ private: true }));
204
255
  const app = builder.build();
205
- const publicRes = await request(app).post('/rpc/public/1').send({});
206
- const privateRes = await request(app).post('/rpc/private/1').send({});
256
+ const publicRes = await request(app).post('/public/public-method/1').send({});
257
+ const privateRes = await request(app).post('/private/private-method/1').send({});
207
258
  expect(publicRes.body).toEqual({ isPublic: true });
208
259
  expect(privateRes.body).toEqual({ isPrivate: true });
209
260
  });
@@ -211,12 +262,12 @@ describe('ExpressRPCAppBuilder', () => {
211
262
  const factoryContext = { requestId: 'req-123' };
212
263
  const builder = new ExpressRPCAppBuilder();
213
264
  const RPC = Procedures();
214
- RPC.Create('GetRequestId', { name: 'get-request-id', version: 1 }, async (ctx) => ({
265
+ RPC.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
215
266
  id: ctx.requestId,
216
267
  }));
217
268
  builder.register(RPC, factoryContext);
218
269
  const app = builder.build();
219
- const res = await request(app).post('/rpc/get-request-id/1').send({});
270
+ const res = await request(app).post('/get-request-id/get-request-id/1').send({});
220
271
  expect(res.body).toEqual({ id: 'req-123' });
221
272
  });
222
273
  test('factoryContext can be async function', async () => {
@@ -225,12 +276,12 @@ describe('ExpressRPCAppBuilder', () => {
225
276
  });
226
277
  const builder = new ExpressRPCAppBuilder();
227
278
  const RPC = Procedures();
228
- RPC.Create('GetRequestId', { name: 'get-request-id', version: 1 }, async (ctx) => ({
279
+ RPC.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
229
280
  id: ctx.requestId,
230
281
  }));
231
282
  builder.register(RPC, factoryContext);
232
283
  const app = builder.build();
233
- await request(app).post('/rpc/get-request-id/1').send({});
284
+ await request(app).post('/get-request-id/get-request-id/1').send({});
234
285
  expect(factoryContext).toHaveBeenCalledTimes(1);
235
286
  });
236
287
  test('factoryContext function receives Express request object', async () => {
@@ -239,12 +290,12 @@ describe('ExpressRPCAppBuilder', () => {
239
290
  }));
240
291
  const builder = new ExpressRPCAppBuilder();
241
292
  const RPC = Procedures();
242
- RPC.Create('GetAuth', { name: 'get-auth', version: 1 }, async (ctx) => ({
293
+ RPC.Create('GetAuth', { scope: 'get-auth', version: 1 }, async (ctx) => ({
243
294
  auth: ctx.authHeader,
244
295
  }));
245
296
  builder.register(RPC, factoryContext);
246
297
  const app = builder.build();
247
- await request(app).post('/rpc/get-auth/1').set('Authorization', 'Bearer token123').send({});
298
+ await request(app).post('/get-auth/get-auth/1').set('Authorization', 'Bearer token123').send({});
248
299
  expect(factoryContext).toHaveBeenCalledTimes(1);
249
300
  expect(factoryContext.mock.calls[0][0]).toHaveProperty('headers');
250
301
  expect(factoryContext.mock.calls[0][0].headers).toHaveProperty('authorization', 'Bearer token123');
@@ -257,12 +308,12 @@ describe('ExpressRPCAppBuilder', () => {
257
308
  test('creates POST routes for all procedures', async () => {
258
309
  const builder = new ExpressRPCAppBuilder();
259
310
  const RPC = Procedures();
260
- RPC.Create('MethodOne', { name: 'method-one', version: 1 }, async () => ({ m: 1 }));
261
- RPC.Create('MethodTwo', { name: 'method-two', version: 2 }, async () => ({ m: 2 }));
311
+ RPC.Create('MethodOne', { scope: 'method-one', version: 1 }, async () => ({ m: 1 }));
312
+ RPC.Create('MethodTwo', { scope: 'method-two', version: 2 }, async () => ({ m: 2 }));
262
313
  builder.register(RPC, () => ({}));
263
314
  const app = builder.build();
264
- const res1 = await request(app).post('/rpc/method-one/1').send({});
265
- const res2 = await request(app).post('/rpc/method-two/2').send({});
315
+ const res1 = await request(app).post('/method-one/method-one/1').send({});
316
+ const res2 = await request(app).post('/method-two/method-two/2').send({});
266
317
  expect(res1.status).toBe(200);
267
318
  expect(res2.status).toBe(200);
268
319
  expect(res1.body).toEqual({ m: 1 });
@@ -277,31 +328,31 @@ describe('ExpressRPCAppBuilder', () => {
277
328
  test('populates docs array after build', () => {
278
329
  const builder = new ExpressRPCAppBuilder();
279
330
  const RPC = Procedures();
280
- RPC.Create('MethodOne', { name: 'method-one', version: 1 }, async () => ({}));
281
- RPC.Create('MethodTwo', { name: ['nested', 'method'], version: 2 }, async () => ({}));
331
+ RPC.Create('MethodOne', { scope: 'method-one', version: 1 }, async () => ({}));
332
+ RPC.Create('MethodTwo', { scope: ['nested', 'method'], version: 2 }, async () => ({}));
282
333
  expect(builder.docs).toHaveLength(0);
283
334
  builder.register(RPC, () => ({}));
284
335
  builder.build();
285
336
  expect(builder.docs).toHaveLength(2);
286
- expect(builder.docs[0].path).toBe('/rpc/method-one/1');
287
- expect(builder.docs[1].path).toBe('/rpc/nested/method/2');
337
+ expect(builder.docs[0].path).toBe('/method-one/method-one/1');
338
+ expect(builder.docs[1].path).toBe('/nested/method/method-two/2');
288
339
  });
289
340
  test('passes request body to handler as params', async () => {
290
341
  const builder = new ExpressRPCAppBuilder();
291
342
  const RPC = Procedures();
292
- RPC.Create('Echo', { name: 'echo', version: 1, schema: { params: v.object({ data: v.string() }) } }, async (ctx, params) => ({ received: params.data }));
343
+ RPC.Create('Echo', { scope: 'echo', version: 1, schema: { params: v.object({ data: v.string() }) } }, async (ctx, params) => ({ received: params.data }));
293
344
  builder.register(RPC, () => ({}));
294
345
  const app = builder.build();
295
- const res = await request(app).post('/rpc/echo/1').send({ data: 'test-data' });
346
+ const res = await request(app).post('/echo/echo/1').send({ data: 'test-data' });
296
347
  expect(res.body).toEqual({ received: 'test-data' });
297
348
  });
298
349
  test('GET requests return 404 (RPC uses POST only)', async () => {
299
350
  const builder = new ExpressRPCAppBuilder();
300
351
  const RPC = Procedures();
301
- RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }));
352
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }));
302
353
  builder.register(RPC, () => ({}));
303
354
  const app = builder.build();
304
- const res = await request(app).get('/rpc/test/1');
355
+ const res = await request(app).get('/test/test/1');
305
356
  expect(res.status).toBe(404);
306
357
  });
307
358
  });
@@ -313,36 +364,36 @@ describe('ExpressRPCAppBuilder', () => {
313
364
  beforeEach(() => {
314
365
  builder = new ExpressRPCAppBuilder();
315
366
  });
316
- test("simple string: 'users' → /rpc/users/1", () => {
317
- const path = builder.makeRPCHttpRoutePath({ name: 'users', version: 1 });
318
- expect(path).toBe('/rpc/users/1');
367
+ test("simple scope with procedure name: 'users' + 'GetUser' → /users/get-user/1", () => {
368
+ const path = builder.makeRPCHttpRoutePath('GetUser', { scope: 'users', version: 1 });
369
+ expect(path).toBe('/users/get-user/1');
319
370
  });
320
- test("array name: ['users', 'get-by-id'] → /rpc/users/get-by-id/1", () => {
321
- const path = builder.makeRPCHttpRoutePath({ name: ['users', 'get-by-id'], version: 1 });
322
- expect(path).toBe('/rpc/users/get-by-id/1');
371
+ test("array scope with procedure name: ['users', 'profile'] + 'GetById' → /users/profile/get-by-id/1", () => {
372
+ const path = builder.makeRPCHttpRoutePath('GetById', { scope: ['users', 'profile'], version: 1 });
373
+ expect(path).toBe('/users/profile/get-by-id/1');
323
374
  });
324
- test("camelCase: 'getUserById' → /rpc/get-user-by-id/1", () => {
325
- const path = builder.makeRPCHttpRoutePath({ name: 'getUserById', version: 1 });
326
- expect(path).toBe('/rpc/get-user-by-id/1');
375
+ test("camelCase procedure name: 'users' + 'getProfile' → /users/get-profile/1", () => {
376
+ const path = builder.makeRPCHttpRoutePath('getProfile', { scope: 'users', version: 1 });
377
+ expect(path).toBe('/users/get-profile/1');
327
378
  });
328
- test("PascalCase: 'GetUserById' → /rpc/get-user-by-id/1", () => {
329
- const path = builder.makeRPCHttpRoutePath({ name: 'GetUserById', version: 1 });
330
- expect(path).toBe('/rpc/get-user-by-id/1');
379
+ test("PascalCase procedure name: 'users' + 'UpdateProfile' → /users/update-profile/1", () => {
380
+ const path = builder.makeRPCHttpRoutePath('UpdateProfile', { scope: 'users', version: 1 });
381
+ expect(path).toBe('/users/update-profile/1');
331
382
  });
332
383
  test('version number included in path', () => {
333
- const pathV1 = builder.makeRPCHttpRoutePath({ name: 'test', version: 1 });
334
- const pathV2 = builder.makeRPCHttpRoutePath({ name: 'test', version: 2 });
335
- const pathV99 = builder.makeRPCHttpRoutePath({ name: 'test', version: 99 });
336
- expect(pathV1).toBe('/rpc/test/1');
337
- expect(pathV2).toBe('/rpc/test/2');
338
- expect(pathV99).toBe('/rpc/test/99');
384
+ const pathV1 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 1 });
385
+ const pathV2 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 2 });
386
+ const pathV99 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 99 });
387
+ expect(pathV1).toBe('/test/test/1');
388
+ expect(pathV2).toBe('/test/test/2');
389
+ expect(pathV99).toBe('/test/test/99');
339
390
  });
340
391
  test('handles mixed case in array segments', () => {
341
- const path = builder.makeRPCHttpRoutePath({
342
- name: ['UserModule', 'getActiveUsers'],
392
+ const path = builder.makeRPCHttpRoutePath('ListUsers', {
393
+ scope: ['UserModule', 'getActiveUsers'],
343
394
  version: 1,
344
395
  });
345
- expect(path).toBe('/rpc/user-module/get-active-users/1');
396
+ expect(path).toBe('/user-module/get-active-users/list-users/1');
346
397
  });
347
398
  });
348
399
  // --------------------------------------------------------------------------
@@ -357,36 +408,39 @@ describe('ExpressRPCAppBuilder', () => {
357
408
  const paramsSchema = v.object({ id: v.string() });
358
409
  const returnSchema = v.object({ name: v.string() });
359
410
  const RPC = Procedures();
360
- const { info } = RPC.Create('GetUser', { name: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } }, async () => ({ name: 'test' }));
361
- const procedure = RPC.getProcedures()[0];
362
- const doc = builder.buildRpcHttpRouteDoc(procedure);
363
- expect(doc.path).toBe('/rpc/users/1');
411
+ RPC.Create('GetUser', { scope: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } }, async () => ({ name: 'test' }));
412
+ builder.register(RPC, () => ({}));
413
+ builder.build();
414
+ const doc = builder.docs[0];
415
+ expect(doc.path).toBe('/users/get-user/1');
364
416
  expect(doc.method).toBe('post');
365
417
  expect(doc.jsonSchema.body).toBeDefined();
366
418
  expect(doc.jsonSchema.response).toBeDefined();
367
419
  });
368
420
  test('omits body schema when no params defined', () => {
369
421
  const RPC = Procedures();
370
- RPC.Create('NoParams', { name: 'no-params', version: 1 }, async () => ({ ok: true }));
371
- const procedure = RPC.getProcedures()[0];
372
- const doc = builder.buildRpcHttpRouteDoc(procedure);
422
+ RPC.Create('NoParams', { scope: 'no-params', version: 1 }, async () => ({ ok: true }));
423
+ builder.register(RPC, () => ({}));
424
+ builder.build();
425
+ const doc = builder.docs[0];
373
426
  expect(doc.jsonSchema.body).toBeUndefined();
374
427
  });
375
428
  test('omits response schema when no returnType defined', () => {
376
429
  const RPC = Procedures();
377
- RPC.Create('NoReturn', { name: 'no-return', version: 1, schema: { params: v.object({ x: v.number() }) } }, async () => ({}));
378
- const procedure = RPC.getProcedures()[0];
379
- const doc = builder.buildRpcHttpRouteDoc(procedure);
430
+ RPC.Create('NoReturn', { scope: 'no-return', version: 1, schema: { params: v.object({ x: v.number() }) } }, async () => ({}));
431
+ builder.register(RPC, () => ({}));
432
+ builder.build();
433
+ const doc = builder.docs[0];
380
434
  expect(doc.jsonSchema.body).toBeDefined();
381
435
  expect(doc.jsonSchema.response).toBeUndefined();
382
436
  });
383
437
  test("method is always 'post'", () => {
384
438
  const RPC = Procedures();
385
- RPC.Create('Test1', { name: 't1', version: 1 }, async () => ({}));
386
- RPC.Create('Test2', { name: 't2', version: 2 }, async () => ({}));
387
- const procedures = RPC.getProcedures();
388
- procedures.forEach((proc) => {
389
- const doc = builder.buildRpcHttpRouteDoc(proc);
439
+ RPC.Create('Test1', { scope: 't1', version: 1 }, async () => ({}));
440
+ RPC.Create('Test2', { scope: 't2', version: 2 }, async () => ({}));
441
+ builder.register(RPC, () => ({}));
442
+ builder.build();
443
+ builder.docs.forEach((doc) => {
390
444
  expect(doc.method).toBe('post');
391
445
  });
392
446
  });
@@ -400,20 +454,20 @@ describe('ExpressRPCAppBuilder', () => {
400
454
  const PublicRPC = Procedures();
401
455
  const AuthRPC = Procedures();
402
456
  // Create public procedures
403
- PublicRPC.Create('GetVersion', { name: ['system', 'version'], version: 1 }, async () => ({
457
+ PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
404
458
  version: '1.0.0',
405
459
  }));
406
- PublicRPC.Create('HealthCheck', { name: 'health', version: 1 }, async () => ({
460
+ PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
407
461
  status: 'ok',
408
462
  }));
409
463
  // Create authenticated procedures
410
464
  AuthRPC.Create('GetProfile', {
411
- name: ['users', 'profile'],
465
+ scope: ['users', 'profile'],
412
466
  version: 1,
413
467
  schema: { returnType: v.object({ userId: v.string(), source: v.string() }) },
414
468
  }, async (ctx) => ({ userId: ctx.userId, source: ctx.source }));
415
469
  AuthRPC.Create('UpdateProfile', {
416
- name: ['users', 'profile'],
470
+ scope: ['users', 'profile'],
417
471
  version: 2,
418
472
  schema: { params: v.object({ name: v.string() }) },
419
473
  }, async (ctx, params) => ({ userId: ctx.userId, name: params.name }));
@@ -432,21 +486,21 @@ describe('ExpressRPCAppBuilder', () => {
432
486
  }));
433
487
  const app = builder.build();
434
488
  // Test public endpoints
435
- const versionRes = await request(app).post('/rpc/system/version/1').send({});
489
+ const versionRes = await request(app).post('/system/version/get-version/1').send({});
436
490
  expect(versionRes.status).toBe(200);
437
491
  expect(versionRes.body).toEqual({ version: '1.0.0' });
438
- const healthRes = await request(app).post('/rpc/health/1').send({});
492
+ const healthRes = await request(app).post('/health/health-check/1').send({});
439
493
  expect(healthRes.status).toBe(200);
440
494
  expect(healthRes.body).toEqual({ status: 'ok' });
441
495
  // Test authenticated endpoints
442
496
  const profileRes = await request(app)
443
- .post('/rpc/users/profile/1')
497
+ .post('/users/profile/get-profile/1')
444
498
  .set('X-User-Id', 'user-123')
445
499
  .send({});
446
500
  expect(profileRes.status).toBe(200);
447
501
  expect(profileRes.body).toEqual({ userId: 'user-123', source: 'auth' });
448
502
  const updateRes = await request(app)
449
- .post('/rpc/users/profile/2')
503
+ .post('/users/profile/update-profile/2')
450
504
  .set('X-User-Id', 'user-456')
451
505
  .send({ name: 'John Doe' });
452
506
  expect(updateRes.status).toBe(200);
@@ -454,10 +508,10 @@ describe('ExpressRPCAppBuilder', () => {
454
508
  // Verify documentation
455
509
  expect(builder.docs).toHaveLength(4);
456
510
  const paths = builder.docs.map((d) => d.path);
457
- expect(paths).toContain('/rpc/system/version/1');
458
- expect(paths).toContain('/rpc/health/1');
459
- expect(paths).toContain('/rpc/users/profile/1');
460
- expect(paths).toContain('/rpc/users/profile/2');
511
+ expect(paths).toContain('/system/version/get-version/1');
512
+ expect(paths).toContain('/health/health-check/1');
513
+ expect(paths).toContain('/users/profile/get-profile/1');
514
+ expect(paths).toContain('/users/profile/update-profile/2');
461
515
  // Verify hooks were called
462
516
  expect(events).toContain('request-start');
463
517
  expect(events).toContain('success:GetVersion');