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.
- package/LICENSE +21 -0
- package/README.md +796 -0
- package/examples/example.js +41 -0
- package/examples/server-client-example.js +114 -0
- package/package.json +95 -0
- package/src/batch.js +237 -0
- package/src/clients/rpc-client.js +249 -0
- package/src/clients/rpc-client.min.js +1 -0
- package/src/clients/rpc-client.min.mjs +1 -0
- package/src/clients/rpc-client.mjs +275 -0
- package/src/index.d.ts +147 -0
- package/src/index.js +580 -0
- package/src/logger.js +153 -0
- package/src/middleware.js +224 -0
- package/src/validation.js +221 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const express = require("express");
|
|
5
|
+
|
|
6
|
+
/** @typedef {import("express").Router} Router */
|
|
7
|
+
/** @typedef {import("express").Request} Request */
|
|
8
|
+
/** @typedef {import("express").Response} Response */
|
|
9
|
+
|
|
10
|
+
const NestedError = require('nested-error-stacks');
|
|
11
|
+
const Logger = require('./logger');
|
|
12
|
+
const { MiddlewareManager, builtInMiddlewares } = require('./middleware');
|
|
13
|
+
const { SchemaValidator, commonSchemas, SchemaBuilder } = require('./validation');
|
|
14
|
+
const BatchHandler = require('./batch');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default properties to include in error serialization.
|
|
18
|
+
* @type {Array<string>}
|
|
19
|
+
*/
|
|
20
|
+
const defaultProperties = [
|
|
21
|
+
'stackTraceLimit', 'cause', 'code', 'message', 'stack', 'address',
|
|
22
|
+
'dest', 'errno', 'info', 'path', 'port', 'syscall', 'opensslErrorStack',
|
|
23
|
+
'function', 'library', 'reason'
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Serializes an error into a JSON-compatible format, with optional sanitization.
|
|
28
|
+
* @param {Error | NestedError | Object} error - The error to serialize.
|
|
29
|
+
* @param {boolean} sanitize - Whether to remove sensitive fields.
|
|
30
|
+
* @param {Array<string>} properties - Properties to include in serialization.
|
|
31
|
+
* @returns {Object} The serialized error.
|
|
32
|
+
*/
|
|
33
|
+
const serializeError = (error, sanitize = false, properties = defaultProperties) => {
|
|
34
|
+
if (error === null || error === undefined) return error;
|
|
35
|
+
|
|
36
|
+
const result = {};
|
|
37
|
+
const includeProperties = sanitize
|
|
38
|
+
? properties.filter(prop => !['address', 'path'].includes(prop))
|
|
39
|
+
: properties;
|
|
40
|
+
|
|
41
|
+
includeProperties.forEach(prop => {
|
|
42
|
+
if (error?.[prop] !== undefined) {
|
|
43
|
+
result[prop] = error[prop];
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (error instanceof NestedError) {
|
|
48
|
+
result.type = 'NestedError';
|
|
49
|
+
if (error.nested) result.nested = serializeError(error.nested, sanitize, properties);
|
|
50
|
+
} else if (error instanceof Error) {
|
|
51
|
+
result.type = 'Error';
|
|
52
|
+
} else if (error instanceof Object) {
|
|
53
|
+
result.type = 'Object';
|
|
54
|
+
Object.assign(result, error);
|
|
55
|
+
} else {
|
|
56
|
+
result.type = 'Default';
|
|
57
|
+
result.instance = error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @template C
|
|
65
|
+
*/
|
|
66
|
+
class RpcEndpoint {
|
|
67
|
+
/**
|
|
68
|
+
* Serve client scripts for making JSON-RPC calls from the browser.
|
|
69
|
+
* @param {Router} router Express.Router instance
|
|
70
|
+
* @param {string} [url="/vendor/rpc-client"]
|
|
71
|
+
* The path from which client scripts will be served.
|
|
72
|
+
*/
|
|
73
|
+
static serveScripts(router, url = "/vendor/rpc-client") {
|
|
74
|
+
router.use(url, express.static(path.join(__dirname, "clients")));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @type {string} */
|
|
78
|
+
#endpoint;
|
|
79
|
+
|
|
80
|
+
/** @type {{ [name: string]: any }} */
|
|
81
|
+
#methods = {};
|
|
82
|
+
|
|
83
|
+
/** @type {Logger} */
|
|
84
|
+
#logger;
|
|
85
|
+
|
|
86
|
+
/** @type {MiddlewareManager} */
|
|
87
|
+
#middleware;
|
|
88
|
+
|
|
89
|
+
/** @type {SchemaValidator} */
|
|
90
|
+
#validator;
|
|
91
|
+
|
|
92
|
+
/** @type {BatchHandler} */
|
|
93
|
+
#batchHandler;
|
|
94
|
+
|
|
95
|
+
/** @type {Object} */
|
|
96
|
+
#options;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {Router} router The Express router to attach the endpoint to.
|
|
100
|
+
* @param {C} context The context object to pass to the method handlers.
|
|
101
|
+
* @param {string|Object} [endpointOrOptions="/api"] The endpoint path or options object.
|
|
102
|
+
* @param {Object} [options={}] Options object if first param is string.
|
|
103
|
+
*/
|
|
104
|
+
constructor(router, context, endpointOrOptions = "/api", options = {}) {
|
|
105
|
+
// Handle both signatures: (router, context, "/api", options) and (router, context, options)
|
|
106
|
+
if (typeof endpointOrOptions === 'string') {
|
|
107
|
+
this.#endpoint = endpointOrOptions;
|
|
108
|
+
this.#options = { ...options };
|
|
109
|
+
} else {
|
|
110
|
+
this.#endpoint = endpointOrOptions.endpoint || "/api";
|
|
111
|
+
this.#options = { ...endpointOrOptions };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Initialize components
|
|
115
|
+
this.#logger = new Logger(this.#options.logging || {});
|
|
116
|
+
this.#middleware = new MiddlewareManager();
|
|
117
|
+
this.#validator = new SchemaValidator(this.#options.validation || {});
|
|
118
|
+
this.#batchHandler = new BatchHandler(this);
|
|
119
|
+
|
|
120
|
+
// Setup built-in middleware if configured
|
|
121
|
+
this.#setupBuiltInMiddleware();
|
|
122
|
+
|
|
123
|
+
// Wire up routes
|
|
124
|
+
this.#setupRoutes(router, context);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Setup built-in middleware based on options
|
|
129
|
+
*/
|
|
130
|
+
#setupBuiltInMiddleware() {
|
|
131
|
+
const { rateLimit, cors, auth, timing, methodWhitelist } = this.#options;
|
|
132
|
+
|
|
133
|
+
if (timing !== false) {
|
|
134
|
+
this.use('beforeCall', builtInMiddlewares.timing());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (cors) {
|
|
138
|
+
this.use('beforeCall', builtInMiddlewares.cors(cors));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (rateLimit) {
|
|
142
|
+
this.use('beforeCall', builtInMiddlewares.rateLimit(rateLimit));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (auth) {
|
|
146
|
+
this.use('beforeCall', builtInMiddlewares.auth(auth));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (methodWhitelist && Array.isArray(methodWhitelist)) {
|
|
150
|
+
this.use('beforeCall', builtInMiddlewares.methodWhitelist(methodWhitelist));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Setup Express routes
|
|
156
|
+
* @param {Router} router
|
|
157
|
+
* @param {C} context
|
|
158
|
+
*/
|
|
159
|
+
#setupRoutes(router, context) {
|
|
160
|
+
// Health check endpoint
|
|
161
|
+
if (this.#options.healthCheck !== false) {
|
|
162
|
+
router.get(`${this.#endpoint}/health`, (req, res) => {
|
|
163
|
+
res.json({
|
|
164
|
+
status: 'ok',
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
version: require('../package.json').version
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Metrics endpoint
|
|
172
|
+
if (this.#options.metrics) {
|
|
173
|
+
router.get(`${this.#endpoint}/metrics`, (req, res) => {
|
|
174
|
+
res.json(this.getMetrics());
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Main JSON-RPC endpoint
|
|
179
|
+
router.post(this.#endpoint, async (req, res) => {
|
|
180
|
+
const startTime = Date.now();
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// Check if it's a batch request
|
|
184
|
+
if (this.#batchHandler.isBatchRequest(req.body)) {
|
|
185
|
+
this.#logger.info('Batch request received', {
|
|
186
|
+
batchSize: req.body.length,
|
|
187
|
+
ip: req.ip
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const results = await this.#batchHandler.processBatch(req.body, req, res, context);
|
|
191
|
+
|
|
192
|
+
// Only send response if there are results (non-notification requests)
|
|
193
|
+
if (results.length > 0) {
|
|
194
|
+
res.json(results);
|
|
195
|
+
} else {
|
|
196
|
+
res.status(204).end(); // No content for all-notification batch
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.#logger.info('Batch request completed', {
|
|
200
|
+
batchSize: req.body.length,
|
|
201
|
+
responseCount: results.length,
|
|
202
|
+
duration: Date.now() - startTime
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Single request processing
|
|
209
|
+
await this.#processSingleRequest(req, res, context, startTime);
|
|
210
|
+
|
|
211
|
+
} catch (error) {
|
|
212
|
+
this.#logger.error('Endpoint error', { error: error.message, stack: error.stack });
|
|
213
|
+
|
|
214
|
+
res.json({
|
|
215
|
+
jsonrpc: "2.0",
|
|
216
|
+
id: null,
|
|
217
|
+
error: {
|
|
218
|
+
code: -32603,
|
|
219
|
+
message: "Internal error",
|
|
220
|
+
data: serializeError(error, true)
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Process a single JSON-RPC request
|
|
229
|
+
* @param {Request} req
|
|
230
|
+
* @param {Response} res
|
|
231
|
+
* @param {C} context
|
|
232
|
+
* @param {number} startTime
|
|
233
|
+
*/
|
|
234
|
+
async #processSingleRequest(req, res, context, startTime) {
|
|
235
|
+
const { jsonrpc, method, params, id } = req.body || {};
|
|
236
|
+
|
|
237
|
+
// Log the incoming request
|
|
238
|
+
this.#logger.rpcCall(method, params, id, req);
|
|
239
|
+
|
|
240
|
+
// Validate JSON-RPC 2.0 request
|
|
241
|
+
if (jsonrpc !== "2.0") {
|
|
242
|
+
return this.reply(res, {
|
|
243
|
+
id,
|
|
244
|
+
error: { code: -32600, message: `Invalid Request: 'jsonrpc' must be '2.0'.` }
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (typeof method !== "string") {
|
|
249
|
+
return this.reply(res, {
|
|
250
|
+
id,
|
|
251
|
+
error: { code: -32600, message: `Invalid Request: 'method' must be a string.` }
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const methodConfig = this.#methods[method];
|
|
256
|
+
if (!methodConfig) {
|
|
257
|
+
return this.reply(res, {
|
|
258
|
+
id,
|
|
259
|
+
error: { code: -32601, message: `Method "${method}" not found` }
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const handler = typeof methodConfig === 'function' ? methodConfig : methodConfig.handler;
|
|
264
|
+
const schema = typeof methodConfig === 'object' ? methodConfig.schema : null;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
// Prepare middleware context
|
|
268
|
+
let middlewareContext = {
|
|
269
|
+
req,
|
|
270
|
+
res,
|
|
271
|
+
method,
|
|
272
|
+
params,
|
|
273
|
+
context,
|
|
274
|
+
id,
|
|
275
|
+
startTime
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Execute beforeCall middleware
|
|
279
|
+
middlewareContext = await this.#middleware.execute('beforeCall', middlewareContext);
|
|
280
|
+
|
|
281
|
+
// Validate parameters if schema is provided
|
|
282
|
+
if (schema) {
|
|
283
|
+
middlewareContext = await this.#middleware.execute('beforeValidation', middlewareContext);
|
|
284
|
+
|
|
285
|
+
const validation = this.#validator.validate(middlewareContext.params, schema);
|
|
286
|
+
if (!validation.valid) {
|
|
287
|
+
const error = new Error('Validation failed');
|
|
288
|
+
error.code = -32602;
|
|
289
|
+
error.data = {
|
|
290
|
+
validationErrors: validation.errors.map(err => ({
|
|
291
|
+
field: err.instancePath || err.schemaPath,
|
|
292
|
+
message: err.message,
|
|
293
|
+
value: err.data
|
|
294
|
+
}))
|
|
295
|
+
};
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
middlewareContext.params = validation.data;
|
|
300
|
+
middlewareContext = await this.#middleware.execute('afterValidation', middlewareContext);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Invoke the handler
|
|
304
|
+
const result = await Promise.resolve(handler(req, context, middlewareContext.params));
|
|
305
|
+
|
|
306
|
+
// Execute afterCall middleware
|
|
307
|
+
middlewareContext.result = result;
|
|
308
|
+
await this.#middleware.execute('afterCall', middlewareContext);
|
|
309
|
+
|
|
310
|
+
// Log success
|
|
311
|
+
const duration = Date.now() - startTime;
|
|
312
|
+
this.#logger.rpcSuccess(method, id, duration, result);
|
|
313
|
+
|
|
314
|
+
// Convert any BigInt/Date values before JSON-stringifying
|
|
315
|
+
const safeResult = this.serializeBigIntsAndDates(result);
|
|
316
|
+
this.reply(res, { id, result: safeResult });
|
|
317
|
+
|
|
318
|
+
} catch (err) {
|
|
319
|
+
const duration = Date.now() - startTime;
|
|
320
|
+
this.#logger.rpcError(method, id, duration, err);
|
|
321
|
+
|
|
322
|
+
// Execute error middleware
|
|
323
|
+
try {
|
|
324
|
+
await this.#middleware.execute('onError', {
|
|
325
|
+
req,
|
|
326
|
+
res,
|
|
327
|
+
method,
|
|
328
|
+
params,
|
|
329
|
+
context,
|
|
330
|
+
id,
|
|
331
|
+
error: err,
|
|
332
|
+
startTime,
|
|
333
|
+
duration
|
|
334
|
+
});
|
|
335
|
+
} catch (middlewareError) {
|
|
336
|
+
this.#logger.error('Error middleware failed', { error: middlewareError.message });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.reply(res, {
|
|
340
|
+
id,
|
|
341
|
+
error: {
|
|
342
|
+
code: err.code || -32603,
|
|
343
|
+
message: err.message || `Internal error`,
|
|
344
|
+
...(err.data && { data: err.data }),
|
|
345
|
+
error: serializeError(err, true)
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Register a new JSON-RPC method.
|
|
353
|
+
* @param {string} name The method name.
|
|
354
|
+
* @param {Function|Object} handlerOrConfig The function to handle calls or config object.
|
|
355
|
+
* @param {Object} [schema] Optional schema for parameter validation.
|
|
356
|
+
*/
|
|
357
|
+
addMethod(name, handlerOrConfig, schema = null) {
|
|
358
|
+
if (typeof handlerOrConfig === 'function') {
|
|
359
|
+
this.#methods[name] = schema ? { handler: handlerOrConfig, schema } : handlerOrConfig;
|
|
360
|
+
} else if (typeof handlerOrConfig === 'object' && handlerOrConfig.handler) {
|
|
361
|
+
this.#methods[name] = handlerOrConfig;
|
|
362
|
+
} else {
|
|
363
|
+
throw new Error('Invalid handler configuration');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.#logger.debug('Method registered', { method: name, hasSchema: !!schema });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Add middleware for specific hooks
|
|
371
|
+
* @param {string} hook
|
|
372
|
+
* @param {Function} middleware
|
|
373
|
+
*/
|
|
374
|
+
use(hook, middleware) {
|
|
375
|
+
this.#middleware.use(hook, middleware);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Remove method
|
|
380
|
+
* @param {string} name
|
|
381
|
+
*/
|
|
382
|
+
removeMethod(name) {
|
|
383
|
+
delete this.#methods[name];
|
|
384
|
+
this.#logger.debug('Method removed', { method: name });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Get method configuration
|
|
389
|
+
* @param {string} name
|
|
390
|
+
* @returns {any}
|
|
391
|
+
*/
|
|
392
|
+
getMethod(name) {
|
|
393
|
+
return this.#methods[name];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* List all registered methods
|
|
398
|
+
* @returns {string[]}
|
|
399
|
+
*/
|
|
400
|
+
listMethods() {
|
|
401
|
+
return Object.keys(this.#methods);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** @returns {string} The endpoint path (e.g. "/api"). */
|
|
405
|
+
get endpoint() {
|
|
406
|
+
return this.#endpoint;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* @returns {{ [name: string]: any }}
|
|
411
|
+
* All registered methods.
|
|
412
|
+
*/
|
|
413
|
+
get methods() {
|
|
414
|
+
return this.#methods;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get logger instance
|
|
419
|
+
* @returns {Logger}
|
|
420
|
+
*/
|
|
421
|
+
get logger() {
|
|
422
|
+
return this.#logger;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Get middleware manager
|
|
427
|
+
* @returns {MiddlewareManager}
|
|
428
|
+
*/
|
|
429
|
+
get middleware() {
|
|
430
|
+
return this.#middleware;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Get validator instance
|
|
435
|
+
* @returns {SchemaValidator}
|
|
436
|
+
*/
|
|
437
|
+
get validator() {
|
|
438
|
+
return this.#validator;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get metrics data
|
|
443
|
+
* @returns {Object}
|
|
444
|
+
*/
|
|
445
|
+
getMetrics() {
|
|
446
|
+
return {
|
|
447
|
+
methods: Object.keys(this.#methods).length,
|
|
448
|
+
middleware: {
|
|
449
|
+
beforeCall: this.#middleware.getMiddlewares('beforeCall').length,
|
|
450
|
+
afterCall: this.#middleware.getMiddlewares('afterCall').length,
|
|
451
|
+
onError: this.#middleware.getMiddlewares('onError').length
|
|
452
|
+
},
|
|
453
|
+
uptime: process.uptime(),
|
|
454
|
+
memory: process.memoryUsage(),
|
|
455
|
+
timestamp: new Date().toISOString()
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Send a JSON-RPC 2.0 response.
|
|
461
|
+
* @param {Response} res The Express response object.
|
|
462
|
+
* @param {{
|
|
463
|
+
* id?: string|number|null,
|
|
464
|
+
* result?: any,
|
|
465
|
+
* error?: { code: number, message: string, data?: any }
|
|
466
|
+
* }} responsePayload The JSON-RPC response fields.
|
|
467
|
+
*/
|
|
468
|
+
reply(res, { id, result, error }) {
|
|
469
|
+
const response = { jsonrpc: "2.0", id: id === undefined ? null : id };
|
|
470
|
+
|
|
471
|
+
if (error) {
|
|
472
|
+
response.error = error;
|
|
473
|
+
} else {
|
|
474
|
+
response.result = result;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
res.json(response);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Recursively convert BigInt values to strings and Date objects to ISO strings.
|
|
482
|
+
* This is crucial because JSON.stringify() cannot handle BigInt natively
|
|
483
|
+
* and Date objects are best transmitted in a standardized format.
|
|
484
|
+
*
|
|
485
|
+
* @param {any} value The value to serialize.
|
|
486
|
+
* @returns {any} A version of `value` safe for JSON serialization.
|
|
487
|
+
*/
|
|
488
|
+
serializeBigIntsAndDates(value) {
|
|
489
|
+
if (typeof value === "bigint") {
|
|
490
|
+
// Convert BigInt to string
|
|
491
|
+
return value.toString();
|
|
492
|
+
} else if (Array.isArray(value)) {
|
|
493
|
+
// Recurse into arrays
|
|
494
|
+
return value.map((v) => this.serializeBigIntsAndDates(v));
|
|
495
|
+
} else if (value instanceof Date) {
|
|
496
|
+
// Convert Date to ISO string (UTC)
|
|
497
|
+
return value.toISOString();
|
|
498
|
+
} else if (value && typeof value === "object") {
|
|
499
|
+
// Recurse into plain objects
|
|
500
|
+
const result = {};
|
|
501
|
+
for (const [key, val] of Object.entries(value)) {
|
|
502
|
+
result[key] = this.serializeBigIntsAndDates(val);
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// If it's none of the above, return as-is
|
|
508
|
+
return value;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Recursively convert stringified BigInts back to BigInt,
|
|
513
|
+
* and ISO 8601 date strings back to Date objects.
|
|
514
|
+
*
|
|
515
|
+
* This is useful if you receive JSON that was serialized
|
|
516
|
+
* by `serializeBigIntsAndDates()` and want to re-hydrate
|
|
517
|
+
* the original types.
|
|
518
|
+
*
|
|
519
|
+
* @param {any} value The value to deserialize.
|
|
520
|
+
* @returns {any} The re-hydrated value.
|
|
521
|
+
*/
|
|
522
|
+
deserializeBigIntsAndDates(value) {
|
|
523
|
+
// More comprehensive ISO date regex that handles:
|
|
524
|
+
// - UTC: 2023-01-01T12:00:00.000Z
|
|
525
|
+
// - With timezone: 2023-01-01T12:00:00.000+01:00
|
|
526
|
+
// - Without timezone: 2023-01-01T12:00:00.000 (treated as local)
|
|
527
|
+
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
528
|
+
|
|
529
|
+
// 1. Check if it's a string that might be a BigInt or a Date
|
|
530
|
+
if (typeof value === "string") {
|
|
531
|
+
// BigInt check: matches digits (including negative), optionally ending in "n"
|
|
532
|
+
// e.g., "42n", "42", "-42n", "-42"
|
|
533
|
+
if (/^-?\d+n?$/.test(value)) {
|
|
534
|
+
return BigInt(value.replace(/n$/, ""));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Date check: matches an ISO 8601 string
|
|
538
|
+
if (ISO_DATE_REGEX.test(value)) {
|
|
539
|
+
const date = new Date(value);
|
|
540
|
+
// Double-check that we got a valid date
|
|
541
|
+
if (!isNaN(date.getTime())) {
|
|
542
|
+
return date;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 2. If it's an array, handle each element
|
|
548
|
+
if (Array.isArray(value)) {
|
|
549
|
+
return value.map((v) => this.deserializeBigIntsAndDates(v));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 3. If it's a plain object, recurse into each property
|
|
553
|
+
if (value && typeof value === "object") {
|
|
554
|
+
return Object.fromEntries(
|
|
555
|
+
Object.entries(value).map(([key, val]) => [
|
|
556
|
+
key,
|
|
557
|
+
this.deserializeBigIntsAndDates(val)
|
|
558
|
+
])
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 4. For everything else (number, boolean, null, undefined, etc.), return as-is
|
|
563
|
+
return value;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Import the client class
|
|
568
|
+
const RpcClient = require('./clients/rpc-client');
|
|
569
|
+
|
|
570
|
+
// Export both server and client, plus utilities
|
|
571
|
+
module.exports = RpcEndpoint;
|
|
572
|
+
module.exports.RpcEndpoint = RpcEndpoint;
|
|
573
|
+
module.exports.RpcClient = RpcClient;
|
|
574
|
+
module.exports.Logger = Logger;
|
|
575
|
+
module.exports.MiddlewareManager = MiddlewareManager;
|
|
576
|
+
module.exports.builtInMiddlewares = builtInMiddlewares;
|
|
577
|
+
module.exports.SchemaValidator = SchemaValidator;
|
|
578
|
+
module.exports.commonSchemas = commonSchemas;
|
|
579
|
+
module.exports.SchemaBuilder = SchemaBuilder;
|
|
580
|
+
module.exports.BatchHandler = BatchHandler;
|