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.
- package/build/errors.d.ts +2 -1
- package/build/errors.js +3 -2
- package/build/errors.js.map +1 -1
- package/build/errors.test.js +40 -0
- package/build/errors.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +36 -35
- package/build/implementations/http/express-rpc/index.js +29 -13
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +146 -92
- 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.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.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 +151 -228
- package/src/implementations/http/express-rpc/index.test.ts +167 -93
- package/src/implementations/http/express-rpc/index.ts +67 -38
- 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
- package/build/implementations/http/client/index.js +0 -2
- package/build/implementations/http/client/index.js.map +0 -1
- package/build/implementations/http/express/example/factories.d.ts +0 -97
- package/build/implementations/http/express/example/factories.js +0 -4
- package/build/implementations/http/express/example/factories.js.map +0 -1
- package/build/implementations/http/express/example/procedures/auth.d.ts +0 -1
- package/build/implementations/http/express/example/procedures/auth.js +0 -22
- package/build/implementations/http/express/example/procedures/auth.js.map +0 -1
- package/build/implementations/http/express/example/procedures/users.d.ts +0 -1
- package/build/implementations/http/express/example/procedures/users.js +0 -30
- package/build/implementations/http/express/example/procedures/users.js.map +0 -1
- package/build/implementations/http/express/example/server.d.ts +0 -3
- package/build/implementations/http/express/example/server.js +0 -49
- package/build/implementations/http/express/example/server.js.map +0 -1
- package/build/implementations/http/express/example/server.test.d.ts +0 -1
- package/build/implementations/http/express/example/server.test.js +0 -110
- package/build/implementations/http/express/example/server.test.js.map +0 -1
- package/build/implementations/http/express/index.d.ts +0 -35
- package/build/implementations/http/express/index.js +0 -75
- package/build/implementations/http/express/index.js.map +0 -1
- package/build/implementations/http/express/index.test.js +0 -329
- package/build/implementations/http/express/index.test.js.map +0 -1
- package/build/implementations/http/express/types.d.ts +0 -17
- package/build/implementations/http/express/types.js.map +0 -1
- /package/build/{implementations/http/client/index.d.ts → errors.test.d.ts} +0 -0
- /package/build/implementations/http/{express → hono-rpc}/index.test.d.ts +0 -0
- /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 `/
|
|
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', {
|
|
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('/
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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', '/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
246
|
+
PublicRPC.Create('PublicMethod', { scope: 'public', version: 1 }, async (ctx) => ({
|
|
196
247
|
isPublic: ctx.public,
|
|
197
248
|
}));
|
|
198
|
-
PrivateRPC.Create('PrivateMethod', {
|
|
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('/
|
|
206
|
-
const privateRes = await request(app).post('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
261
|
-
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 }));
|
|
262
313
|
builder.register(RPC, () => ({}));
|
|
263
314
|
const app = builder.build();
|
|
264
|
-
const res1 = await request(app).post('/
|
|
265
|
-
const res2 = await request(app).post('/
|
|
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', {
|
|
281
|
-
RPC.Create('MethodTwo', {
|
|
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('/
|
|
287
|
-
expect(builder.docs[1].path).toBe('/
|
|
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', {
|
|
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('/
|
|
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', {
|
|
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('/
|
|
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
|
|
317
|
-
const path = builder.makeRPCHttpRoutePath({
|
|
318
|
-
expect(path).toBe('/
|
|
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', '
|
|
321
|
-
const path = builder.makeRPCHttpRoutePath({
|
|
322
|
-
expect(path).toBe('/
|
|
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: '
|
|
325
|
-
const path = builder.makeRPCHttpRoutePath({
|
|
326
|
-
expect(path).toBe('/
|
|
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: '
|
|
329
|
-
const path = builder.makeRPCHttpRoutePath({
|
|
330
|
-
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');
|
|
331
382
|
});
|
|
332
383
|
test('version number included in path', () => {
|
|
333
|
-
const pathV1 = builder.makeRPCHttpRoutePath({
|
|
334
|
-
const pathV2 = builder.makeRPCHttpRoutePath({
|
|
335
|
-
const pathV99 = builder.makeRPCHttpRoutePath({
|
|
336
|
-
expect(pathV1).toBe('/
|
|
337
|
-
expect(pathV2).toBe('/
|
|
338
|
-
expect(pathV99).toBe('/
|
|
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
|
-
|
|
392
|
+
const path = builder.makeRPCHttpRoutePath('ListUsers', {
|
|
393
|
+
scope: ['UserModule', 'getActiveUsers'],
|
|
343
394
|
version: 1,
|
|
344
395
|
});
|
|
345
|
-
expect(path).toBe('/
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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', {
|
|
371
|
-
|
|
372
|
-
|
|
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', {
|
|
378
|
-
|
|
379
|
-
|
|
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', {
|
|
386
|
-
RPC.Create('Test2', {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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', {
|
|
457
|
+
PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
|
|
404
458
|
version: '1.0.0',
|
|
405
459
|
}));
|
|
406
|
-
PublicRPC.Create('HealthCheck', {
|
|
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
|
-
|
|
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
|
-
|
|
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('/
|
|
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('/
|
|
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('/
|
|
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('/
|
|
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('/
|
|
458
|
-
expect(paths).toContain('/
|
|
459
|
-
expect(paths).toContain('/
|
|
460
|
-
expect(paths).toContain('/
|
|
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');
|