@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.
@@ -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
- * @returns {string|null} The ETag value, or null if not present
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
- * console.log(`ETag: ${etag}`);
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
- * @returns {string|null} The Last-Modified header value in HTTP date format, or null if not present
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
- * console.log(`Last modified: ${lastModified}`);
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
- return ( headers !== null && key in headers) ? headers[key] : null
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
- if ( !("if-none-match" in connection.headers) && cache.getETag() !== null) {
3051
- connection.headers['if-none-match'] = cache.getETag();
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
- if (!("if-modified-since" in connection.headers) && cache.getLastModified() !== null) {
3054
- connection.headers['if-modified-since'] = cache.getLastModified();
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
  };