rpc-express-toolkit 2.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.
@@ -0,0 +1,41 @@
1
+ const express = require('express');
2
+ const { RpcEndpoint } = require('../src/index');
3
+
4
+ const app = express();
5
+
6
+ app.use(express.json());
7
+
8
+ const context = { user: 'admin' };
9
+
10
+ // Attach to the app router at POST /api endpoint
11
+ const rpc = new RpcEndpoint(app, context);
12
+
13
+ rpc.addMethod('greet', (req, ctx, params) => {
14
+ const { name } = params;
15
+ return `Hello, ${name}!`;
16
+ });
17
+
18
+ rpc.addMethod('getTime', (req, ctx, params) => {
19
+ return new Date().toISOString();
20
+ });
21
+
22
+ rpc.addMethod('invalid-token', (req, ctx, params) => {
23
+ // Example: Validate the token from request headers
24
+ const token = req.headers.authorization;
25
+ if (!token || !isValidToken(token)) { // Replace `isValidToken` with your validation logic
26
+ throw new Error('Invalid or missing token');
27
+ }
28
+ return 'OK';
29
+ });
30
+
31
+ // Example token validation function (replace with your actual logic)
32
+ function isValidToken(token) {
33
+ // Simple example: Check if the token matches a predefined value
34
+ const expectedToken = 'my-secret-token'; // Replace with your actual token handling logic
35
+ return token === expectedToken;
36
+ }
37
+
38
+ const PORT = 3000;
39
+ app.listen(PORT, () => {
40
+ console.log(`Server listening on port ${PORT}`);
41
+ });
@@ -0,0 +1,114 @@
1
+ const express = require('express');
2
+ const { RpcEndpoint, RpcClient } = require('../src/index');
3
+
4
+ // === SERVER SETUP ===
5
+ const app = express();
6
+ app.use(express.json());
7
+
8
+ const context = {
9
+ database: new Map(),
10
+ userId: 'system'
11
+ };
12
+
13
+ // Create JSON-RPC endpoint with clean names
14
+ const rpc = new RpcEndpoint(app, context);
15
+
16
+ // Add some methods
17
+ rpc.addMethod('saveData', (req, ctx, params) => {
18
+ const { key, value } = params;
19
+ ctx.database.set(key, value);
20
+ return { success: true, message: `Data saved with key: ${key}` };
21
+ });
22
+
23
+ rpc.addMethod('getData', (req, ctx, params) => {
24
+ const { key } = params;
25
+ const value = ctx.database.get(key);
26
+ if (value === undefined) {
27
+ throw new Error(`Key '${key}' not found`);
28
+ }
29
+ return { key, value };
30
+ });
31
+
32
+ rpc.addMethod('getAllData', (req, ctx) => {
33
+ return Array.from(ctx.database.entries()).map(([key, value]) => ({ key, value }));
34
+ });
35
+
36
+ rpc.addMethod('testBigInt', (req, ctx, params) => {
37
+ const bigNumber = BigInt(params.number || '123456789012345678901234567890');
38
+ return {
39
+ original: params.number,
40
+ bigint: bigNumber,
41
+ doubled: bigNumber * 2n,
42
+ timestamp: new Date()
43
+ };
44
+ });
45
+
46
+ // Start server
47
+ const PORT = process.env.PORT || 3000;
48
+ const server = app.listen(PORT, () => {
49
+ console.log(`šŸš€ JSON-RPC Server running on http://localhost:${PORT}`);
50
+ console.log(`šŸ“” Endpoint: http://localhost:${PORT}/api`);
51
+
52
+ // === CLIENT DEMO ===
53
+ setTimeout(async () => {
54
+ console.log('\nšŸ”§ Testing client...');
55
+
56
+ const client = new RpcClient(`http://localhost:${PORT}/api`);
57
+
58
+ try {
59
+ // Test 1: Save some data
60
+ console.log('šŸ“ Saving data...');
61
+ const saveResult = await client.call('saveData', { key: 'test', value: 'Hello World!' });
62
+ console.log('āœ… Save result:', saveResult);
63
+
64
+ // Test 2: Get data back
65
+ console.log('\nšŸ“– Getting data...');
66
+ const getData = await client.call('getData', { key: 'test' });
67
+ console.log('āœ… Get result:', getData);
68
+
69
+ // Test 3: Test BigInt handling
70
+ console.log('\nšŸ”¢ Testing BigInt...');
71
+ const bigIntResult = await client.call('testBigInt', { number: '999999999999999999999999999999' });
72
+ console.log('āœ… BigInt result:', bigIntResult);
73
+ console.log(' - Original BigInt type:', typeof bigIntResult.bigint);
74
+ console.log(' - Doubled BigInt type:', typeof bigIntResult.doubled);
75
+ console.log(' - Timestamp type:', typeof bigIntResult.timestamp);
76
+
77
+ // Test 4: Get all data
78
+ console.log('\nšŸ“Š Getting all data...');
79
+ const allData = await client.call('getAllData');
80
+ console.log('āœ… All data:', allData);
81
+
82
+ // Test 5: Test error handling
83
+ console.log('\nāŒ Testing error handling...');
84
+ try {
85
+ await client.call('getData', { key: 'nonexistent' });
86
+ } catch (error) {
87
+ console.log('āœ… Error caught correctly:', error.message);
88
+ }
89
+
90
+ console.log('\n✨ All tests completed successfully!');
91
+
92
+ } catch (error) {
93
+ console.error('āŒ Client error:', error);
94
+ }
95
+
96
+ // Close server after demo
97
+ setTimeout(() => {
98
+ server.close(() => {
99
+ console.log('\nšŸ‘‹ Server closed');
100
+ process.exit(0);
101
+ });
102
+ }, 1000);
103
+
104
+ }, 1000);
105
+ });
106
+
107
+ // Handle graceful shutdown
108
+ process.on('SIGINT', () => {
109
+ console.log('\nšŸ›‘ Shutting down server...');
110
+ server.close(() => {
111
+ console.log('šŸ‘‹ Server closed');
112
+ process.exit(0);
113
+ });
114
+ });
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "rpc-express-toolkit",
3
+ "version": "2.0.0",
4
+ "description": "Enterprise-ready JSON-RPC 2.0 toolkit for Express.js with simplified APIs, structured logging, middleware system, schema validation, and comprehensive BigInt/Date support.",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "files": [
8
+ "src/",
9
+ "examples/",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "scripts": {
14
+ "test": "jest",
15
+ "lint": "eslint src/ examples/",
16
+ "lint:fix": "eslint src/ examples/ --fix",
17
+ "clean": "node -e \"require('fs').existsSync('dist') && require('fs').rmSync('dist', {recursive: true})\"",
18
+ "build": "npm run build:minify",
19
+ "build:minify": "npm run build:js && npm run build:mjs",
20
+ "build:js": "terser src/clients/rpc-client.js -o src/clients/rpc-client.min.js --compress --mangle --comments false",
21
+ "build:mjs": "terser src/clients/rpc-client.mjs -o src/clients/rpc-client.min.mjs --compress --mangle --comments false --module",
22
+ "prepublishOnly": "npm run build",
23
+ "precommit": "npm run lint && npm test"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/n-car/rpc-express-toolkit.git"
28
+ },
29
+ "keywords": [
30
+ "json-rpc",
31
+ "express",
32
+ "toolkit",
33
+ "rpc",
34
+ "server",
35
+ "client",
36
+ "middleware",
37
+ "validation",
38
+ "bigint",
39
+ "enterprise",
40
+ "nodejs"
41
+ ],
42
+ "author": "Nicola Carpanese <nicola.carpanese@gmail.com>",
43
+ "license": "MIT",
44
+ "bugs": {
45
+ "url": "https://github.com/n-car/rpc-express-toolkit/issues"
46
+ },
47
+ "homepage": "https://github.com/n-car/rpc-express-toolkit#readme",
48
+ "engines": {
49
+ "node": ">=14.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "eslint": "^8.57.1",
53
+ "eslint-config-airbnb-base": "^15.0.0",
54
+ "eslint-config-prettier": "^8.8.0",
55
+ "eslint-plugin-import": "^2.31.0",
56
+ "eslint-plugin-prettier": "^4.2.1",
57
+ "husky": "^8.0.0",
58
+ "jest": "^29.7.0",
59
+ "lint-staged": "^13.0.0",
60
+ "prettier": "^3.4.2",
61
+ "supertest": "^6.3.3",
62
+ "terser": "^5.43.1"
63
+ },
64
+ "dependencies": {
65
+ "ajv": "^8.12.0",
66
+ "ajv-formats": "^2.1.1",
67
+ "express": "^4.21.2",
68
+ "nested-error-stacks": "^2.1.1"
69
+ },
70
+ "optionalDependencies": {
71
+ "node-fetch": "^2.7.0"
72
+ },
73
+ "contributors": [
74
+ {
75
+ "name": "Nicola Carpanese",
76
+ "email": "nicola.carpanese@gmail.com",
77
+ "url": "https://github.com/n-car"
78
+ }
79
+ ],
80
+ "husky": {
81
+ "hooks": {
82
+ "pre-commit": "lint-staged"
83
+ }
84
+ },
85
+ "lint-staged": {
86
+ "src/**/*.js": [
87
+ "eslint --fix",
88
+ "prettier --write"
89
+ ],
90
+ "examples/**/*.js": [
91
+ "eslint --fix",
92
+ "prettier --write"
93
+ ]
94
+ }
95
+ }
package/src/batch.js ADDED
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Batch request handler for JSON-RPC 2.0
5
+ */
6
+ class BatchHandler {
7
+ constructor(endpoint) {
8
+ this.endpoint = endpoint;
9
+ }
10
+
11
+ /**
12
+ * Check if request is a batch request
13
+ * @param {any} body
14
+ * @returns {boolean}
15
+ */
16
+ isBatchRequest(body) {
17
+ return Array.isArray(body) && body.length > 0;
18
+ }
19
+
20
+ /**
21
+ * Validate batch request
22
+ * @param {Array} batch
23
+ * @returns {Object}
24
+ */
25
+ validateBatch(batch) {
26
+ if (!Array.isArray(batch)) {
27
+ return {
28
+ valid: false,
29
+ error: { code: -32600, message: 'Invalid Request: Batch must be an array' }
30
+ };
31
+ }
32
+
33
+ if (batch.length === 0) {
34
+ return {
35
+ valid: false,
36
+ error: { code: -32600, message: 'Invalid Request: Batch cannot be empty' }
37
+ };
38
+ }
39
+
40
+ // Check for duplicate IDs in the batch
41
+ const ids = batch.map(req => req.id).filter(id => id !== undefined && id !== null);
42
+ const uniqueIds = new Set(ids);
43
+
44
+ if (ids.length !== uniqueIds.size) {
45
+ return {
46
+ valid: false,
47
+ error: { code: -32600, message: 'Invalid Request: Duplicate IDs in batch' }
48
+ };
49
+ }
50
+
51
+ return { valid: true };
52
+ }
53
+
54
+ /**
55
+ * Process a batch of JSON-RPC requests
56
+ * @param {Array} batch
57
+ * @param {Object} req
58
+ * @param {Object} res
59
+ * @param {any} context
60
+ * @returns {Promise<Array>}
61
+ */
62
+ async processBatch(batch, req, res, context) {
63
+ const validation = this.validateBatch(batch);
64
+ if (!validation.valid) {
65
+ return [{
66
+ jsonrpc: '2.0',
67
+ id: null,
68
+ error: validation.error
69
+ }];
70
+ }
71
+
72
+ // Process all requests in parallel
73
+ const promises = batch.map(async (request, index) => {
74
+ try {
75
+ return await this.processSingleRequest(request, req, context, index);
76
+ } catch (error) {
77
+ return {
78
+ jsonrpc: '2.0',
79
+ id: request.id || null,
80
+ error: {
81
+ code: error.code || -32603,
82
+ message: error.message || 'Internal error',
83
+ data: { batchIndex: index }
84
+ }
85
+ };
86
+ }
87
+ });
88
+
89
+ const results = await Promise.all(promises);
90
+
91
+ // Filter out notifications (requests without id)
92
+ return results.filter(result => result !== null);
93
+ }
94
+
95
+ /**
96
+ * Process a single request within a batch
97
+ * @param {Object} request
98
+ * @param {Object} req
99
+ * @param {any} context
100
+ * @param {number} batchIndex
101
+ * @returns {Promise<Object|null>}
102
+ */
103
+ async processSingleRequest(request, req, context, batchIndex) {
104
+ const { jsonrpc, method, params, id } = request;
105
+
106
+ // Validate JSON-RPC 2.0 request structure
107
+ if (jsonrpc !== "2.0") {
108
+ return {
109
+ jsonrpc: '2.0',
110
+ id: id || null,
111
+ error: {
112
+ code: -32600,
113
+ message: `Invalid Request: 'jsonrpc' must be '2.0'`,
114
+ data: { batchIndex }
115
+ }
116
+ };
117
+ }
118
+
119
+ if (typeof method !== "string") {
120
+ return {
121
+ jsonrpc: '2.0',
122
+ id: id || null,
123
+ error: {
124
+ code: -32600,
125
+ message: `Invalid Request: 'method' must be a string`,
126
+ data: { batchIndex }
127
+ }
128
+ };
129
+ }
130
+
131
+ const handler = this.endpoint.methods[method];
132
+ if (!handler) {
133
+ return {
134
+ jsonrpc: '2.0',
135
+ id: id || null,
136
+ error: {
137
+ code: -32601,
138
+ message: `Method "${method}" not found`,
139
+ data: { batchIndex }
140
+ }
141
+ };
142
+ }
143
+
144
+ // If no id is provided, this is a notification - don't return response
145
+ const isNotification = id === undefined || id === null;
146
+
147
+ try {
148
+ // Execute middleware
149
+ let middlewareContext = {
150
+ req,
151
+ res: null, // No response object for batch items
152
+ method,
153
+ params,
154
+ context,
155
+ batchIndex,
156
+ isNotification
157
+ };
158
+
159
+ if (this.endpoint.middleware) {
160
+ middlewareContext = await this.endpoint.middleware.execute('beforeCall', middlewareContext);
161
+ }
162
+
163
+ // Execute the handler
164
+ const result = await Promise.resolve(handler(req, context, middlewareContext.params));
165
+
166
+ // Execute after middleware
167
+ if (this.endpoint.middleware) {
168
+ middlewareContext.result = result;
169
+ await this.endpoint.middleware.execute('afterCall', middlewareContext);
170
+ }
171
+
172
+ // For notifications, return null (will be filtered out)
173
+ if (isNotification) {
174
+ return null;
175
+ }
176
+
177
+ // Serialize the result
178
+ const safeResult = this.endpoint.serializeBigIntsAndDates(result);
179
+
180
+ return {
181
+ jsonrpc: '2.0',
182
+ id,
183
+ result: safeResult
184
+ };
185
+
186
+ } catch (error) {
187
+ // Execute error middleware
188
+ if (this.endpoint.middleware) {
189
+ try {
190
+ await this.endpoint.middleware.execute('onError', {
191
+ ...middlewareContext,
192
+ error,
193
+ batchIndex
194
+ });
195
+ } catch (middlewareError) {
196
+ // Ignore middleware errors in error handling
197
+ }
198
+ }
199
+
200
+ // For notifications, return null even on error
201
+ if (isNotification) {
202
+ return null;
203
+ }
204
+
205
+ return {
206
+ jsonrpc: '2.0',
207
+ id: id || null,
208
+ error: {
209
+ code: error.code || -32603,
210
+ message: error.message || 'Internal error',
211
+ data: { batchIndex }
212
+ }
213
+ };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Get batch statistics
219
+ * @param {Array} batch
220
+ * @returns {Object}
221
+ */
222
+ getBatchStats(batch) {
223
+ const notifications = batch.filter(req => req.id === undefined || req.id === null).length;
224
+ const requests = batch.length - notifications;
225
+ const methods = [...new Set(batch.map(req => req.method))];
226
+
227
+ return {
228
+ total: batch.length,
229
+ requests,
230
+ notifications,
231
+ uniqueMethods: methods.length,
232
+ methods
233
+ };
234
+ }
235
+ }
236
+
237
+ module.exports = BatchHandler;