rpc-express-toolkit 2.3.3 → 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/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
4
  [![npm version](https://img.shields.io/npm/v/rpc-express-toolkit.svg)](https://www.npmjs.com/package/rpc-express-toolkit)
5
- [![Build Status](https://github.com/n-car/rpc-express-toolkit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/n-car/rpc-express-toolkit/actions/workflows/ci.yml)
6
- [![Coverage Status](https://coveralls.io/repos/github/n-car/rpc-express-toolkit/badge.svg?branch=main)](https://coveralls.io/github/n-car/rpc-express-toolkit?branch=main)
5
+ <!-- [![Build Status](https://github.com/n-car/rpc-express-toolkit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/n-car/rpc-express-toolkit/actions/workflows/ci.yml) -->
6
+ <!-- [![Coverage Status](https://coveralls.io/repos/github/n-car/rpc-express-toolkit/badge.svg?branch=main)](https://coveralls.io/github/n-car/rpc-express-toolkit?branch=main) -->
7
7
 
8
8
  An enterprise-ready JSON-RPC 2.0 toolkit for Express.js applications with simplified APIs, structured logging, middleware system, schema validation, batch processing, and comprehensive BigInt/Date serialization support.
9
9
 
@@ -66,12 +66,13 @@ const express = require('express');
66
66
  const { RpcEndpoint } = require('rpc-express-toolkit');
67
67
 
68
68
  const app = express();
69
+ // Configure JSON middleware yourself
69
70
  app.use(express.json());
70
71
 
71
72
  // Context object to pass to method handlers
72
73
  const context = { database: db, config: config };
73
74
 
74
- // Create JSON-RPC server with clean API
75
+ // Create JSON-RPC server (validates JSON middleware compatibility)
75
76
  const rpc = new RpcEndpoint(app, context);
76
77
 
77
78
  // Add simple methods
@@ -173,6 +174,22 @@ const rpc = new RpcEndpoint(app, context, {
173
174
  removeAdditional: true
174
175
  },
175
176
 
177
+ // Safe serialization options
178
+ safeStringEnabled: true, // Add 'S:' prefix to strings (default: true)
179
+ safeDateEnabled: true, // Add 'D:' prefix to dates (default: true)
180
+ warnOnUnsafeString: true, // Warn when BigInt found with safeStringEnabled=false (default: true)
181
+ warnOnUnsafeDate: true, // Warn when Date found with safeDateEnabled=false (default: true)
182
+
183
+ // Other middleware options
184
+ cors: true,
185
+ rateLimit: { windowMs: 15 * 60 * 1000, max: 100 },
186
+ jsonOptions: { // Options to pass to express.json()
187
+ limit: '10mb', // Body size limit (default: uses maxBodySize)
188
+ strict: true, // Only parse arrays and objects
189
+ type: 'application/json' // Content-Type to parse
190
+ },
191
+ warnOnUnsafeDate: true, // Warn when Date found with safeDateEnabled=false (default: true)
192
+
176
193
  // Feature flags
177
194
  healthCheck: true,
178
195
  metrics: true,
@@ -214,6 +231,67 @@ rpc.addMethod('getUser', {
214
231
 
215
232
  ### Middleware System
216
233
 
234
+ **JSON Middleware Validation**
235
+
236
+ RpcEndpoint validates that your Express router can handle JSON parsing properly:
237
+
238
+ ```javascript
239
+ const express = require('express');
240
+ const { RpcEndpoint } = require('rpc-express-toolkit');
241
+
242
+ const app = express();
243
+ app.use(express.json()); // You must configure JSON middleware
244
+
245
+ try {
246
+ // RpcEndpoint validates JSON middleware compatibility during construction
247
+ const rpc = new RpcEndpoint(app, context);
248
+ console.log('✅ JSON middleware validation passed');
249
+ } catch (error) {
250
+ console.error('❌ JSON middleware validation failed:', error.message);
251
+ // This router cannot handle JSON-RPC requests properly.
252
+ }
253
+ ```
254
+
255
+ **Benefits of JSON Middleware Validation:**
256
+
257
+ - 🛡️ **Early Error Detection**: Catches JSON parsing issues at startup, not runtime
258
+ - 🎯 **Express Version Compatibility**: Ensures Express ≥4.16.0 for `express.json()` support
259
+ - 📋 **Clear Error Messages**: Provides specific solutions for configuration problems
260
+ - 🔍 **Debug Information**: Logs validation status for troubleshooting
261
+
262
+ **JSON Middleware Validation:**
263
+
264
+ The system validates JSON middleware functionality during RpcEndpoint construction:
265
+
266
+ **Customizing JSON Options:**
267
+ ```javascript
268
+ const rpc = new RpcEndpoint(app, context, {
269
+ endpoint: '/api',
270
+ jsonOptions: {
271
+ limit: '50mb', // Increase body size limit
272
+ strict: false, // Allow non-objects/arrays
273
+ type: ['application/json', 'text/plain'] // Accept multiple content types
274
+ }
275
+ });
276
+ ```
277
+
278
+ **Common JSON Validation Errors:**
279
+
280
+ ```javascript
281
+ // Error: Express version too old
282
+ // Solution: Update Express to ≥4.16.0
283
+ npm install express@latest
284
+
285
+ // Error: No JSON middleware configured
286
+ // Solution: Add JSON middleware to your router
287
+ app.use(express.json());
288
+
289
+ // Error: body-parser required for older Express
290
+ // Solution: Install and configure body-parser
291
+ npm install body-parser
292
+ app.use(require('body-parser').json());
293
+ ```
294
+
217
295
  ## API Reference
218
296
 
219
297
  ### RpcEndpoint Class
@@ -505,6 +583,144 @@ const bigNum = await client.call('getBigNumber'); // Returns BigInt
505
583
  const date = await client.call('getCurrentTime'); // Returns Date object
506
584
  ```
507
585
 
586
+ ### Handling Numeric Strings with Leading Zeros
587
+
588
+ When working with numeric strings that need to preserve leading zeros (like IDs, codes, or references), be aware of BigInt auto-conversion behavior:
589
+
590
+ ```javascript
591
+ // ✅ CORRECT: Numeric strings without 'n' suffix are preserved
592
+ await client.call('processCode', { code: '0123456' });
593
+ // → Server receives: '0123456' (string)
594
+ // → Client receives: '0123456' (string) ✅
595
+
596
+ // ❌ POTENTIAL ISSUE: Strings ending with 'n' are converted to BigInt
597
+ await client.call('processRef', { ref: '012312031203120301230123123n' });
598
+ // → Server receives: '012312031203120301230123123n' (string)
599
+ // → Client receives: 12312031203120301230123123n (BigInt, leading zero lost!) ❌
600
+
601
+ // ✅ SOLUTIONS for strings that should remain strings:
602
+
603
+ // Solution 1: Use schema validation to force string type
604
+ rpc.addMethod('processRef', (req, ctx, params) => {
605
+ // params.ref will always be a string due to schema validation
606
+ return { processed: params.ref };
607
+ }, {
608
+ type: 'object',
609
+ properties: {
610
+ ref: { type: 'string' } // Forces string type, prevents BigInt conversion
611
+ }
612
+ });
613
+
614
+ // Solution 2: Use escape prefix for problematic strings
615
+ await client.call('processRef', { ref: 'S:012312031203120301230123123n' });
616
+ // Server strips prefix: const actualRef = params.ref.replace(/^S:/, '');
617
+
618
+ // Solution 3: Use different suffix
619
+ await client.call('processRef', { ref: '012312031203120301230123123_id' });
620
+ // → Remains string: '012312031203120301230123123_id' ✅
621
+ ```
622
+
623
+ **Note:** Only strings explicitly ending with 'n' and containing only digits are auto-converted to BigInt. This preserves leading zeros for most numeric strings while maintaining BigInt functionality for intentional use.
624
+
625
+ ### Safe Serialization with Prefixes
626
+
627
+ The toolkit provides safe serialization options to prevent confusion between strings, BigInt values, and dates:
628
+
629
+ ```javascript
630
+ // Enable safe serialization (default: true for both)
631
+ const rpc = new RpcEndpoint(app, context, {
632
+ safeStringEnabled: true, // Strings get 'S:' prefix
633
+ safeDateEnabled: true // Dates get 'D:' prefix
634
+ });
635
+
636
+ // Example serialization with safe options enabled:
637
+ rpc.addMethod('getComplexData', () => {
638
+ return {
639
+ message: "Hello World", // → "S:Hello World"
640
+ timestamp: new Date(), // → "D:2023-06-15T10:30:00.000Z"
641
+ count: 42n, // → "42n"
642
+ value: 123 // → 123 (unchanged)
643
+ };
644
+ });
645
+
646
+ // Client automatically deserializes based on server headers:
647
+ const client = new RpcClient('http://localhost:3000/api');
648
+ const result = await client.call('getComplexData');
649
+ // result.message: "Hello World" (string, prefix removed)
650
+ // result.timestamp: Date object
651
+ // result.count: 42n (BigInt)
652
+ // result.value: 123 (number)
653
+ ```
654
+
655
+ #### Cross-Configuration Support
656
+
657
+ Client and server can have different safe options - they communicate automatically via HTTP headers:
658
+
659
+ ```javascript
660
+ // Server with safe dates disabled
661
+ const rpc = new RpcEndpoint(app, context, {
662
+ safeStringEnabled: true, // S: prefix for strings
663
+ safeDateEnabled: false // No prefix for dates
664
+ });
665
+
666
+ // Client with safe dates enabled
667
+ const client = new RpcClient('http://localhost:3000/api', {
668
+ safeStringEnabled: true, // S: prefix for strings
669
+ safeDateEnabled: true // D: prefix for dates
670
+ });
671
+
672
+ // Data flows correctly despite different configurations:
673
+ // Client sends: { date: "D:2023-01-01T00:00:00.000Z" }
674
+ // Server receives: Date object (auto-deserialized using client's headers)
675
+ // Server responds: { date: "2023-01-01T00:00:00.000Z" } (no prefix, per server config)
676
+ // Client receives: Date object (auto-deserialized using server's headers)
677
+ ```
678
+
679
+ #### Benefits of Safe Prefixes
680
+
681
+ - **Prevents ambiguity:** `"123n"` vs `"S:123n"` (string) vs `123n` (BigInt)
682
+ - **Cross-configuration:** Different client/server settings work seamlessly
683
+ - **Compact:** Short prefixes (`S:`, `D:`) minimize payload overhead
684
+ - **Automatic:** No manual intervention required, handled by headers
685
+ - **Backward compatible:** Can be disabled if needed
686
+
687
+ ### Disabling Serialization Warnings
688
+
689
+ When safe options are disabled (`safeStringEnabled: false` or `safeDateEnabled: false`), the toolkit warns about potential data type confusion. You can disable these warnings to reduce console noise:
690
+
691
+ ```javascript
692
+ // Disable all serialization warnings
693
+ const rpc = new RpcEndpoint(app, context, {
694
+ safeStringEnabled: false, // Disable safe strings
695
+ safeDateEnabled: false, // Disable safe dates
696
+ warnOnUnsafeString: false, // Disable BigInt warnings
697
+ warnOnUnsafeDate: false // Disable Date warnings
698
+ });
699
+
700
+ // Disable only specific warnings
701
+ const rpc = new RpcEndpoint(app, context, {
702
+ safeStringEnabled: false, // Still disabled
703
+ safeDateEnabled: false, // Still disabled
704
+ warnOnUnsafeString: false, // No BigInt warnings
705
+ warnOnUnsafeDate: true // Keep Date warnings
706
+ });
707
+
708
+ // Client also supports warning options
709
+ const client = new RpcClient('http://localhost:3000/api', {
710
+ safeStringEnabled: false,
711
+ warnOnUnsafeString: false // Disable client-side warnings
712
+ });
713
+ ```
714
+
715
+ **Warning types:**
716
+ - `warnOnUnsafeString`: Warns when BigInt values are serialized without `safeStringEnabled`
717
+ - `warnOnUnsafeDate`: Warns when Date values are serialized without `safeDateEnabled`
718
+
719
+ **Use cases for disabling warnings:**
720
+ - Production environments where warnings would clutter logs
721
+ - Legacy applications that intentionally use unsafe serialization
722
+ - Bulk data processing where performance is critical
723
+
508
724
  ### Error Handling
509
725
 
510
726
  ```javascript
@@ -576,8 +792,8 @@ const devClientWithCA = new RpcClient('https://localhost:3000/api', {
576
792
  ca // Pass your self-signed CA certificate
577
793
  // or: agent: new require('https').Agent({ ca, rejectUnauthorized: true })
578
794
  });
795
+ ```
579
796
 
580
- **⚠️ Security Warning**: Use `rejectUnauthorized: false` for development only. For better security, always prefer the `ca` solution to accept only your self-signed CA. In production always use valid certificates signed by a trusted CA.
581
797
  ```
582
798
 
583
799
  **⚠️ Security Warning**: Use `rejectUnauthorized: false` for development only. For better security, always prefer the `ca` solution to accept only your self-signed CA. In production always use valid certificates signed by a trusted CA.
@@ -620,12 +836,8 @@ rpc.use('beforeCall', builtInMiddlewares.methodWhitelist([
620
836
 
621
837
  // Request timing
622
838
  rpc.use('beforeCall', builtInMiddlewares.timing());
839
+ ```
623
840
  ```
624
- if (!token || !isValidToken(token)) { // Replace `isValidToken` with your validation logic
625
- throw new Error('Invalid or missing token');
626
- }
627
- return 'OK';
628
- });
629
841
 
630
842
  // Example token validation function (replace with your actual logic)
631
843
  function isValidToken(token) {
@@ -11,31 +11,30 @@ const context = { user: 'admin' };
11
11
  const rpc = new RpcEndpoint(app, context);
12
12
 
13
13
  rpc.addMethod('greet', (req, ctx, params) => {
14
- const { name } = params;
15
- return `Hello, ${name}!`;
14
+ const { name } = params;
15
+ return `Hello, ${name}!`;
16
16
  });
17
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
- });
18
+ rpc.addMethod('getTime', () => new Date().toISOString());
30
19
 
31
20
  // Example token validation function (replace with your actual logic)
32
21
  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;
22
+ // Simple example: Check if the token matches a predefined value
23
+ const expectedToken = 'my-secret-token'; // Replace with your actual token handling logic
24
+ return token === expectedToken;
36
25
  }
37
26
 
27
+ rpc.addMethod('invalid-token', (req) => {
28
+ // Example: Validate the token from request headers
29
+ const token = req.headers.authorization;
30
+ if (!token || !isValidToken(token)) {
31
+ // Replace `isValidToken` with your validation logic
32
+ throw new Error('Invalid or missing token');
33
+ }
34
+ return 'OK';
35
+ });
36
+
38
37
  const PORT = 3000;
39
38
  app.listen(PORT, () => {
40
- console.log(`Server listening on port ${PORT}`);
39
+ console.log(`Server listening on port ${PORT}`);
41
40
  });
@@ -1,114 +1,120 @@
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
- });
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]) => ({
34
+ key,
35
+ value,
36
+ }));
37
+ });
38
+
39
+ rpc.addMethod('testBigInt', (req, ctx, params) => {
40
+ const bigNumber = BigInt(params.number || '123456789012345678901234567890');
41
+ return {
42
+ original: params.number,
43
+ bigint: bigNumber,
44
+ doubled: bigNumber * 2n,
45
+ timestamp: new Date(),
46
+ };
47
+ });
48
+
49
+ // Start server
50
+ const PORT = process.env.PORT || 3000;
51
+ const server = app.listen(PORT, () => {
52
+ console.log(`🚀 JSON-RPC Server running on http://localhost:${PORT}`);
53
+ console.log(`📡 Endpoint: http://localhost:${PORT}/api`);
54
+
55
+ // === CLIENT DEMO ===
56
+ setTimeout(async () => {
57
+ console.log('\n🔧 Testing client...');
58
+
59
+ const client = new RpcClient(`http://localhost:${PORT}/api`);
60
+
61
+ try {
62
+ // Test 1: Save some data
63
+ console.log('📝 Saving data...');
64
+ const saveResult = await client.call('saveData', {
65
+ key: 'test',
66
+ value: 'Hello World!',
67
+ });
68
+ console.log('✅ Save result:', saveResult);
69
+
70
+ // Test 2: Get data back
71
+ console.log('\n📖 Getting data...');
72
+ const getData = await client.call('getData', { key: 'test' });
73
+ console.log('✅ Get result:', getData);
74
+
75
+ // Test 3: Test BigInt handling
76
+ console.log('\n🔢 Testing BigInt...');
77
+ const bigIntResult = await client.call('testBigInt', {
78
+ number: '999999999999999999999999999999',
79
+ });
80
+ console.log('✅ BigInt result:', bigIntResult);
81
+ console.log(' - Original BigInt type:', typeof bigIntResult.bigint);
82
+ console.log(' - Doubled BigInt type:', typeof bigIntResult.doubled);
83
+ console.log(' - Timestamp type:', typeof bigIntResult.timestamp);
84
+
85
+ // Test 4: Get all data
86
+ console.log('\n📊 Getting all data...');
87
+ const allData = await client.call('getAllData');
88
+ console.log('✅ All data:', allData);
89
+
90
+ // Test 5: Test error handling
91
+ console.log('\n❌ Testing error handling...');
92
+ try {
93
+ await client.call('getData', { key: 'nonexistent' });
94
+ } catch (error) {
95
+ console.log('✅ Error caught correctly:', error.message);
96
+ }
97
+
98
+ console.log('\n✨ All tests completed successfully!');
99
+ } catch (error) {
100
+ console.error('❌ Client error:', error);
101
+ }
102
+
103
+ // Close server after demo
104
+ setTimeout(() => {
105
+ server.close(() => {
106
+ console.log('\n👋 Server closed');
107
+ process.exit(0);
108
+ });
109
+ }, 1000);
110
+ }, 1000);
111
+ });
112
+
113
+ // Handle graceful shutdown
114
+ process.on('SIGINT', () => {
115
+ console.log('\n🛑 Shutting down server...');
116
+ server.close(() => {
117
+ console.log('👋 Server closed');
118
+ process.exit(0);
119
+ });
120
+ });