ts-procedures 2.1.1 → 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.
- package/build/errors.d.ts +2 -1
- package/build/errors.js +3 -2
- package/build/errors.js.map +1 -1
- package/build/errors.test.d.ts +1 -0
- package/build/errors.test.js +40 -0
- package/build/errors.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +3 -2
- package/build/implementations/http/express-rpc/index.js +6 -6
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +93 -93
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.d.ts +83 -0
- package/build/implementations/http/hono-rpc/index.js +148 -0
- package/build/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/index.test.js +647 -0
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/types.d.ts +28 -0
- package/build/implementations/http/hono-rpc/types.js +2 -0
- package/build/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/implementations/types.d.ts +1 -1
- package/build/index.d.ts +12 -0
- package/build/index.js +29 -7
- package/build/index.js.map +1 -1
- package/build/index.test.js +65 -0
- package/build/index.test.js.map +1 -1
- package/build/schema/parser.js +3 -0
- package/build/schema/parser.js.map +1 -1
- package/build/schema/parser.test.js +18 -0
- package/build/schema/parser.test.js.map +1 -1
- package/package.json +8 -2
- package/src/errors.test.ts +53 -0
- package/src/errors.ts +4 -2
- package/src/implementations/http/README.md +172 -0
- package/src/implementations/http/express-rpc/README.md +152 -243
- package/src/implementations/http/express-rpc/index.test.ts +93 -93
- package/src/implementations/http/express-rpc/index.ts +15 -7
- package/src/implementations/http/hono-rpc/README.md +293 -0
- package/src/implementations/http/hono-rpc/index.test.ts +847 -0
- package/src/implementations/http/hono-rpc/index.ts +202 -0
- package/src/implementations/http/hono-rpc/types.ts +33 -0
- package/src/implementations/types.ts +2 -1
- package/src/index.test.ts +83 -0
- package/src/index.ts +34 -8
- package/src/schema/parser.test.ts +26 -0
- 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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
246
|
+
PublicRPC.Create('PublicMethod', { scope: 'public', version: 1 }, async (ctx) => ({
|
|
247
247
|
isPublic: ctx.public,
|
|
248
248
|
}));
|
|
249
|
-
PrivateRPC.Create('PrivateMethod', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
312
|
-
RPC.Create('MethodTwo', {
|
|
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', {
|
|
332
|
-
RPC.Create('MethodTwo', {
|
|
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', {
|
|
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', {
|
|
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
|
|
368
|
-
const path = builder.makeRPCHttpRoutePath({
|
|
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', '
|
|
372
|
-
const path = builder.makeRPCHttpRoutePath({
|
|
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: '
|
|
376
|
-
const path = builder.makeRPCHttpRoutePath({
|
|
377
|
-
expect(path).toBe('/get-
|
|
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: '
|
|
380
|
-
const path = builder.makeRPCHttpRoutePath({
|
|
381
|
-
expect(path).toBe('/
|
|
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({
|
|
385
|
-
const pathV2 = builder.makeRPCHttpRoutePath({
|
|
386
|
-
const pathV99 = builder.makeRPCHttpRoutePath({
|
|
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
|
-
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
440
|
-
RPC.Create('Test2', {
|
|
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', {
|
|
457
|
+
PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
|
|
458
458
|
version: '1.0.0',
|
|
459
459
|
}));
|
|
460
|
-
PublicRPC.Create('HealthCheck', {
|
|
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
|
-
|
|
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
|
-
|
|
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');
|