navis.js 3.0.2 → 4.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 +66 -1
- package/examples/lambda-optimized.js +103 -0
- package/examples/lambda.js +30 -29
- package/examples/v4-features-demo.js +171 -0
- package/package.json +1 -1
- package/src/auth/authenticator.js +223 -0
- package/src/core/advanced-router.js +186 -0
- package/src/core/app.js +255 -176
- package/src/core/lambda-handler.js +130 -0
- package/src/errors/error-handler.js +157 -0
- package/src/index.js +62 -0
- package/src/middleware/cold-start-tracker.js +56 -0
- package/src/middleware/rate-limiter.js +159 -0
- package/src/utils/lazy-init.js +100 -0
- package/src/utils/service-client-pool.js +131 -0
- package/src/validation/validator.js +301 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServiceClient Pool - Connection reuse for Lambda
|
|
3
|
+
* v3.1: Connection pooling to reduce cold start overhead
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ServiceClient = require('./service-client');
|
|
7
|
+
|
|
8
|
+
class ServiceClientPool {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.clients = new Map();
|
|
11
|
+
this.maxSize = 10; // Maximum cached clients
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get or create a ServiceClient instance
|
|
16
|
+
* Reuses existing clients to avoid re-initialization
|
|
17
|
+
* @param {string} baseUrl - Service base URL
|
|
18
|
+
* @param {Object} options - ServiceClient options
|
|
19
|
+
* @returns {ServiceClient} - Cached or new ServiceClient
|
|
20
|
+
*/
|
|
21
|
+
get(baseUrl, options = {}) {
|
|
22
|
+
// Create a unique key for this client configuration
|
|
23
|
+
const key = this._createKey(baseUrl, options);
|
|
24
|
+
|
|
25
|
+
if (!this.clients.has(key)) {
|
|
26
|
+
// Create new client if not in pool
|
|
27
|
+
const client = new ServiceClient(baseUrl, options);
|
|
28
|
+
this.clients.set(key, client);
|
|
29
|
+
|
|
30
|
+
// Limit pool size (remove oldest if needed)
|
|
31
|
+
if (this.clients.size > this.maxSize) {
|
|
32
|
+
const firstKey = this.clients.keys().next().value;
|
|
33
|
+
this.clients.delete(firstKey);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return this.clients.get(key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a client exists in pool
|
|
42
|
+
* @param {string} baseUrl - Service base URL
|
|
43
|
+
* @param {Object} options - ServiceClient options
|
|
44
|
+
* @returns {boolean} - True if client exists in pool
|
|
45
|
+
*/
|
|
46
|
+
has(baseUrl, options = {}) {
|
|
47
|
+
const key = this._createKey(baseUrl, options);
|
|
48
|
+
return this.clients.has(key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Remove a client from pool
|
|
53
|
+
* @param {string} baseUrl - Service base URL
|
|
54
|
+
* @param {Object} options - ServiceClient options
|
|
55
|
+
*/
|
|
56
|
+
delete(baseUrl, options = {}) {
|
|
57
|
+
const key = this._createKey(baseUrl, options);
|
|
58
|
+
this.clients.delete(key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clear all clients from pool
|
|
63
|
+
*/
|
|
64
|
+
clear() {
|
|
65
|
+
this.clients.clear();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get pool size
|
|
70
|
+
* @returns {number} - Number of cached clients
|
|
71
|
+
*/
|
|
72
|
+
size() {
|
|
73
|
+
return this.clients.size;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get all cached client URLs
|
|
78
|
+
* @returns {Array} - Array of base URLs
|
|
79
|
+
*/
|
|
80
|
+
getCachedUrls() {
|
|
81
|
+
return Array.from(this.clients.keys()).map(key => {
|
|
82
|
+
const [url] = key.split('::');
|
|
83
|
+
return url;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create unique key for client
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
_createKey(baseUrl, options) {
|
|
92
|
+
// Normalize options to create consistent key
|
|
93
|
+
const normalizedOptions = {
|
|
94
|
+
timeout: options.timeout || 5000,
|
|
95
|
+
maxRetries: options.maxRetries,
|
|
96
|
+
retryBaseDelay: options.retryBaseDelay,
|
|
97
|
+
circuitBreaker: options.circuitBreaker ? JSON.stringify(options.circuitBreaker) : undefined,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return `${baseUrl}::${JSON.stringify(normalizedOptions)}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Singleton instance for Lambda (reused across invocations)
|
|
105
|
+
let poolInstance = null;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get singleton ServiceClientPool instance
|
|
109
|
+
* In Lambda, this instance persists across invocations
|
|
110
|
+
* @returns {ServiceClientPool} - Singleton pool instance
|
|
111
|
+
*/
|
|
112
|
+
function getPool() {
|
|
113
|
+
if (!poolInstance) {
|
|
114
|
+
poolInstance = new ServiceClientPool();
|
|
115
|
+
}
|
|
116
|
+
return poolInstance;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reset pool (useful for testing)
|
|
121
|
+
*/
|
|
122
|
+
function resetPool() {
|
|
123
|
+
poolInstance = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
ServiceClientPool,
|
|
128
|
+
getPool,
|
|
129
|
+
resetPool,
|
|
130
|
+
};
|
|
131
|
+
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Validation Middleware
|
|
3
|
+
* v4: Schema-based request validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ValidationError extends Error {
|
|
7
|
+
constructor(message, errors = []) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ValidationError';
|
|
10
|
+
this.statusCode = 400;
|
|
11
|
+
this.errors = errors;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate request against schema
|
|
17
|
+
* @param {Object} schema - Validation schema
|
|
18
|
+
* @returns {Function} - Middleware function
|
|
19
|
+
*/
|
|
20
|
+
function validate(schema) {
|
|
21
|
+
return async (req, res, next) => {
|
|
22
|
+
try {
|
|
23
|
+
const errors = [];
|
|
24
|
+
|
|
25
|
+
// Validate body
|
|
26
|
+
if (schema.body) {
|
|
27
|
+
const bodyErrors = validateObject(req.body || {}, schema.body);
|
|
28
|
+
if (bodyErrors.length > 0) {
|
|
29
|
+
errors.push(...bodyErrors.map(e => ({ field: `body.${e.field}`, ...e })));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Validate query parameters
|
|
34
|
+
if (schema.query) {
|
|
35
|
+
const queryErrors = validateObject(req.query || {}, schema.query);
|
|
36
|
+
if (queryErrors.length > 0) {
|
|
37
|
+
errors.push(...queryErrors.map(e => ({ field: `query.${e.field}`, ...e })));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate path parameters
|
|
42
|
+
if (schema.params) {
|
|
43
|
+
const paramsErrors = validateObject(req.params || {}, schema.params);
|
|
44
|
+
if (paramsErrors.length > 0) {
|
|
45
|
+
errors.push(...paramsErrors.map(e => ({ field: `params.${e.field}`, ...e })));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate headers
|
|
50
|
+
if (schema.headers) {
|
|
51
|
+
const headersErrors = validateObject(req.headers || {}, schema.headers);
|
|
52
|
+
if (headersErrors.length > 0) {
|
|
53
|
+
errors.push(...headersErrors.map(e => ({ field: `headers.${e.field}`, ...e })));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (errors.length > 0) {
|
|
58
|
+
throw new ValidationError('Validation failed', errors);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
next();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof ValidationError) {
|
|
64
|
+
res.statusCode = error.statusCode;
|
|
65
|
+
res.body = {
|
|
66
|
+
error: error.message,
|
|
67
|
+
errors: error.errors,
|
|
68
|
+
};
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate object against schema
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
function validateObject(obj, schema) {
|
|
81
|
+
const errors = [];
|
|
82
|
+
|
|
83
|
+
for (const [field, rules] of Object.entries(schema)) {
|
|
84
|
+
const value = obj[field];
|
|
85
|
+
const fieldErrors = validateField(field, value, rules);
|
|
86
|
+
errors.push(...fieldErrors);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return errors;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a single field
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
function validateField(field, value, rules) {
|
|
97
|
+
const errors = [];
|
|
98
|
+
|
|
99
|
+
// Required check
|
|
100
|
+
if (rules.required && (value === undefined || value === null || value === '')) {
|
|
101
|
+
errors.push({
|
|
102
|
+
field,
|
|
103
|
+
message: `${field} is required`,
|
|
104
|
+
code: 'REQUIRED',
|
|
105
|
+
});
|
|
106
|
+
return errors; // Don't check other rules if required fails
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Skip other validations if value is optional and not provided
|
|
110
|
+
if (!rules.required && (value === undefined || value === null)) {
|
|
111
|
+
return errors;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Type check
|
|
115
|
+
if (rules.type) {
|
|
116
|
+
const typeError = checkType(field, value, rules.type);
|
|
117
|
+
if (typeError) {
|
|
118
|
+
errors.push(typeError);
|
|
119
|
+
return errors; // Don't check other rules if type fails
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// String validations
|
|
124
|
+
if (rules.type === 'string') {
|
|
125
|
+
if (rules.minLength !== undefined && value.length < rules.minLength) {
|
|
126
|
+
errors.push({
|
|
127
|
+
field,
|
|
128
|
+
message: `${field} must be at least ${rules.minLength} characters`,
|
|
129
|
+
code: 'MIN_LENGTH',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (rules.maxLength !== undefined && value.length > rules.maxLength) {
|
|
133
|
+
errors.push({
|
|
134
|
+
field,
|
|
135
|
+
message: `${field} must be at most ${rules.maxLength} characters`,
|
|
136
|
+
code: 'MAX_LENGTH',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (rules.pattern && !new RegExp(rules.pattern).test(value)) {
|
|
140
|
+
errors.push({
|
|
141
|
+
field,
|
|
142
|
+
message: `${field} does not match required pattern`,
|
|
143
|
+
code: 'PATTERN',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (rules.format === 'email' && !isValidEmail(value)) {
|
|
147
|
+
errors.push({
|
|
148
|
+
field,
|
|
149
|
+
message: `${field} must be a valid email address`,
|
|
150
|
+
code: 'EMAIL_FORMAT',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (rules.format === 'uuid' && !isValidUUID(value)) {
|
|
154
|
+
errors.push({
|
|
155
|
+
field,
|
|
156
|
+
message: `${field} must be a valid UUID`,
|
|
157
|
+
code: 'UUID_FORMAT',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Number validations
|
|
163
|
+
if (rules.type === 'number') {
|
|
164
|
+
if (rules.min !== undefined && value < rules.min) {
|
|
165
|
+
errors.push({
|
|
166
|
+
field,
|
|
167
|
+
message: `${field} must be at least ${rules.min}`,
|
|
168
|
+
code: 'MIN',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (rules.max !== undefined && value > rules.max) {
|
|
172
|
+
errors.push({
|
|
173
|
+
field,
|
|
174
|
+
message: `${field} must be at most ${rules.max}`,
|
|
175
|
+
code: 'MAX',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Array validations
|
|
181
|
+
if (rules.type === 'array') {
|
|
182
|
+
if (!Array.isArray(value)) {
|
|
183
|
+
errors.push({
|
|
184
|
+
field,
|
|
185
|
+
message: `${field} must be an array`,
|
|
186
|
+
code: 'TYPE',
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
if (rules.minItems !== undefined && value.length < rules.minItems) {
|
|
190
|
+
errors.push({
|
|
191
|
+
field,
|
|
192
|
+
message: `${field} must have at least ${rules.minItems} items`,
|
|
193
|
+
code: 'MIN_ITEMS',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (rules.maxItems !== undefined && value.length > rules.maxItems) {
|
|
197
|
+
errors.push({
|
|
198
|
+
field,
|
|
199
|
+
message: `${field} must have at most ${rules.maxItems} items`,
|
|
200
|
+
code: 'MAX_ITEMS',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Custom validator
|
|
207
|
+
if (rules.validator && typeof rules.validator === 'function') {
|
|
208
|
+
try {
|
|
209
|
+
const result = rules.validator(value);
|
|
210
|
+
if (result !== true && typeof result === 'string') {
|
|
211
|
+
errors.push({
|
|
212
|
+
field,
|
|
213
|
+
message: result,
|
|
214
|
+
code: 'CUSTOM',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
errors.push({
|
|
219
|
+
field,
|
|
220
|
+
message: error.message || 'Custom validation failed',
|
|
221
|
+
code: 'CUSTOM',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return errors;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if value matches expected type
|
|
231
|
+
* @private
|
|
232
|
+
*/
|
|
233
|
+
function checkType(field, value, expectedType) {
|
|
234
|
+
const actualType = typeof value;
|
|
235
|
+
|
|
236
|
+
if (expectedType === 'string' && actualType !== 'string') {
|
|
237
|
+
return {
|
|
238
|
+
field,
|
|
239
|
+
message: `${field} must be a string`,
|
|
240
|
+
code: 'TYPE',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (expectedType === 'number' && actualType !== 'number') {
|
|
245
|
+
return {
|
|
246
|
+
field,
|
|
247
|
+
message: `${field} must be a number`,
|
|
248
|
+
code: 'TYPE',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (expectedType === 'boolean' && actualType !== 'boolean') {
|
|
253
|
+
return {
|
|
254
|
+
field,
|
|
255
|
+
message: `${field} must be a boolean`,
|
|
256
|
+
code: 'TYPE',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
261
|
+
return {
|
|
262
|
+
field,
|
|
263
|
+
message: `${field} must be an array`,
|
|
264
|
+
code: 'TYPE',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (expectedType === 'object' && (actualType !== 'object' || Array.isArray(value) || value === null)) {
|
|
269
|
+
return {
|
|
270
|
+
field,
|
|
271
|
+
message: `${field} must be an object`,
|
|
272
|
+
code: 'TYPE',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Validate email format
|
|
281
|
+
* @private
|
|
282
|
+
*/
|
|
283
|
+
function isValidEmail(email) {
|
|
284
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
285
|
+
return emailRegex.test(email);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Validate UUID format
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
function isValidUUID(uuid) {
|
|
293
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
294
|
+
return uuidRegex.test(uuid);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
validate,
|
|
299
|
+
ValidationError,
|
|
300
|
+
};
|
|
301
|
+
|