@63klabs/cache-data 1.3.6 → 1.3.8
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/{AI_CONTEXT.md → AGENTS.md} +164 -15
- package/CHANGELOG.md +76 -3
- package/CONTRIBUTING.md +19 -14
- package/README.md +42 -3
- package/eslint.config.js +53 -0
- package/package.json +28 -13
- package/src/lib/dao-cache.js +136 -11
- package/src/lib/tools/APIRequest.class.js +948 -91
- package/src/lib/tools/ClientRequest.class.js +34 -1
- package/src/lib/tools/Connections.classes.js +40 -3
- package/src/lib/tools/Response.class.js +11 -0
- package/src/lib/tools/generic.response.html.js +33 -0
- package/src/lib/tools/generic.response.json.js +40 -1
- package/src/lib/tools/generic.response.rss.js +33 -0
- package/src/lib/tools/generic.response.text.js +34 -1
- package/src/lib/tools/generic.response.xml.js +39 -0
- package/src/lib/tools/index.js +148 -49
- package/scripts/README.md +0 -175
- package/scripts/audit-documentation.mjs +0 -856
- package/scripts/review-documentation-files.mjs +0 -406
- package/scripts/setup-dev-environment.sh +0 -59
- package/scripts/verify-example-files.mjs +0 -194
package/src/lib/dao-cache.js
CHANGED
|
@@ -1683,6 +1683,45 @@ class Cache {
|
|
|
1683
1683
|
// Boolean("false") is true so we need to code for it. As long as it is not "false", trust Boolean()
|
|
1684
1684
|
return (( value !== "false") ? Boolean(value) : false );
|
|
1685
1685
|
};
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Validate that a header value is suitable for HTTP header assignment.
|
|
1689
|
+
* Returns true if the value is a non-empty string (except "undefined") or number, false otherwise.
|
|
1690
|
+
*
|
|
1691
|
+
* This method is used internally to validate header values before assignment
|
|
1692
|
+
* to prevent undefined, null, or invalid types from being used in HTTP headers.
|
|
1693
|
+
* The string "undefined" is explicitly rejected because it causes HTTP errors
|
|
1694
|
+
* when used as a header value.
|
|
1695
|
+
*
|
|
1696
|
+
* @param {*} value The value to validate
|
|
1697
|
+
* @returns {boolean} True if value is valid for HTTP header, false otherwise
|
|
1698
|
+
* @private
|
|
1699
|
+
* @example
|
|
1700
|
+
* Cache._isValidHeaderValue('text'); // true
|
|
1701
|
+
* Cache._isValidHeaderValue(123); // true
|
|
1702
|
+
* Cache._isValidHeaderValue(''); // false
|
|
1703
|
+
* Cache._isValidHeaderValue('undefined'); // false (string "undefined" is invalid)
|
|
1704
|
+
* Cache._isValidHeaderValue(null); // false
|
|
1705
|
+
* Cache._isValidHeaderValue(undefined); // false
|
|
1706
|
+
* Cache._isValidHeaderValue(NaN); // false
|
|
1707
|
+
* Cache._isValidHeaderValue(true); // false
|
|
1708
|
+
* Cache._isValidHeaderValue({}); // false
|
|
1709
|
+
* Cache._isValidHeaderValue([]); // false
|
|
1710
|
+
*/
|
|
1711
|
+
static _isValidHeaderValue(value) {
|
|
1712
|
+
if (value === null || value === undefined) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
const type = typeof value;
|
|
1716
|
+
if (type === 'string') {
|
|
1717
|
+
// Filter out empty strings and the string "undefined"
|
|
1718
|
+
return value.length > 0 && value !== 'undefined';
|
|
1719
|
+
}
|
|
1720
|
+
if (type === 'number') {
|
|
1721
|
+
return !isNaN(value);
|
|
1722
|
+
}
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1686
1725
|
|
|
1687
1726
|
/**
|
|
1688
1727
|
* Generate an ETag hash for cache validation. Creates a unique hash by combining
|
|
@@ -2252,12 +2291,18 @@ class Cache {
|
|
|
2252
2291
|
* Get the ETag header value from the cached data. The ETag is used for
|
|
2253
2292
|
* cache validation and conditional requests.
|
|
2254
2293
|
*
|
|
2255
|
-
*
|
|
2294
|
+
* Returns null if the ETag header is not present, is null, or is undefined.
|
|
2295
|
+
* This method uses getHeader() internally, which normalizes undefined values
|
|
2296
|
+
* to null for consistent behavior.
|
|
2297
|
+
*
|
|
2298
|
+
* @returns {string|null} The ETag value, or null if not present, null, or undefined
|
|
2256
2299
|
* @example
|
|
2257
2300
|
* const cache = new Cache(connection, cacheProfile);
|
|
2258
2301
|
* await cache.read();
|
|
2259
2302
|
* const etag = cache.getETag();
|
|
2260
|
-
*
|
|
2303
|
+
* if (etag !== null) {
|
|
2304
|
+
* console.log(`ETag: ${etag}`);
|
|
2305
|
+
* }
|
|
2261
2306
|
*/
|
|
2262
2307
|
getETag() {
|
|
2263
2308
|
return this.getHeader("etag");
|
|
@@ -2267,12 +2312,18 @@ class Cache {
|
|
|
2267
2312
|
* Get the Last-Modified header value from the cached data. This timestamp
|
|
2268
2313
|
* indicates when the cached content was last modified at the origin.
|
|
2269
2314
|
*
|
|
2270
|
-
*
|
|
2315
|
+
* Returns null if the Last-Modified header is not present, is null, or is undefined.
|
|
2316
|
+
* This method uses getHeader() internally, which normalizes undefined values
|
|
2317
|
+
* to null for consistent behavior.
|
|
2318
|
+
*
|
|
2319
|
+
* @returns {string|null} The Last-Modified header value in HTTP date format, or null if not present, null, or undefined
|
|
2271
2320
|
* @example
|
|
2272
2321
|
* const cache = new Cache(connection, cacheProfile);
|
|
2273
2322
|
* await cache.read();
|
|
2274
2323
|
* const lastModified = cache.getLastModified();
|
|
2275
|
-
*
|
|
2324
|
+
* if (lastModified !== null) {
|
|
2325
|
+
* console.log(`Last modified: ${lastModified}`);
|
|
2326
|
+
* }
|
|
2276
2327
|
*/
|
|
2277
2328
|
getLastModified() {
|
|
2278
2329
|
return this.getHeader("last-modified");
|
|
@@ -2435,18 +2486,36 @@ class Cache {
|
|
|
2435
2486
|
* Get a specific header value from the cached data by key. Header keys are
|
|
2436
2487
|
* case-insensitive (stored as lowercase).
|
|
2437
2488
|
*
|
|
2489
|
+
* Returns null if the header doesn't exist, has a null value, or has an undefined value.
|
|
2490
|
+
* This normalization ensures consistent behavior for conditional checks and prevents
|
|
2491
|
+
* undefined values from being passed to HTTP headers.
|
|
2492
|
+
*
|
|
2493
|
+
* Note: This method normalizes undefined to null. If a header key exists but has an
|
|
2494
|
+
* undefined value, this method returns null (never undefined). This ensures that
|
|
2495
|
+
* conditional checks like `!== null` work reliably.
|
|
2496
|
+
*
|
|
2438
2497
|
* @param {string} key The header key to retrieve (case-insensitive)
|
|
2439
|
-
* @returns {string|number|null} The header value, or null if the header doesn't exist
|
|
2498
|
+
* @returns {string|number|null} The header value, or null if the header doesn't exist, is null, or is undefined
|
|
2440
2499
|
* @example
|
|
2441
2500
|
* const cache = new Cache(connection, cacheProfile);
|
|
2442
2501
|
* await cache.read();
|
|
2443
2502
|
* const contentType = cache.getHeader('content-type');
|
|
2444
2503
|
* const etag = cache.getHeader('ETag'); // Case-insensitive
|
|
2445
2504
|
* console.log(`Content-Type: ${contentType}`);
|
|
2505
|
+
*
|
|
2506
|
+
* @example
|
|
2507
|
+
* // Undefined values are normalized to null
|
|
2508
|
+
* const undefinedHeader = cache.getHeader('missing-header');
|
|
2509
|
+
* console.log(undefinedHeader === null); // true (never undefined)
|
|
2446
2510
|
*/
|
|
2447
2511
|
getHeader(key) {
|
|
2448
2512
|
let headers = this.getHeaders();
|
|
2449
|
-
|
|
2513
|
+
if (headers === null || !(key in headers)) {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
// Normalize undefined to null for consistent behavior
|
|
2517
|
+
const value = headers[key];
|
|
2518
|
+
return (value === undefined || value === null) ? null : value;
|
|
2450
2519
|
};
|
|
2451
2520
|
|
|
2452
2521
|
/**
|
|
@@ -3047,11 +3116,23 @@ class CacheableDataAccess {
|
|
|
3047
3116
|
|
|
3048
3117
|
// add etag and last modified to connection
|
|
3049
3118
|
if ( !("headers" in connection)) { connection.headers = {}; }
|
|
3050
|
-
|
|
3051
|
-
|
|
3119
|
+
|
|
3120
|
+
// Sanitize existing headers to remove invalid values (including string "undefined")
|
|
3121
|
+
for (const key in connection.headers) {
|
|
3122
|
+
if (!Cache._isValidHeaderValue(connection.headers[key])) {
|
|
3123
|
+
delete connection.headers[key];
|
|
3124
|
+
tools.DebugAndLog.debug(`Removed invalid header value for "${key}": ${connection.headers[key]}`);
|
|
3125
|
+
}
|
|
3052
3126
|
}
|
|
3053
|
-
|
|
3054
|
-
|
|
3127
|
+
|
|
3128
|
+
const etag = cache.getETag();
|
|
3129
|
+
if ( !("if-none-match" in connection.headers) && Cache._isValidHeaderValue(etag)) {
|
|
3130
|
+
connection.headers['if-none-match'] = etag;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
const lastModified = cache.getLastModified();
|
|
3134
|
+
if (!("if-modified-since" in connection.headers) && Cache._isValidHeaderValue(lastModified)) {
|
|
3135
|
+
connection.headers['if-modified-since'] = lastModified;
|
|
3055
3136
|
}
|
|
3056
3137
|
|
|
3057
3138
|
// request data from original source
|
|
@@ -3095,7 +3176,51 @@ class CacheableDataAccess {
|
|
|
3095
3176
|
};
|
|
3096
3177
|
};
|
|
3097
3178
|
|
|
3179
|
+
/**
|
|
3180
|
+
* TestHarness provides access to internal classes for testing purposes only.
|
|
3181
|
+
* This class should NEVER be used in production code - it exists solely to
|
|
3182
|
+
* enable proper testing of the cache-dao module without exposing internal
|
|
3183
|
+
* implementation details to end users.
|
|
3184
|
+
*
|
|
3185
|
+
* @private
|
|
3186
|
+
* @example
|
|
3187
|
+
* // In tests only - DO NOT use in production
|
|
3188
|
+
* import { TestHarness } from '../../src/lib/dao-cache.js';
|
|
3189
|
+
* const { CacheData } = TestHarness.getInternals();
|
|
3190
|
+
*
|
|
3191
|
+
* // Mock CacheData.read for testing
|
|
3192
|
+
* const originalRead = CacheData.read;
|
|
3193
|
+
* CacheData.read = async () => ({ cache: { body: 'test', headers: {} } });
|
|
3194
|
+
* // ... run tests ...
|
|
3195
|
+
* CacheData.read = originalRead; // Restore
|
|
3196
|
+
*/
|
|
3197
|
+
class TestHarness {
|
|
3198
|
+
/**
|
|
3199
|
+
* Get access to internal classes for testing purposes.
|
|
3200
|
+
* WARNING: This method is for testing only and should never be used in production.
|
|
3201
|
+
*
|
|
3202
|
+
* @returns {{CacheData: typeof CacheData, S3Cache: typeof S3Cache, DynamoDbCache: typeof DynamoDbCache}} Object containing internal classes
|
|
3203
|
+
* @example
|
|
3204
|
+
* // In tests only - DO NOT use in production
|
|
3205
|
+
* const { CacheData, S3Cache, DynamoDbCache } = TestHarness.getInternals();
|
|
3206
|
+
*
|
|
3207
|
+
* // Mock CacheData.read for property-based testing
|
|
3208
|
+
* const originalRead = CacheData.read;
|
|
3209
|
+
* CacheData.read = async () => ({
|
|
3210
|
+
* cache: { body: 'test', headers: { 'content-type': 'application/json' }, expires: 1234567890, statusCode: '200' }
|
|
3211
|
+
* });
|
|
3212
|
+
*/
|
|
3213
|
+
static getInternals() {
|
|
3214
|
+
return {
|
|
3215
|
+
CacheData,
|
|
3216
|
+
S3Cache,
|
|
3217
|
+
DynamoDbCache
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3098
3222
|
module.exports = {
|
|
3099
3223
|
Cache,
|
|
3100
|
-
CacheableDataAccess
|
|
3224
|
+
CacheableDataAccess,
|
|
3225
|
+
TestHarness
|
|
3101
3226
|
};
|