ts-procedures 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/implementations/http/express-rpc/index.d.ts +35 -35
- package/build/implementations/http/express-rpc/index.js +29 -13
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +108 -54
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/package.json +1 -1
- package/src/implementations/http/express-rpc/README.md +27 -13
- package/src/implementations/http/express-rpc/index.test.ts +128 -54
- package/src/implementations/http/express-rpc/index.ts +59 -38
- package/build/implementations/http/client/index.d.ts +0 -1
- package/build/implementations/http/client/index.js +0 -2
- package/build/implementations/http/client/index.js.map +0 -1
- package/build/implementations/http/express/example/factories.d.ts +0 -97
- package/build/implementations/http/express/example/factories.js +0 -4
- package/build/implementations/http/express/example/factories.js.map +0 -1
- package/build/implementations/http/express/example/procedures/auth.d.ts +0 -1
- package/build/implementations/http/express/example/procedures/auth.js +0 -22
- package/build/implementations/http/express/example/procedures/auth.js.map +0 -1
- package/build/implementations/http/express/example/procedures/users.d.ts +0 -1
- package/build/implementations/http/express/example/procedures/users.js +0 -30
- package/build/implementations/http/express/example/procedures/users.js.map +0 -1
- package/build/implementations/http/express/example/server.d.ts +0 -3
- package/build/implementations/http/express/example/server.js +0 -49
- package/build/implementations/http/express/example/server.js.map +0 -1
- package/build/implementations/http/express/example/server.test.d.ts +0 -1
- package/build/implementations/http/express/example/server.test.js +0 -110
- package/build/implementations/http/express/example/server.test.js.map +0 -1
- package/build/implementations/http/express/index.d.ts +0 -35
- package/build/implementations/http/express/index.js +0 -75
- package/build/implementations/http/express/index.js.map +0 -1
- package/build/implementations/http/express/index.test.d.ts +0 -1
- package/build/implementations/http/express/index.test.js +0 -329
- package/build/implementations/http/express/index.test.js.map +0 -1
- package/build/implementations/http/express/types.d.ts +0 -17
- package/build/implementations/http/express/types.js +0 -2
- package/build/implementations/http/express/types.js.map +0 -1
|
@@ -3,6 +3,20 @@ import { TProcedureRegistration } from '../../../index.js';
|
|
|
3
3
|
import { RPCConfig, RPCHttpRouteDoc } from '../../types.js';
|
|
4
4
|
import { ExtractContext, ProceduresFactory } from './types.js';
|
|
5
5
|
export type { RPCConfig, RPCHttpRouteDoc };
|
|
6
|
+
export type ExpressRPCAppBuilderConfig = {
|
|
7
|
+
/**
|
|
8
|
+
* An existing Express application instance to use.
|
|
9
|
+
* When provided, ensure to set up necessary middleware (e.g., json/body parser) beforehand.
|
|
10
|
+
* If not provided, a new instance will be created.
|
|
11
|
+
*/
|
|
12
|
+
app?: express.Express;
|
|
13
|
+
/** Optional path prefix for all RPC routes. */
|
|
14
|
+
pathPrefix?: string;
|
|
15
|
+
onRequestStart?: (req: express.Request) => void;
|
|
16
|
+
onRequestEnd?: (req: express.Request, res: express.Response) => void;
|
|
17
|
+
onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void;
|
|
18
|
+
error?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void;
|
|
19
|
+
};
|
|
6
20
|
/**
|
|
7
21
|
* Builder class for creating an Express application with RPC routes.
|
|
8
22
|
*
|
|
@@ -19,35 +33,31 @@ export type { RPCConfig, RPCHttpRouteDoc };
|
|
|
19
33
|
* const docs = rpcApp.docs; // RPC route documentation
|
|
20
34
|
*/
|
|
21
35
|
export declare class ExpressRPCAppBuilder {
|
|
22
|
-
readonly config?:
|
|
23
|
-
/**
|
|
24
|
-
* An existing Express application instance to use.
|
|
25
|
-
* When provided, ensure to set up necessary middleware (e.g., json/body parser) beforehand.
|
|
26
|
-
* If not provided, a new instance will be created.
|
|
27
|
-
*/
|
|
28
|
-
app?: express.Express;
|
|
29
|
-
onRequestStart?: (req: express.Request) => void;
|
|
30
|
-
onRequestEnd?: (req: express.Request, res: express.Response) => void;
|
|
31
|
-
onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void;
|
|
32
|
-
error?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void;
|
|
33
|
-
} | undefined;
|
|
36
|
+
readonly config?: ExpressRPCAppBuilderConfig | undefined;
|
|
34
37
|
/**
|
|
35
38
|
* Constructor for ExpressRPCAppBuilder.
|
|
36
39
|
*
|
|
37
40
|
* @param config
|
|
38
41
|
*/
|
|
39
|
-
constructor(config?:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
constructor(config?: ExpressRPCAppBuilderConfig | undefined);
|
|
43
|
+
/**
|
|
44
|
+
* Generates the RPC route path based on the RPC configuration.
|
|
45
|
+
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
46
|
+
*
|
|
47
|
+
* Example
|
|
48
|
+
* name: ['string', 'string-string', 'string']
|
|
49
|
+
* path: /string/string-string/string/version
|
|
50
|
+
* @param config
|
|
51
|
+
*/
|
|
52
|
+
static makeRPCHttpRoutePath({ config, prefix }: {
|
|
53
|
+
prefix?: string;
|
|
54
|
+
config: RPCConfig;
|
|
55
|
+
}): string;
|
|
56
|
+
/**
|
|
57
|
+
* Instance method wrapper for makeRPCHttpRoutePath that uses the builder's pathPrefix.
|
|
58
|
+
* @param config - The RPC configuration
|
|
59
|
+
*/
|
|
60
|
+
makeRPCHttpRoutePath(config: RPCConfig): string;
|
|
51
61
|
private factories;
|
|
52
62
|
private _app;
|
|
53
63
|
private _docs;
|
|
@@ -69,15 +79,5 @@ export declare class ExpressRPCAppBuilder {
|
|
|
69
79
|
* Generates the RPC HTTP route for the given procedure.
|
|
70
80
|
* @param procedure
|
|
71
81
|
*/
|
|
72
|
-
buildRpcHttpRouteDoc
|
|
73
|
-
/**
|
|
74
|
-
* Generates the RPC route path based on the RPC configuration.
|
|
75
|
-
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
76
|
-
*
|
|
77
|
-
* Example
|
|
78
|
-
* name: ['string', 'string-string', 'string']
|
|
79
|
-
* path: /rpc/string/string-string/string/version
|
|
80
|
-
* @param config
|
|
81
|
-
*/
|
|
82
|
-
makeRPCHttpRoutePath(config: RPCConfig): string;
|
|
82
|
+
private buildRpcHttpRouteDoc;
|
|
83
83
|
}
|
|
@@ -47,6 +47,31 @@ export class ExpressRPCAppBuilder {
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Generates the RPC route path based on the RPC configuration.
|
|
52
|
+
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
53
|
+
*
|
|
54
|
+
* Example
|
|
55
|
+
* name: ['string', 'string-string', 'string']
|
|
56
|
+
* path: /string/string-string/string/version
|
|
57
|
+
* @param config
|
|
58
|
+
*/
|
|
59
|
+
static makeRPCHttpRoutePath({ config, prefix }) {
|
|
60
|
+
const normalizedPrefix = prefix
|
|
61
|
+
? (prefix.startsWith('/') ? prefix : `/${prefix}`)
|
|
62
|
+
: '';
|
|
63
|
+
return `${normalizedPrefix}/${castArray(config.name).map(kebabCase).join('/')}/${String(config.version).trim()}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Instance method wrapper for makeRPCHttpRoutePath that uses the builder's pathPrefix.
|
|
67
|
+
* @param config - The RPC configuration
|
|
68
|
+
*/
|
|
69
|
+
makeRPCHttpRoutePath(config) {
|
|
70
|
+
return ExpressRPCAppBuilder.makeRPCHttpRoutePath({
|
|
71
|
+
config,
|
|
72
|
+
prefix: this.config?.pathPrefix,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
50
75
|
factories = [];
|
|
51
76
|
_app = express();
|
|
52
77
|
_docs = [];
|
|
@@ -113,7 +138,10 @@ export class ExpressRPCAppBuilder {
|
|
|
113
138
|
*/
|
|
114
139
|
buildRpcHttpRouteDoc(procedure) {
|
|
115
140
|
const { config } = procedure;
|
|
116
|
-
const path =
|
|
141
|
+
const path = ExpressRPCAppBuilder.makeRPCHttpRoutePath({
|
|
142
|
+
config,
|
|
143
|
+
prefix: this.config?.pathPrefix,
|
|
144
|
+
});
|
|
117
145
|
const method = 'post'; // RPCs use POST method
|
|
118
146
|
const jsonSchema = {};
|
|
119
147
|
if (config.schema?.params) {
|
|
@@ -128,17 +156,5 @@ export class ExpressRPCAppBuilder {
|
|
|
128
156
|
jsonSchema,
|
|
129
157
|
};
|
|
130
158
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Generates the RPC route path based on the RPC configuration.
|
|
133
|
-
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
134
|
-
*
|
|
135
|
-
* Example
|
|
136
|
-
* name: ['string', 'string-string', 'string']
|
|
137
|
-
* path: /rpc/string/string-string/string/version
|
|
138
|
-
* @param config
|
|
139
|
-
*/
|
|
140
|
-
makeRPCHttpRoutePath(config) {
|
|
141
|
-
return `/rpc/${castArray(config.name).map(kebabCase).join('/')}/${String(config.version).trim()}`;
|
|
142
|
-
}
|
|
143
159
|
}
|
|
144
160
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/implementations/http/express-rpc/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAG7C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/implementations/http/express-rpc/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
// --------------------------------------------------------------------------
|
|
@@ -22,7 +22,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/1').send({ message: 'hello' });
|
|
26
26
|
expect(res.status).toBe(200);
|
|
27
27
|
expect(res.body).toEqual({ message: 'hello' });
|
|
28
28
|
});
|
|
@@ -36,7 +36,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/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', { name: '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/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', { name: 'test', version: 1 }, async () => ({ ok: true }));
|
|
75
|
+
builder.register(RPC, () => ({}));
|
|
76
|
+
const app = builder.build();
|
|
77
|
+
const res = await request(app).post('/custom/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', { name: 'test', version: 1 }, async () => ({ ok: true }));
|
|
84
|
+
builder.register(RPC, () => ({}));
|
|
85
|
+
const app = builder.build();
|
|
86
|
+
const res = await request(app).post('/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', { name: 'test', version: 1 }, async () => ({}));
|
|
93
|
+
builder.register(RPC, () => ({}));
|
|
94
|
+
builder.build();
|
|
95
|
+
expect(builder.docs[0].path).toBe('/api/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', { name: 'users', version: 1 }, async () => ({ users: [] }));
|
|
101
|
+
builder.register(RPC, () => ({}));
|
|
102
|
+
const app = builder.build();
|
|
103
|
+
const res = await request(app).post('/rpc/users/1').send({});
|
|
104
|
+
expect(res.status).toBe(200);
|
|
105
|
+
expect(builder.docs[0].path).toBe('/rpc/users/1');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// --------------------------------------------------------------------------
|
|
58
109
|
// Lifecycle Hooks Tests
|
|
59
110
|
// --------------------------------------------------------------------------
|
|
60
111
|
describe('lifecycle hooks', () => {
|
|
@@ -65,10 +116,10 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
65
116
|
RPC.Create('Test', { name: '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/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/1');
|
|
72
123
|
});
|
|
73
124
|
test('onRequestEnd is called after response finishes', async () => {
|
|
74
125
|
const onRequestEnd = vi.fn();
|
|
@@ -77,7 +128,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
77
128
|
RPC.Create('Test', { name: '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/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);
|
|
@@ -89,7 +140,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
89
140
|
RPC.Create('Test', { name: '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/1').send({});
|
|
93
144
|
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
94
145
|
expect(onSuccess.mock.calls[0][0]).toHaveProperty('name', 'Test');
|
|
95
146
|
});
|
|
@@ -102,7 +153,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
102
153
|
});
|
|
103
154
|
builder.register(RPC, () => ({}));
|
|
104
155
|
const app = builder.build();
|
|
105
|
-
await request(app).post('/
|
|
156
|
+
await request(app).post('/test/1').send({});
|
|
106
157
|
expect(onSuccess).not.toHaveBeenCalled();
|
|
107
158
|
});
|
|
108
159
|
test('hooks execute in correct order: start → handler → success → end', async () => {
|
|
@@ -119,7 +170,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
119
170
|
});
|
|
120
171
|
builder.register(RPC, () => ({}));
|
|
121
172
|
const app = builder.build();
|
|
122
|
-
await request(app).post('/
|
|
173
|
+
await request(app).post('/test/1').send({});
|
|
123
174
|
expect(order).toEqual(['start', 'handler', 'success', 'end']);
|
|
124
175
|
});
|
|
125
176
|
});
|
|
@@ -138,7 +189,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/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);
|
|
@@ -154,7 +205,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/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');
|
|
@@ -169,7 +220,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/1').send({});
|
|
173
224
|
// Unhandled exceptions are caught and returned as error response
|
|
174
225
|
expect(res.body).toHaveProperty('error');
|
|
175
226
|
});
|
|
@@ -202,8 +253,8 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/1').send({});
|
|
257
|
+
const privateRes = await request(app).post('/private/1').send({});
|
|
207
258
|
expect(publicRes.body).toEqual({ isPublic: true });
|
|
208
259
|
expect(privateRes.body).toEqual({ isPrivate: true });
|
|
209
260
|
});
|
|
@@ -216,7 +267,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/1').send({});
|
|
220
271
|
expect(res.body).toEqual({ id: 'req-123' });
|
|
221
272
|
});
|
|
222
273
|
test('factoryContext can be async function', async () => {
|
|
@@ -230,7 +281,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/1').send({});
|
|
234
285
|
expect(factoryContext).toHaveBeenCalledTimes(1);
|
|
235
286
|
});
|
|
236
287
|
test('factoryContext function receives Express request object', async () => {
|
|
@@ -244,7 +295,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/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');
|
|
@@ -261,8 +312,8 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
261
312
|
RPC.Create('MethodTwo', { name: '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/1').send({});
|
|
316
|
+
const res2 = await request(app).post('/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 });
|
|
@@ -283,8 +334,8 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
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/1');
|
|
338
|
+
expect(builder.docs[1].path).toBe('/nested/method/2');
|
|
288
339
|
});
|
|
289
340
|
test('passes request body to handler as params', async () => {
|
|
290
341
|
const builder = new ExpressRPCAppBuilder();
|
|
@@ -292,7 +343,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
292
343
|
RPC.Create('Echo', { name: '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/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 () => {
|
|
@@ -301,7 +352,7 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
301
352
|
RPC.Create('Test', { name: '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/1');
|
|
305
356
|
expect(res.status).toBe(404);
|
|
306
357
|
});
|
|
307
358
|
});
|
|
@@ -313,36 +364,36 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
313
364
|
beforeEach(() => {
|
|
314
365
|
builder = new ExpressRPCAppBuilder();
|
|
315
366
|
});
|
|
316
|
-
test("simple string: 'users' → /
|
|
367
|
+
test("simple string: 'users' → /users/1", () => {
|
|
317
368
|
const path = builder.makeRPCHttpRoutePath({ name: 'users', version: 1 });
|
|
318
|
-
expect(path).toBe('/
|
|
369
|
+
expect(path).toBe('/users/1');
|
|
319
370
|
});
|
|
320
|
-
test("array name: ['users', 'get-by-id'] → /
|
|
371
|
+
test("array name: ['users', 'get-by-id'] → /users/get-by-id/1", () => {
|
|
321
372
|
const path = builder.makeRPCHttpRoutePath({ name: ['users', 'get-by-id'], version: 1 });
|
|
322
|
-
expect(path).toBe('/
|
|
373
|
+
expect(path).toBe('/users/get-by-id/1');
|
|
323
374
|
});
|
|
324
|
-
test("camelCase: 'getUserById' → /
|
|
375
|
+
test("camelCase: 'getUserById' → /get-user-by-id/1", () => {
|
|
325
376
|
const path = builder.makeRPCHttpRoutePath({ name: 'getUserById', version: 1 });
|
|
326
|
-
expect(path).toBe('/
|
|
377
|
+
expect(path).toBe('/get-user-by-id/1');
|
|
327
378
|
});
|
|
328
|
-
test("PascalCase: 'GetUserById' → /
|
|
379
|
+
test("PascalCase: 'GetUserById' → /get-user-by-id/1", () => {
|
|
329
380
|
const path = builder.makeRPCHttpRoutePath({ name: 'GetUserById', version: 1 });
|
|
330
|
-
expect(path).toBe('/
|
|
381
|
+
expect(path).toBe('/get-user-by-id/1');
|
|
331
382
|
});
|
|
332
383
|
test('version number included in path', () => {
|
|
333
384
|
const pathV1 = builder.makeRPCHttpRoutePath({ name: 'test', version: 1 });
|
|
334
385
|
const pathV2 = builder.makeRPCHttpRoutePath({ name: 'test', version: 2 });
|
|
335
386
|
const pathV99 = builder.makeRPCHttpRoutePath({ name: 'test', version: 99 });
|
|
336
|
-
expect(pathV1).toBe('/
|
|
337
|
-
expect(pathV2).toBe('/
|
|
338
|
-
expect(pathV99).toBe('/
|
|
387
|
+
expect(pathV1).toBe('/test/1');
|
|
388
|
+
expect(pathV2).toBe('/test/2');
|
|
389
|
+
expect(pathV99).toBe('/test/99');
|
|
339
390
|
});
|
|
340
391
|
test('handles mixed case in array segments', () => {
|
|
341
392
|
const path = builder.makeRPCHttpRoutePath({
|
|
342
393
|
name: ['UserModule', 'getActiveUsers'],
|
|
343
394
|
version: 1,
|
|
344
395
|
});
|
|
345
|
-
expect(path).toBe('/
|
|
396
|
+
expect(path).toBe('/user-module/get-active-users/1');
|
|
346
397
|
});
|
|
347
398
|
});
|
|
348
399
|
// --------------------------------------------------------------------------
|
|
@@ -357,10 +408,11 @@ 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', { name: '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/1');
|
|
364
416
|
expect(doc.method).toBe('post');
|
|
365
417
|
expect(doc.jsonSchema.body).toBeDefined();
|
|
366
418
|
expect(doc.jsonSchema.response).toBeDefined();
|
|
@@ -368,15 +420,17 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
368
420
|
test('omits body schema when no params defined', () => {
|
|
369
421
|
const RPC = Procedures();
|
|
370
422
|
RPC.Create('NoParams', { name: 'no-params', version: 1 }, async () => ({ ok: true }));
|
|
371
|
-
|
|
372
|
-
|
|
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
430
|
RPC.Create('NoReturn', { name: 'no-return', version: 1, schema: { params: v.object({ x: v.number() }) } }, async () => ({}));
|
|
378
|
-
|
|
379
|
-
|
|
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
|
});
|
|
@@ -384,9 +438,9 @@ describe('ExpressRPCAppBuilder', () => {
|
|
|
384
438
|
const RPC = Procedures();
|
|
385
439
|
RPC.Create('Test1', { name: 't1', version: 1 }, async () => ({}));
|
|
386
440
|
RPC.Create('Test2', { name: 't2', version: 2 }, async () => ({}));
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
441
|
+
builder.register(RPC, () => ({}));
|
|
442
|
+
builder.build();
|
|
443
|
+
builder.docs.forEach((doc) => {
|
|
390
444
|
expect(doc.method).toBe('post');
|
|
391
445
|
});
|
|
392
446
|
});
|
|
@@ -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/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/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/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/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/1');
|
|
512
|
+
expect(paths).toContain('/health/1');
|
|
513
|
+
expect(paths).toContain('/users/profile/1');
|
|
514
|
+
expect(paths).toContain('/users/profile/2');
|
|
461
515
|
// Verify hooks were called
|
|
462
516
|
expect(events).toContain('request-start');
|
|
463
517
|
expect(events).toContain('success:GetVersion');
|