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.
@@ -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
+