@63klabs/cache-data 1.3.8 → 1.3.9
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/AGENTS.md +96 -1
- package/CHANGELOG.md +63 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +17 -31
- package/package.json +5 -4
- package/src/lib/dao-cache.js +30 -13
- package/src/lib/tools/AWS.classes.js +58 -5
- package/src/lib/tools/CachedParametersSecrets.classes.js +1 -0
- package/src/lib/tools/ClientRequest.class.js +826 -32
- package/src/lib/tools/index.js +96 -34
- package/src/lib/utils/ValidationExecutor.class.js +66 -0
- package/src/lib/utils/ValidationMatcher.class.js +405 -0
package/src/lib/tools/index.js
CHANGED
|
@@ -107,7 +107,11 @@ class AppConfig {
|
|
|
107
107
|
static _ssmParameters = null;
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Initialize the Config class
|
|
110
|
+
* Initialize the Config class with asynchronous parallel execution.
|
|
111
|
+
*
|
|
112
|
+
* This method returns immediately (synchronously) while initialization operations
|
|
113
|
+
* execute asynchronously in parallel. Use AppConfig.promise() to wait for all
|
|
114
|
+
* initialization to complete before accessing initialized configuration.
|
|
111
115
|
*
|
|
112
116
|
* @param {object} options Configuration options
|
|
113
117
|
* @param {object} options.settings Application settings retrieved by Config.settings()
|
|
@@ -124,8 +128,10 @@ class AppConfig {
|
|
|
124
128
|
* @param {object} options.responses.rssResponses
|
|
125
129
|
* @param {object} options.responses.textResponses
|
|
126
130
|
* @param {object} options.ssmParameters Parameter Store
|
|
127
|
-
* @
|
|
131
|
+
* @param {boolean} [options.debug=false] Enable debug logging
|
|
132
|
+
* @returns {boolean} True if initialization started successfully, false on synchronous error
|
|
128
133
|
* @example
|
|
134
|
+
* // Initialize configuration (returns immediately)
|
|
129
135
|
* const { Config } = require("./config");
|
|
130
136
|
* Config.init({
|
|
131
137
|
* settings: {
|
|
@@ -143,48 +149,100 @@ class AppConfig {
|
|
|
143
149
|
* }
|
|
144
150
|
* }
|
|
145
151
|
* });
|
|
152
|
+
*
|
|
153
|
+
* // Wait for all initialization to complete
|
|
154
|
+
* await Config.promise();
|
|
155
|
+
*
|
|
156
|
+
* // Now safe to access initialized configuration
|
|
157
|
+
* const settings = Config.settings();
|
|
158
|
+
* const conn = Config.getConn('myConnection');
|
|
146
159
|
*/
|
|
147
160
|
static init(options = {}) {
|
|
148
161
|
|
|
149
|
-
|
|
162
|
+
try {
|
|
150
163
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
const debug = (options?.debug === true);
|
|
165
|
+
if (debug) {
|
|
166
|
+
DebugAndLog.debug("Config Init in debug mode");
|
|
167
|
+
}
|
|
155
168
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
if (options.settings) {
|
|
170
|
+
const settingsPromise = new Promise((resolve) => {
|
|
171
|
+
try {
|
|
172
|
+
AppConfig._settings = options.settings;
|
|
173
|
+
if (debug) {
|
|
174
|
+
DebugAndLog.debug("Settings initialized", AppConfig._settings);
|
|
175
|
+
}
|
|
176
|
+
resolve(true);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
DebugAndLog.error(`Settings initialization failed: ${error.message}`, error.stack);
|
|
179
|
+
resolve(false);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
AppConfig.add(settingsPromise);
|
|
183
|
+
}
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
if (options.connections) {
|
|
186
|
+
const connectionsPromise = new Promise((resolve) => {
|
|
187
|
+
try {
|
|
188
|
+
AppConfig._connections = new Connections(options.connections);
|
|
189
|
+
if (debug) {
|
|
190
|
+
DebugAndLog.debug("Connections initialized", AppConfig._connections.info());
|
|
191
|
+
}
|
|
192
|
+
resolve(true);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
DebugAndLog.error(`Connections initialization failed: ${error.message}`, error.stack);
|
|
195
|
+
resolve(false);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
AppConfig.add(connectionsPromise);
|
|
199
|
+
}
|
|
165
200
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
201
|
+
if (options.validations) {
|
|
202
|
+
const validationsPromise = new Promise((resolve) => {
|
|
203
|
+
try {
|
|
204
|
+
ClientRequest.init(options.validations);
|
|
205
|
+
if (debug) {
|
|
206
|
+
DebugAndLog.debug("ClientRequest initialized", ClientRequest.info());
|
|
207
|
+
}
|
|
208
|
+
resolve(true);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
DebugAndLog.error(`ClientRequest initialization failed: ${error.message}`, error.stack);
|
|
211
|
+
resolve(false);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
AppConfig.add(validationsPromise);
|
|
215
|
+
}
|
|
170
216
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
217
|
+
if (options.responses) {
|
|
218
|
+
const responsesPromise = new Promise((resolve) => {
|
|
219
|
+
try {
|
|
220
|
+
Response.init(options.responses);
|
|
221
|
+
if (debug) {
|
|
222
|
+
DebugAndLog.debug("Response initialized", Response.info());
|
|
223
|
+
}
|
|
224
|
+
resolve(true);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
DebugAndLog.error(`Response initialization failed: ${error.message}`, error.stack);
|
|
227
|
+
resolve(false);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
AppConfig.add(responsesPromise);
|
|
231
|
+
}
|
|
175
232
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
233
|
+
if (options.ssmParameters) {
|
|
234
|
+
AppConfig._ssmParameters = AppConfig._initParameters(options.ssmParameters);
|
|
235
|
+
AppConfig.add(AppConfig._ssmParameters);
|
|
236
|
+
}
|
|
180
237
|
|
|
181
|
-
|
|
238
|
+
return true;
|
|
182
239
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
240
|
+
} catch (error) {
|
|
241
|
+
DebugAndLog.error(`Could not initialize Config ${error.message}`, error.stack);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
186
244
|
}
|
|
187
|
-
|
|
245
|
+
;
|
|
188
246
|
|
|
189
247
|
/**
|
|
190
248
|
* Add a promise to AppConfig. Use AppConfig.promise() to ensure all are resolved.
|
|
@@ -299,13 +357,13 @@ class AppConfig {
|
|
|
299
357
|
|
|
300
358
|
/**
|
|
301
359
|
*
|
|
302
|
-
* @returns {Promise} A promise that resolves when the Config class has finished initializing
|
|
360
|
+
* @returns {Promise<array>} A promise that resolves when the Config class has finished initializing
|
|
303
361
|
*/
|
|
304
362
|
static promise() {
|
|
305
363
|
if (AppConfig._promise !== null ) { // Backwards compatibility
|
|
306
364
|
AppConfig._promises.push(AppConfig._promise);
|
|
307
365
|
}
|
|
308
|
-
return Promise.all
|
|
366
|
+
return Promise.all(AppConfig._promises);
|
|
309
367
|
};
|
|
310
368
|
|
|
311
369
|
|
|
@@ -473,8 +531,11 @@ module.exports = {
|
|
|
473
531
|
nodeVerMinor,
|
|
474
532
|
nodeVerMajorMinor,
|
|
475
533
|
AWS,
|
|
534
|
+
Aws: AWS,
|
|
476
535
|
AWSXRay,
|
|
536
|
+
AwsXRay: AWSXRay, // Alias
|
|
477
537
|
APIRequest,
|
|
538
|
+
ApiRequest: APIRequest, // Alias
|
|
478
539
|
ImmutableObject,
|
|
479
540
|
Timer,
|
|
480
541
|
DebugAndLog,
|
|
@@ -489,6 +550,7 @@ module.exports = {
|
|
|
489
550
|
AppConfig,
|
|
490
551
|
_ConfigSuperClass: AppConfig, // Alias
|
|
491
552
|
CachedSSMParameter,
|
|
553
|
+
CachedSsmParameter: CachedSSMParameter, // Alias
|
|
492
554
|
CachedSecret,
|
|
493
555
|
CachedParameterSecret,
|
|
494
556
|
CachedParameterSecrets,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ValidationExecutor - Executes validation functions with appropriate interfaces
|
|
3
|
+
*
|
|
4
|
+
* This class provides static methods for executing validation functions with either
|
|
5
|
+
* single-parameter or multi-parameter interfaces. It handles errors gracefully and
|
|
6
|
+
* logs validation failures.
|
|
7
|
+
*
|
|
8
|
+
* @private
|
|
9
|
+
* @class ValidationExecutor
|
|
10
|
+
*/
|
|
11
|
+
class ValidationExecutor {
|
|
12
|
+
/**
|
|
13
|
+
* Execute validation function with appropriate interface.
|
|
14
|
+
*
|
|
15
|
+
* Determines whether to pass a single value or an object based on the number
|
|
16
|
+
* of parameters specified. Handles validation errors gracefully by catching
|
|
17
|
+
* exceptions and logging them.
|
|
18
|
+
*
|
|
19
|
+
* @param {Function} validateFn - Validation function to execute
|
|
20
|
+
* @param {Array<string>} paramNames - Parameter names to validate
|
|
21
|
+
* @param {Object} paramValues - All parameter values available
|
|
22
|
+
* @returns {boolean} True if validation passes, false if fails or throws
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Single parameter validation
|
|
26
|
+
* const isValid = ValidationExecutor.execute(
|
|
27
|
+
* (value) => value.length > 0,
|
|
28
|
+
* ['id'],
|
|
29
|
+
* { id: '123' }
|
|
30
|
+
* );
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Multi-parameter validation
|
|
34
|
+
* const isValid = ValidationExecutor.execute(
|
|
35
|
+
* ({page, limit}) => page >= 1 && limit >= 1 && limit <= 100,
|
|
36
|
+
* ['page', 'limit'],
|
|
37
|
+
* { page: 1, limit: 10 }
|
|
38
|
+
* );
|
|
39
|
+
*/
|
|
40
|
+
static execute(validateFn, paramNames, paramValues) {
|
|
41
|
+
try {
|
|
42
|
+
if (paramNames.length === 1) {
|
|
43
|
+
// Single parameter: pass value directly
|
|
44
|
+
const value = paramValues[paramNames[0]];
|
|
45
|
+
return validateFn(value);
|
|
46
|
+
} else {
|
|
47
|
+
// Multiple parameters: pass object
|
|
48
|
+
const paramObj = {};
|
|
49
|
+
for (const name of paramNames) {
|
|
50
|
+
paramObj[name] = paramValues[name];
|
|
51
|
+
}
|
|
52
|
+
return validateFn(paramObj);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Log error and treat as validation failure
|
|
56
|
+
const tools = require("../tools/index.js");
|
|
57
|
+
tools.DebugAndLog.error(
|
|
58
|
+
`Validation function threw error for parameters [${paramNames.join(", ")}]: ${error?.message || "Unknown error"}`,
|
|
59
|
+
error?.stack
|
|
60
|
+
);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = ValidationExecutor;
|
|
@@ -0,0 +1,405 @@
|
|
|
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
|
+
// Check if parameter has a global validation function
|
|
320
|
+
if (this.#paramValidations[paramName] && typeof this.#paramValidations[paramName] === 'function') {
|
|
321
|
+
return {
|
|
322
|
+
validate: this.#paramValidations[paramName],
|
|
323
|
+
params: [paramName]
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Extract parameter names from route pattern.
|
|
332
|
+
* Extracts both path parameters (from {}) and query parameters (after ?).
|
|
333
|
+
* When ? is present, query parameters are added to path parameters.
|
|
334
|
+
* When ? is absent, only path parameters in {} are extracted.
|
|
335
|
+
*
|
|
336
|
+
* @private
|
|
337
|
+
* @param {string} routePattern - Route pattern to parse (e.g., 'product/{id}?key', 'POST:users/{userId}/posts/{postId}')
|
|
338
|
+
* @returns {Array<string>} Array of unique parameter names to validate
|
|
339
|
+
* @example
|
|
340
|
+
* // Path parameters only (no ? specification)
|
|
341
|
+
* #extractParamNames('product/{id}') // ['id']
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* // Multiple path parameters (no ? specification)
|
|
345
|
+
* #extractParamNames('users/{userId}/posts/{postId}') // ['userId', 'postId']
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* // Query parameters only (? specification, no path params)
|
|
349
|
+
* #extractParamNames('search?query,limit') // ['query', 'limit']
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* // Path and query parameters combined
|
|
353
|
+
* #extractParamNames('POST:product/{id}?key') // ['id', 'key'] - both path and query params
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* // Multiple path and query parameters
|
|
357
|
+
* #extractParamNames('users/{userId}/posts/{postId}?includeProfile') // ['userId', 'postId', 'includeProfile']
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* // Duplicate parameters removed
|
|
361
|
+
* #extractParamNames('product/{id}?id,key') // ['id', 'key'] (not ['id', 'id', 'key'])
|
|
362
|
+
*/
|
|
363
|
+
#extractParamNames(routePattern) {
|
|
364
|
+
if (!routePattern || typeof routePattern !== 'string') {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const params = [];
|
|
369
|
+
|
|
370
|
+
// Remove method prefix if present
|
|
371
|
+
let pattern = routePattern;
|
|
372
|
+
if (pattern.includes(':')) {
|
|
373
|
+
const parts = pattern.split(':', 2);
|
|
374
|
+
pattern = parts[1] || '';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Split route and query parts
|
|
378
|
+
const [routePart, queryPart] = pattern.split('?', 2);
|
|
379
|
+
|
|
380
|
+
// >! Extract path parameters from {} placeholders
|
|
381
|
+
if (routePart) {
|
|
382
|
+
const pathParamMatches = routePart.match(/\{([^}]+)\}/g);
|
|
383
|
+
if (pathParamMatches) {
|
|
384
|
+
for (const match of pathParamMatches) {
|
|
385
|
+
// Remove braces and add to params
|
|
386
|
+
const paramName = match.slice(1, -1).trim();
|
|
387
|
+
if (paramName.length > 0) {
|
|
388
|
+
params.push(paramName);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// >! Add query parameters to the list
|
|
395
|
+
if (queryPart) {
|
|
396
|
+
const queryParams = queryPart.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
397
|
+
params.push(...queryParams);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// >! Remove duplicates while preserving order
|
|
401
|
+
return [...new Set(params)];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
module.exports = ValidationMatcher;
|