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