@63klabs/cache-data 1.3.8 → 1.3.10
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/CHANGELOG.md +109 -5
- package/CONTRIBUTING.md +58 -5
- package/README.md +20 -34
- package/package.json +16 -27
- package/src/lib/dao-cache.js +55 -47
- package/src/lib/dao-endpoint.js +68 -38
- package/src/lib/tools/AWS.classes.js +58 -5
- package/src/lib/tools/{APIRequest.class.js → ApiRequest.class.js} +164 -51
- package/src/lib/tools/CachedParametersSecrets.classes.js +10 -9
- package/src/lib/tools/ClientRequest.class.js +987 -44
- package/src/lib/tools/Connections.classes.js +5 -5
- package/src/lib/tools/Response.class.js +25 -0
- package/src/lib/tools/generic.response.html.js +8 -113
- package/src/lib/tools/generic.response.js +73 -0
- package/src/lib/tools/generic.response.json.js +5 -135
- package/src/lib/tools/generic.response.rss.js +10 -114
- package/src/lib/tools/generic.response.text.js +5 -115
- package/src/lib/tools/generic.response.xml.js +10 -114
- package/src/lib/tools/index.js +113 -40
- package/src/lib/utils/ValidationExecutor.class.js +70 -0
- package/src/lib/utils/ValidationMatcher.class.js +417 -0
- package/AGENTS.md +0 -1012
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
const DebugAndLog = require('../tools/DebugAndLog.class');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal class for matching validation rules to request parameters.
|
|
5
|
+
* Implements a four-tier priority system for validation rule resolution:
|
|
6
|
+
* 1. Method-and-route match (BY_ROUTE with "METHOD:route")
|
|
7
|
+
* 2. Route-only match (BY_ROUTE with "route")
|
|
8
|
+
* 3. Method-only match (BY_METHOD with "METHOD")
|
|
9
|
+
* 4. Global parameter name
|
|
10
|
+
*
|
|
11
|
+
* @private
|
|
12
|
+
* @example
|
|
13
|
+
* // Internal use only - not exposed in public API
|
|
14
|
+
* const matcher = new ValidationMatcher(
|
|
15
|
+
* paramValidations,
|
|
16
|
+
* 'GET',
|
|
17
|
+
* '/product/{id}'
|
|
18
|
+
* );
|
|
19
|
+
* const rule = matcher.findValidationRule('id');
|
|
20
|
+
*/
|
|
21
|
+
class ValidationMatcher {
|
|
22
|
+
#paramValidations;
|
|
23
|
+
#httpMethod;
|
|
24
|
+
#resourcePath;
|
|
25
|
+
#normalizedResourcePath;
|
|
26
|
+
#cachedPatterns;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new ValidationMatcher instance.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} paramValidations - Parameter validation configuration
|
|
32
|
+
* @param {string} httpMethod - HTTP method of request (e.g., 'GET', 'POST')
|
|
33
|
+
* @param {string} resourcePath - Resource path template (e.g., '/product/{id}')
|
|
34
|
+
*/
|
|
35
|
+
constructor(paramValidations, httpMethod, resourcePath) {
|
|
36
|
+
this.#paramValidations = paramValidations || {};
|
|
37
|
+
this.#httpMethod = (httpMethod || '').toUpperCase();
|
|
38
|
+
this.#resourcePath = resourcePath || '';
|
|
39
|
+
this.#normalizedResourcePath = this.#normalizeRoute(this.#resourcePath);
|
|
40
|
+
this.#cachedPatterns = new Map();
|
|
41
|
+
|
|
42
|
+
// >! Cache normalized patterns during initialization for performance
|
|
43
|
+
this.#cacheNormalizedPatterns();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find the best matching validation rule for a parameter.
|
|
48
|
+
* Uses four-tier priority resolution:
|
|
49
|
+
* 1. Method-and-route match (BY_ROUTE with "METHOD:route")
|
|
50
|
+
* 2. Route-only match (BY_ROUTE with "route")
|
|
51
|
+
* 3. Method-only match (BY_METHOD with "METHOD")
|
|
52
|
+
* 4. Global parameter name
|
|
53
|
+
*
|
|
54
|
+
* @param {string} paramName - Parameter name to find validation for
|
|
55
|
+
* @returns {{validate: Function, params: Array<string>}|null} Validation rule with function and parameter list, or null if no match
|
|
56
|
+
* @example
|
|
57
|
+
* const rule = matcher.findValidationRule('id');
|
|
58
|
+
* if (rule) {
|
|
59
|
+
* const isValid = rule.validate(paramValue);
|
|
60
|
+
* }
|
|
61
|
+
*/
|
|
62
|
+
findValidationRule(paramName) {
|
|
63
|
+
// >! Early exit on first match to minimize pattern comparisons
|
|
64
|
+
// Priority 1: Method-and-route match
|
|
65
|
+
const methodRouteMatch = this.#findMethodRouteMatch(paramName);
|
|
66
|
+
if (methodRouteMatch) return methodRouteMatch;
|
|
67
|
+
|
|
68
|
+
// Priority 2: Route-only match
|
|
69
|
+
const routeMatch = this.#findRouteMatch(paramName);
|
|
70
|
+
if (routeMatch) return routeMatch;
|
|
71
|
+
|
|
72
|
+
// Priority 3: Method-only match
|
|
73
|
+
const methodMatch = this.#findMethodMatch(paramName);
|
|
74
|
+
if (methodMatch) return methodMatch;
|
|
75
|
+
|
|
76
|
+
// Priority 4: Global parameter name
|
|
77
|
+
return this.#findGlobalMatch(paramName);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalize route by removing leading/trailing slashes and converting to lowercase.
|
|
82
|
+
*
|
|
83
|
+
* @private
|
|
84
|
+
* @param {string} route - Route to normalize
|
|
85
|
+
* @returns {string} Normalized route
|
|
86
|
+
*/
|
|
87
|
+
#normalizeRoute(route) {
|
|
88
|
+
if (!route || typeof route !== 'string') {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
return route.replace(/^\/|\/$/g, '').toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Cache normalized patterns during initialization for performance optimization.
|
|
96
|
+
*
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
#cacheNormalizedPatterns() {
|
|
100
|
+
// Cache BY_ROUTE patterns
|
|
101
|
+
if (this.#paramValidations.BY_ROUTE && Array.isArray(this.#paramValidations.BY_ROUTE)) {
|
|
102
|
+
for (const rule of this.#paramValidations.BY_ROUTE) {
|
|
103
|
+
if (rule.route && typeof rule.route === 'string') {
|
|
104
|
+
// Extract route part (remove method prefix if present)
|
|
105
|
+
let routePart = rule.route;
|
|
106
|
+
if (routePart.includes(':')) {
|
|
107
|
+
const parts = routePart.split(':', 2);
|
|
108
|
+
routePart = parts[1] || '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Remove query parameter specification for pattern matching
|
|
112
|
+
const routeWithoutQuery = routePart.split('?')[0];
|
|
113
|
+
const normalized = this.#normalizeRoute(routeWithoutQuery);
|
|
114
|
+
this.#cachedPatterns.set(rule.route, normalized);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if a route pattern matches the request route.
|
|
122
|
+
* Supports placeholder matching for any number of placeholders (e.g., {id}, {userId}, {postId}).
|
|
123
|
+
* Performs segment-by-segment comparison with placeholder detection.
|
|
124
|
+
*
|
|
125
|
+
* @private
|
|
126
|
+
* @param {string} pattern - Route pattern to match (e.g., 'users/{userId}/posts/{postId}')
|
|
127
|
+
* @param {string} requestRoute - Normalized request route (e.g., 'users/123/posts/456')
|
|
128
|
+
* @returns {boolean} True if pattern matches request route, false otherwise
|
|
129
|
+
* @example
|
|
130
|
+
* // Single placeholder
|
|
131
|
+
* #routeMatches('product/{id}', 'product/123') // true
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* // Multiple placeholders
|
|
135
|
+
* #routeMatches('users/{userId}/posts/{postId}', 'users/123/posts/456') // true
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* // Segment count mismatch
|
|
139
|
+
* #routeMatches('product/{id}', 'product/123/extra') // false
|
|
140
|
+
*/
|
|
141
|
+
#routeMatches(pattern, requestRoute) {
|
|
142
|
+
if (!pattern || !requestRoute) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Split into segments
|
|
147
|
+
const patternSegments = pattern.split('/').filter(s => s.length > 0);
|
|
148
|
+
const requestSegments = requestRoute.split('/').filter(s => s.length > 0);
|
|
149
|
+
|
|
150
|
+
// Segment count must match
|
|
151
|
+
if (patternSegments.length !== requestSegments.length) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// >! Check each segment - placeholders match any value
|
|
156
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
157
|
+
const patternSeg = patternSegments[i];
|
|
158
|
+
const requestSeg = requestSegments[i];
|
|
159
|
+
|
|
160
|
+
// Check if pattern segment is a placeholder
|
|
161
|
+
if (patternSeg.startsWith('{') && patternSeg.endsWith('}')) {
|
|
162
|
+
continue; // Placeholder matches any value
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Exact match (case-insensitive)
|
|
166
|
+
if (patternSeg.toLowerCase() !== requestSeg.toLowerCase()) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Find method-and-route match (Priority 1: METHOD:route).
|
|
176
|
+
* Matches validation rules with method prefix (e.g., 'POST:product/{id}').
|
|
177
|
+
* Extracts method and route parts, validates both match the request.
|
|
178
|
+
*
|
|
179
|
+
* @private
|
|
180
|
+
* @param {string} paramName - Parameter name to find validation for
|
|
181
|
+
* @returns {{validate: Function, params: Array<string>}|null} Validation rule with function and parameter list, or null if no match
|
|
182
|
+
* @example
|
|
183
|
+
* // Matches 'POST:product/{id}' for POST request to /product/123
|
|
184
|
+
* #findMethodRouteMatch('id') // { validate: Function, params: ['id'] }
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* // Does not match 'GET:product/{id}' for POST request
|
|
188
|
+
* #findMethodRouteMatch('id') // null
|
|
189
|
+
*/
|
|
190
|
+
#findMethodRouteMatch(paramName) {
|
|
191
|
+
if (!this.#paramValidations.BY_ROUTE || !Array.isArray(this.#paramValidations.BY_ROUTE)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const rule of this.#paramValidations.BY_ROUTE) {
|
|
196
|
+
if (!rule.route || typeof rule.route !== 'string' || !rule.validate) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if route includes method prefix
|
|
201
|
+
if (!rule.route.includes(':')) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const [method, routePart] = rule.route.split(':', 2);
|
|
206
|
+
|
|
207
|
+
// >! Match method case-insensitively
|
|
208
|
+
if (method.toUpperCase() !== this.#httpMethod) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Extract route pattern (without query params)
|
|
213
|
+
const routeWithoutQuery = routePart.split('?')[0];
|
|
214
|
+
const normalizedPattern = this.#cachedPatterns.get(rule.route) || this.#normalizeRoute(routeWithoutQuery);
|
|
215
|
+
|
|
216
|
+
// Check if route matches
|
|
217
|
+
if (!this.#routeMatches(normalizedPattern, this.#normalizedResourcePath)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Extract parameter names from route pattern
|
|
222
|
+
const params = this.#extractParamNames(rule.route);
|
|
223
|
+
|
|
224
|
+
// Check if this rule applies to the parameter
|
|
225
|
+
if (params.length === 0 || params.includes(paramName)) {
|
|
226
|
+
return {
|
|
227
|
+
validate: rule.validate,
|
|
228
|
+
params: params.length > 0 ? params : [paramName]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Find route-only match (Priority 2: route).
|
|
238
|
+
*
|
|
239
|
+
* @private
|
|
240
|
+
* @param {string} paramName - Parameter name
|
|
241
|
+
* @returns {{validate: Function, params: Array<string>}|null} Validation rule or null
|
|
242
|
+
*/
|
|
243
|
+
#findRouteMatch(paramName) {
|
|
244
|
+
if (!this.#paramValidations.BY_ROUTE || !Array.isArray(this.#paramValidations.BY_ROUTE)) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const rule of this.#paramValidations.BY_ROUTE) {
|
|
249
|
+
if (!rule.route || typeof rule.route !== 'string' || !rule.validate) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Skip method-and-route patterns
|
|
254
|
+
if (rule.route.includes(':')) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Extract route pattern (without query params)
|
|
259
|
+
const routeWithoutQuery = rule.route.split('?')[0];
|
|
260
|
+
const normalizedPattern = this.#cachedPatterns.get(rule.route) || this.#normalizeRoute(routeWithoutQuery);
|
|
261
|
+
|
|
262
|
+
// Check if route matches
|
|
263
|
+
if (!this.#routeMatches(normalizedPattern, this.#normalizedResourcePath)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Extract parameter names from route pattern
|
|
268
|
+
const params = this.#extractParamNames(rule.route);
|
|
269
|
+
|
|
270
|
+
// Check if this rule applies to the parameter
|
|
271
|
+
if (params.length === 0 || params.includes(paramName)) {
|
|
272
|
+
return {
|
|
273
|
+
validate: rule.validate,
|
|
274
|
+
params: params.length > 0 ? params : [paramName]
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Find method-only match (Priority 3: METHOD).
|
|
284
|
+
*
|
|
285
|
+
* @private
|
|
286
|
+
* @param {string} paramName - Parameter name
|
|
287
|
+
* @returns {{validate: Function, params: Array<string>}|null} Validation rule or null
|
|
288
|
+
*/
|
|
289
|
+
#findMethodMatch(paramName) {
|
|
290
|
+
if (!this.#paramValidations.BY_METHOD || !Array.isArray(this.#paramValidations.BY_METHOD)) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const rule of this.#paramValidations.BY_METHOD) {
|
|
295
|
+
if (!rule.method || typeof rule.method !== 'string' || !rule.validate) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// >! Match method case-insensitively
|
|
300
|
+
if (rule.method.toUpperCase() === this.#httpMethod) {
|
|
301
|
+
return {
|
|
302
|
+
validate: rule.validate,
|
|
303
|
+
params: [paramName]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Find global parameter match (Priority 4: global parameter name).
|
|
313
|
+
*
|
|
314
|
+
* @private
|
|
315
|
+
* @param {string} paramName - Parameter name
|
|
316
|
+
* @returns {{validate: Function, params: Array<string>}|null} Validation rule or null
|
|
317
|
+
*/
|
|
318
|
+
#findGlobalMatch(paramName) {
|
|
319
|
+
// >! Try exact match first (for backwards compatibility)
|
|
320
|
+
if (this.#paramValidations[paramName] && typeof this.#paramValidations[paramName] === 'function') {
|
|
321
|
+
return {
|
|
322
|
+
validate: this.#paramValidations[paramName],
|
|
323
|
+
params: [paramName]
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// >! Try case-insensitive match for query parameters
|
|
328
|
+
// >! Query parameters are lowercased in ClientRequest, but validation rules may use camelCase
|
|
329
|
+
const lowerParamName = paramName.toLowerCase();
|
|
330
|
+
for (const key in this.#paramValidations) {
|
|
331
|
+
if (key.toLowerCase() === lowerParamName && typeof this.#paramValidations[key] === 'function') {
|
|
332
|
+
return {
|
|
333
|
+
validate: this.#paramValidations[key],
|
|
334
|
+
params: [paramName] // Return the lowercased paramName that was passed in
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Extract parameter names from route pattern.
|
|
344
|
+
* Extracts both path parameters (from {}) and query parameters (after ?).
|
|
345
|
+
* When ? is present, query parameters are added to path parameters.
|
|
346
|
+
* When ? is absent, only path parameters in {} are extracted.
|
|
347
|
+
*
|
|
348
|
+
* @private
|
|
349
|
+
* @param {string} routePattern - Route pattern to parse (e.g., 'product/{id}?key', 'POST:users/{userId}/posts/{postId}')
|
|
350
|
+
* @returns {Array<string>} Array of unique parameter names to validate
|
|
351
|
+
* @example
|
|
352
|
+
* // Path parameters only (no ? specification)
|
|
353
|
+
* #extractParamNames('product/{id}') // ['id']
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* // Multiple path parameters (no ? specification)
|
|
357
|
+
* #extractParamNames('users/{userId}/posts/{postId}') // ['userId', 'postId']
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* // Query parameters only (? specification, no path params)
|
|
361
|
+
* #extractParamNames('search?query,limit') // ['query', 'limit']
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* // Path and query parameters combined
|
|
365
|
+
* #extractParamNames('POST:product/{id}?key') // ['id', 'key'] - both path and query params
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* // Multiple path and query parameters
|
|
369
|
+
* #extractParamNames('users/{userId}/posts/{postId}?includeProfile') // ['userId', 'postId', 'includeProfile']
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* // Duplicate parameters removed
|
|
373
|
+
* #extractParamNames('product/{id}?id,key') // ['id', 'key'] (not ['id', 'id', 'key'])
|
|
374
|
+
*/
|
|
375
|
+
#extractParamNames(routePattern) {
|
|
376
|
+
if (!routePattern || typeof routePattern !== 'string') {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const params = [];
|
|
381
|
+
|
|
382
|
+
// Remove method prefix if present
|
|
383
|
+
let pattern = routePattern;
|
|
384
|
+
if (pattern.includes(':')) {
|
|
385
|
+
const parts = pattern.split(':', 2);
|
|
386
|
+
pattern = parts[1] || '';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Split route and query parts
|
|
390
|
+
const [routePart, queryPart] = pattern.split('?', 2);
|
|
391
|
+
|
|
392
|
+
// >! Extract path parameters from {} placeholders
|
|
393
|
+
if (routePart) {
|
|
394
|
+
const pathParamMatches = routePart.match(/\{([^}]+)\}/g);
|
|
395
|
+
if (pathParamMatches) {
|
|
396
|
+
for (const match of pathParamMatches) {
|
|
397
|
+
// Remove braces and add to params
|
|
398
|
+
const paramName = match.slice(1, -1).trim();
|
|
399
|
+
if (paramName.length > 0) {
|
|
400
|
+
params.push(paramName);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// >! Add query parameters to the list
|
|
407
|
+
if (queryPart) {
|
|
408
|
+
const queryParams = queryPart.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
409
|
+
params.push(...queryParams);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// >! Remove duplicates while preserving order
|
|
413
|
+
return [...new Set(params)];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = ValidationMatcher;
|