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/logger.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple structured logger for JSON-RPC operations
|
|
5
|
+
*/
|
|
6
|
+
class Logger {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.level = options.level || 'info';
|
|
9
|
+
this.enabled = options.enabled !== false;
|
|
10
|
+
this.prefix = options.prefix || '[JSON-RPC]';
|
|
11
|
+
this.customLogger = options.logger || null;
|
|
12
|
+
|
|
13
|
+
this.levels = {
|
|
14
|
+
error: 0,
|
|
15
|
+
warn: 1,
|
|
16
|
+
info: 2,
|
|
17
|
+
debug: 3,
|
|
18
|
+
trace: 4
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if the given level should be logged
|
|
24
|
+
* @param {string} level
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
shouldLog(level) {
|
|
28
|
+
return this.enabled && this.levels[level] <= this.levels[this.level];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format log message with metadata
|
|
33
|
+
* @param {string} level
|
|
34
|
+
* @param {string} message
|
|
35
|
+
* @param {Object} meta
|
|
36
|
+
* @returns {Object}
|
|
37
|
+
*/
|
|
38
|
+
formatMessage(level, message, meta = {}) {
|
|
39
|
+
return {
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
level: level.toUpperCase(),
|
|
42
|
+
message,
|
|
43
|
+
...meta
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generic log method
|
|
49
|
+
* @param {string} level
|
|
50
|
+
* @param {string} message
|
|
51
|
+
* @param {Object} meta
|
|
52
|
+
*/
|
|
53
|
+
log(level, message, meta = {}) {
|
|
54
|
+
if (!this.shouldLog(level)) return;
|
|
55
|
+
|
|
56
|
+
const logData = this.formatMessage(level, message, meta);
|
|
57
|
+
|
|
58
|
+
if (this.customLogger) {
|
|
59
|
+
this.customLogger[level](logData);
|
|
60
|
+
} else {
|
|
61
|
+
const logMessage = `${this.prefix} ${logData.timestamp} [${logData.level}] ${message}`;
|
|
62
|
+
const metaStr = Object.keys(meta).length > 0 ? ` | ${JSON.stringify(meta)}` : '';
|
|
63
|
+
console[level === 'debug' || level === 'trace' ? 'log' : level](`${logMessage}${metaStr}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
error(message, meta) { this.log('error', message, meta); }
|
|
68
|
+
warn(message, meta) { this.log('warn', message, meta); }
|
|
69
|
+
info(message, meta) { this.log('info', message, meta); }
|
|
70
|
+
debug(message, meta) { this.log('debug', message, meta); }
|
|
71
|
+
trace(message, meta) { this.log('trace', message, meta); }
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Log RPC call start
|
|
75
|
+
* @param {string} method
|
|
76
|
+
* @param {any} params
|
|
77
|
+
* @param {string|number} id
|
|
78
|
+
* @param {Object} req
|
|
79
|
+
*/
|
|
80
|
+
rpcCall(method, params, id, req) {
|
|
81
|
+
this.info('RPC call started', {
|
|
82
|
+
method,
|
|
83
|
+
id,
|
|
84
|
+
params: this.sanitizeParams(params),
|
|
85
|
+
userAgent: req.headers['user-agent'],
|
|
86
|
+
ip: req.ip || req.connection?.remoteAddress
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Log RPC call success
|
|
92
|
+
* @param {string} method
|
|
93
|
+
* @param {string|number} id
|
|
94
|
+
* @param {number} duration
|
|
95
|
+
* @param {any} result
|
|
96
|
+
*/
|
|
97
|
+
rpcSuccess(method, id, duration, result) {
|
|
98
|
+
this.info('RPC call completed', {
|
|
99
|
+
method,
|
|
100
|
+
id,
|
|
101
|
+
duration: `${duration}ms`,
|
|
102
|
+
resultType: typeof result,
|
|
103
|
+
resultSize: JSON.stringify(result).length
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Log RPC call error
|
|
109
|
+
* @param {string} method
|
|
110
|
+
* @param {string|number} id
|
|
111
|
+
* @param {number} duration
|
|
112
|
+
* @param {Error} error
|
|
113
|
+
*/
|
|
114
|
+
rpcError(method, id, duration, error) {
|
|
115
|
+
this.error('RPC call failed', {
|
|
116
|
+
method,
|
|
117
|
+
id,
|
|
118
|
+
duration: `${duration}ms`,
|
|
119
|
+
error: error.message,
|
|
120
|
+
code: error.code,
|
|
121
|
+
stack: error.stack
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sanitize parameters for logging (remove sensitive data)
|
|
127
|
+
* @param {any} params
|
|
128
|
+
* @returns {any}
|
|
129
|
+
*/
|
|
130
|
+
sanitizeParams(params) {
|
|
131
|
+
if (!params || typeof params !== 'object') return params;
|
|
132
|
+
|
|
133
|
+
const sensitiveKeys = ['password', 'token', 'key', 'secret', 'auth', 'authorization'];
|
|
134
|
+
const sanitized = Array.isArray(params) ? [...params] : { ...params };
|
|
135
|
+
|
|
136
|
+
const sanitizeObj = (obj) => {
|
|
137
|
+
if (typeof obj !== 'object' || obj === null) return obj;
|
|
138
|
+
|
|
139
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
140
|
+
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
|
|
141
|
+
obj[key] = '[REDACTED]';
|
|
142
|
+
} else if (typeof value === 'object') {
|
|
143
|
+
sanitizeObj(value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return obj;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return sanitizeObj(sanitized);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = Logger;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Middleware manager for JSON-RPC operations
|
|
5
|
+
*/
|
|
6
|
+
class MiddlewareManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.middlewares = {
|
|
9
|
+
beforeCall: [],
|
|
10
|
+
afterCall: [],
|
|
11
|
+
onError: [],
|
|
12
|
+
beforeValidation: [],
|
|
13
|
+
afterValidation: []
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Add middleware for a specific hook
|
|
19
|
+
* @param {string} hook - The hook name (beforeCall, afterCall, onError, etc.)
|
|
20
|
+
* @param {Function} middleware - The middleware function
|
|
21
|
+
*/
|
|
22
|
+
use(hook, middleware) {
|
|
23
|
+
if (!this.middlewares[hook]) {
|
|
24
|
+
throw new Error(`Unknown middleware hook: ${hook}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof middleware !== 'function') {
|
|
28
|
+
throw new Error('Middleware must be a function');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.middlewares[hook].push(middleware);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Execute middlewares for a specific hook
|
|
36
|
+
* @param {string} hook
|
|
37
|
+
* @param {Object} context
|
|
38
|
+
* @returns {Promise<Object>}
|
|
39
|
+
*/
|
|
40
|
+
async execute(hook, context) {
|
|
41
|
+
const middlewares = this.middlewares[hook] || [];
|
|
42
|
+
let result = context;
|
|
43
|
+
|
|
44
|
+
for (const middleware of middlewares) {
|
|
45
|
+
try {
|
|
46
|
+
const middlewareResult = await middleware(result);
|
|
47
|
+
|
|
48
|
+
// If middleware returns an object, merge it with context
|
|
49
|
+
if (middlewareResult && typeof middlewareResult === 'object') {
|
|
50
|
+
result = { ...result, ...middlewareResult };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If middleware explicitly returns false, stop execution
|
|
54
|
+
if (middlewareResult === false) {
|
|
55
|
+
throw new Error('Middleware execution stopped');
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// For error hooks, continue even if middleware fails
|
|
59
|
+
if (hook === 'onError') {
|
|
60
|
+
console.error('Error middleware failed:', error);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all middlewares for a hook
|
|
72
|
+
* @param {string} hook
|
|
73
|
+
* @returns {Function[]}
|
|
74
|
+
*/
|
|
75
|
+
getMiddlewares(hook) {
|
|
76
|
+
return this.middlewares[hook] || [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Remove all middlewares for a hook
|
|
81
|
+
* @param {string} hook
|
|
82
|
+
*/
|
|
83
|
+
clear(hook) {
|
|
84
|
+
if (hook) {
|
|
85
|
+
this.middlewares[hook] = [];
|
|
86
|
+
} else {
|
|
87
|
+
// Clear all hooks
|
|
88
|
+
Object.keys(this.middlewares).forEach(h => {
|
|
89
|
+
this.middlewares[h] = [];
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Remove a specific middleware from a hook
|
|
96
|
+
* @param {string} hook
|
|
97
|
+
* @param {Function} middleware
|
|
98
|
+
*/
|
|
99
|
+
remove(hook, middleware) {
|
|
100
|
+
if (!this.middlewares[hook]) return;
|
|
101
|
+
|
|
102
|
+
const index = this.middlewares[hook].indexOf(middleware);
|
|
103
|
+
if (index > -1) {
|
|
104
|
+
this.middlewares[hook].splice(index, 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Built-in middleware functions
|
|
111
|
+
*/
|
|
112
|
+
const builtInMiddlewares = {
|
|
113
|
+
/**
|
|
114
|
+
* Rate limiting middleware
|
|
115
|
+
* @param {Object} options
|
|
116
|
+
* @returns {Function}
|
|
117
|
+
*/
|
|
118
|
+
rateLimit(options = {}) {
|
|
119
|
+
const windowMs = options.windowMs || 60000; // 1 minute
|
|
120
|
+
const max = options.max || 100; // 100 requests per window
|
|
121
|
+
const message = options.message || 'Too many requests';
|
|
122
|
+
|
|
123
|
+
const requests = new Map();
|
|
124
|
+
|
|
125
|
+
return async (context) => {
|
|
126
|
+
const { req } = context;
|
|
127
|
+
const key = req.ip || req.connection?.remoteAddress || 'unknown';
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
|
|
130
|
+
// Clean old entries
|
|
131
|
+
if (requests.has(key)) {
|
|
132
|
+
const userRequests = requests.get(key);
|
|
133
|
+
const validRequests = userRequests.filter(time => now - time < windowMs);
|
|
134
|
+
requests.set(key, validRequests);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const userRequests = requests.get(key) || [];
|
|
138
|
+
|
|
139
|
+
if (userRequests.length >= max) {
|
|
140
|
+
const error = new Error(message);
|
|
141
|
+
error.code = -32000; // Custom JSON-RPC error code
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
userRequests.push(now);
|
|
146
|
+
requests.set(key, userRequests);
|
|
147
|
+
|
|
148
|
+
return context;
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Authentication middleware
|
|
154
|
+
* @param {Function} authFunction
|
|
155
|
+
* @returns {Function}
|
|
156
|
+
*/
|
|
157
|
+
auth(authFunction) {
|
|
158
|
+
return async (context) => {
|
|
159
|
+
const { req } = context;
|
|
160
|
+
const isAuthenticated = await authFunction(req);
|
|
161
|
+
|
|
162
|
+
if (!isAuthenticated) {
|
|
163
|
+
const error = new Error('Authentication required');
|
|
164
|
+
error.code = -32001;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return context;
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* CORS middleware
|
|
174
|
+
* @param {Object} options
|
|
175
|
+
* @returns {Function}
|
|
176
|
+
*/
|
|
177
|
+
cors(options = {}) {
|
|
178
|
+
const origin = options.origin || '*';
|
|
179
|
+
const methods = options.methods || ['POST'];
|
|
180
|
+
const headers = options.headers || ['Content-Type'];
|
|
181
|
+
|
|
182
|
+
return async (context) => {
|
|
183
|
+
const { res } = context;
|
|
184
|
+
|
|
185
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
186
|
+
res.header('Access-Control-Allow-Methods', methods.join(', '));
|
|
187
|
+
res.header('Access-Control-Allow-Headers', headers.join(', '));
|
|
188
|
+
|
|
189
|
+
return context;
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Request timing middleware
|
|
195
|
+
* @returns {Function}
|
|
196
|
+
*/
|
|
197
|
+
timing() {
|
|
198
|
+
return async (context) => {
|
|
199
|
+
context.startTime = Date.now();
|
|
200
|
+
return context;
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Method whitelist middleware
|
|
206
|
+
* @param {string[]} allowedMethods
|
|
207
|
+
* @returns {Function}
|
|
208
|
+
*/
|
|
209
|
+
methodWhitelist(allowedMethods) {
|
|
210
|
+
return async (context) => {
|
|
211
|
+
const { method } = context;
|
|
212
|
+
|
|
213
|
+
if (!allowedMethods.includes(method)) {
|
|
214
|
+
const error = new Error(`Method '${method}' is not allowed`);
|
|
215
|
+
error.code = -32601;
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return context;
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
module.exports = { MiddlewareManager, builtInMiddlewares };
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const Ajv = require('ajv');
|
|
4
|
+
const addFormats = require('ajv-formats');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Schema validator for JSON-RPC methods
|
|
8
|
+
*/
|
|
9
|
+
class SchemaValidator {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.ajv = new Ajv({
|
|
12
|
+
allErrors: true,
|
|
13
|
+
removeAdditional: options.removeAdditional || false,
|
|
14
|
+
useDefaults: options.useDefaults !== false,
|
|
15
|
+
...options.ajvOptions
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
addFormats(this.ajv);
|
|
19
|
+
|
|
20
|
+
// Add custom formats
|
|
21
|
+
this.addCustomFormats();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add custom format validators
|
|
26
|
+
*/
|
|
27
|
+
addCustomFormats() {
|
|
28
|
+
// BigInt format
|
|
29
|
+
this.ajv.addFormat('bigint', {
|
|
30
|
+
type: 'string',
|
|
31
|
+
validate: (data) => /^-?\d+n?$/.test(data)
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ObjectId format (MongoDB style)
|
|
35
|
+
this.ajv.addFormat('objectid', {
|
|
36
|
+
type: 'string',
|
|
37
|
+
validate: (data) => /^[0-9a-fA-F]{24}$/.test(data)
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate parameters against a schema
|
|
43
|
+
* @param {any} params
|
|
44
|
+
* @param {Object} schema
|
|
45
|
+
* @returns {Object} validation result
|
|
46
|
+
*/
|
|
47
|
+
validate(params, schema) {
|
|
48
|
+
const validator = this.ajv.compile(schema);
|
|
49
|
+
const valid = validator(params);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
valid,
|
|
53
|
+
errors: valid ? null : validator.errors,
|
|
54
|
+
data: params
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a validation middleware
|
|
60
|
+
* @param {Object} schema
|
|
61
|
+
* @returns {Function}
|
|
62
|
+
*/
|
|
63
|
+
middleware(schema) {
|
|
64
|
+
return async (context) => {
|
|
65
|
+
const { params } = context;
|
|
66
|
+
const result = this.validate(params, schema);
|
|
67
|
+
|
|
68
|
+
if (!result.valid) {
|
|
69
|
+
const error = new Error('Validation failed');
|
|
70
|
+
error.code = -32602; // Invalid params
|
|
71
|
+
error.data = {
|
|
72
|
+
validationErrors: result.errors.map(err => ({
|
|
73
|
+
field: err.instancePath || err.schemaPath,
|
|
74
|
+
message: err.message,
|
|
75
|
+
value: err.data
|
|
76
|
+
}))
|
|
77
|
+
};
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Update params with validated/coerced data
|
|
82
|
+
context.params = result.data;
|
|
83
|
+
return context;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add a custom keyword
|
|
89
|
+
* @param {string} name
|
|
90
|
+
* @param {Object} definition
|
|
91
|
+
*/
|
|
92
|
+
addKeyword(name, definition) {
|
|
93
|
+
this.ajv.addKeyword(name, definition);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Common schema definitions
|
|
99
|
+
*/
|
|
100
|
+
const commonSchemas = {
|
|
101
|
+
// Pagination parameters
|
|
102
|
+
pagination: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
page: { type: 'integer', minimum: 1, default: 1 },
|
|
106
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
|
107
|
+
sort: { type: 'string' },
|
|
108
|
+
order: { type: 'string', enum: ['asc', 'desc'], default: 'asc' }
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// User identification
|
|
113
|
+
userId: {
|
|
114
|
+
oneOf: [
|
|
115
|
+
{ type: 'string', format: 'objectid' },
|
|
116
|
+
{ type: 'string', format: 'uuid' },
|
|
117
|
+
{ type: 'integer', minimum: 1 }
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Email validation
|
|
122
|
+
email: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
format: 'email',
|
|
125
|
+
maxLength: 255
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Password requirements
|
|
129
|
+
password: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
minLength: 8,
|
|
132
|
+
maxLength: 128,
|
|
133
|
+
pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)'
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// BigInt validation
|
|
137
|
+
bigintString: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
format: 'bigint'
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Date validation
|
|
143
|
+
dateString: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
format: 'date-time'
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Schema builder helper
|
|
151
|
+
*/
|
|
152
|
+
class SchemaBuilder {
|
|
153
|
+
constructor() {
|
|
154
|
+
this.schema = {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {},
|
|
157
|
+
required: []
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Add a property to the schema
|
|
163
|
+
* @param {string} name
|
|
164
|
+
* @param {Object} definition
|
|
165
|
+
* @param {boolean} required
|
|
166
|
+
* @returns {SchemaBuilder}
|
|
167
|
+
*/
|
|
168
|
+
property(name, definition, required = false) {
|
|
169
|
+
this.schema.properties[name] = definition;
|
|
170
|
+
if (required) {
|
|
171
|
+
this.schema.required.push(name);
|
|
172
|
+
}
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Add multiple properties
|
|
178
|
+
* @param {Object} properties
|
|
179
|
+
* @returns {SchemaBuilder}
|
|
180
|
+
*/
|
|
181
|
+
properties(properties) {
|
|
182
|
+
Object.entries(properties).forEach(([name, definition]) => {
|
|
183
|
+
this.property(name, definition);
|
|
184
|
+
});
|
|
185
|
+
return this;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Set required fields
|
|
190
|
+
* @param {string[]} fields
|
|
191
|
+
* @returns {SchemaBuilder}
|
|
192
|
+
*/
|
|
193
|
+
required(fields) {
|
|
194
|
+
this.schema.required = [...new Set([...this.schema.required, ...fields])];
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Add additional properties setting
|
|
200
|
+
* @param {boolean} allowed
|
|
201
|
+
* @returns {SchemaBuilder}
|
|
202
|
+
*/
|
|
203
|
+
additionalProperties(allowed) {
|
|
204
|
+
this.schema.additionalProperties = allowed;
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Build and return the schema
|
|
210
|
+
* @returns {Object}
|
|
211
|
+
*/
|
|
212
|
+
build() {
|
|
213
|
+
return this.schema;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
SchemaValidator,
|
|
219
|
+
commonSchemas,
|
|
220
|
+
SchemaBuilder
|
|
221
|
+
};
|