@bymax-one/nest-cache 1.2.2 → 1.2.4

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.
@@ -65,7 +65,8 @@ var CACHE_ERROR_CODES = {
65
65
  INVALID_SCOPE: "cache.invalid_scope",
66
66
  SCOPE_NOT_FOUND: "cache.scope_not_found",
67
67
  SCOPE_NOT_READABLE: "cache.scope_not_readable",
68
- KEY_NOT_IN_SCOPE: "cache.key_not_in_scope"
68
+ KEY_NOT_IN_SCOPE: "cache.key_not_in_scope",
69
+ INVALID_TTL: "cache.invalid_ttl"
69
70
  };
70
71
 
71
72
  // src/server/errors/cache-error-codes.ts
@@ -106,12 +107,17 @@ var CACHE_ERROR_MESSAGES = /* @__PURE__ */ new Map([
106
107
  [
107
108
  CACHE_ERROR_CODES.KEY_NOT_IN_SCOPE,
108
109
  "That key does not belong to the named administration scope."
110
+ ],
111
+ [
112
+ CACHE_ERROR_CODES.INVALID_TTL,
113
+ "A TTL in seconds must be a safe integer, and at least 1 for a write that sets one."
109
114
  ]
110
115
  ]);
111
116
  var CACHE_ERROR_STATUS = /* @__PURE__ */ new Map([
112
117
  [CACHE_ERROR_CODES.COMMAND_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT],
113
118
  [CACHE_ERROR_CODES.CONNECTION_LOST, HttpStatus.SERVICE_UNAVAILABLE],
114
119
  [CACHE_ERROR_CODES.INVALID_KEY, HttpStatus.BAD_REQUEST],
120
+ [CACHE_ERROR_CODES.INVALID_TTL, HttpStatus.BAD_REQUEST],
115
121
  [CACHE_ERROR_CODES.FLUSH_DISABLED_IN_PRODUCTION, HttpStatus.FORBIDDEN]
116
122
  ]);
117
123
  var CacheException = class extends HttpException {
@@ -709,7 +715,7 @@ KeyBuilder = __decorateClass([
709
715
  Injectable(),
710
716
  __decorateParam(0, Inject(BYMAX_CACHE_OPTIONS))
711
717
  ], KeyBuilder);
712
- var MAX_PREVIEW_LENGTH = 100;
718
+ var DESERIALIZATION_ERROR = "stored value is not valid JSON";
713
719
  var extractErrorMessage = (err) => err instanceof Error ? err.message : String(err);
714
720
  var JsonSerializer = class {
715
721
  /**
@@ -751,17 +757,17 @@ var JsonSerializer = class {
751
757
  * @param raw - The JSON string to decode.
752
758
  * @returns The decoded value, typed as `T`.
753
759
  * @throws {CacheException} `DESERIALIZATION_FAILED` when `raw` is not valid
754
- * JSON. `details.preview` carries at most {@link MAX_PREVIEW_LENGTH}
755
- * characters of `raw` (truncated with an ellipsis) to aid debugging without
756
- * leaking a large payload that may contain PII.
760
+ * JSON. `details` holds a fixed `error` text and the `length` of `raw`; no
761
+ * part of the stored value is echoed, as it may carry PII or a credential
762
+ * (CLAUDE.md §4).
757
763
  */
758
764
  deserialize(raw) {
759
765
  try {
760
766
  return JSON.parse(raw);
761
- } catch (err) {
767
+ } catch {
762
768
  throw new CacheException(CACHE_ERROR_CODES.DESERIALIZATION_FAILED, {
763
- error: extractErrorMessage(err),
764
- preview: raw.length > MAX_PREVIEW_LENGTH ? `${raw.substring(0, MAX_PREVIEW_LENGTH)}...` : raw
769
+ error: DESERIALIZATION_ERROR,
770
+ length: raw.length
765
771
  });
766
772
  }
767
773
  }
@@ -777,6 +783,17 @@ var resolveSerializer = (options, injected) => (
777
783
  options.serializer ?? injected ?? new JsonSerializer()
778
784
  );
779
785
 
786
+ // src/server/utils/ttl.ts
787
+ var MIN_WRITE_TTL_SECONDS = 1;
788
+ function assertTtlSeconds(ttlSeconds, minimum) {
789
+ if (!Number.isSafeInteger(ttlSeconds)) {
790
+ throw new CacheException(CACHE_ERROR_CODES.INVALID_TTL, { reason: "not_a_safe_integer" });
791
+ }
792
+ if (minimum !== void 0 && ttlSeconds < minimum) {
793
+ throw new CacheException(CACHE_ERROR_CODES.INVALID_TTL, { reason: "below_minimum", minimum });
794
+ }
795
+ }
796
+
780
797
  // src/server/services/cache.service.ts
781
798
  var PRODUCTION_ENV = "production";
782
799
  var SCAN_COUNT = 100;
@@ -837,14 +854,17 @@ var CacheService = class {
837
854
  * @param prefix - Entity-group prefix.
838
855
  * @param id - Entity id.
839
856
  * @param value - The value to store (passed through the serializer).
840
- * @param ttlSeconds - Optional expiry in seconds; omit for no expiration.
857
+ * @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
858
+ * no expiration.
841
859
  * @returns Resolves once the write completes.
842
860
  * @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
861
+ * @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
843
862
  */
844
863
  async set(prefix, id, value, ttlSeconds) {
845
864
  const key = this.keyBuilder.build(prefix, id);
846
865
  const raw = this.serializer.serialize(value);
847
866
  if (ttlSeconds !== void 0) {
867
+ assertTtlSeconds(ttlSeconds, MIN_WRITE_TTL_SECONDS);
848
868
  await this.connection.getClient().set(key, raw, "EX", ttlSeconds);
849
869
  } else {
850
870
  await this.connection.getClient().set(key, raw);
@@ -856,12 +876,15 @@ var CacheService = class {
856
876
  * @param prefix - Entity-group prefix.
857
877
  * @param id - Entity id.
858
878
  * @param value - The raw string to store as-is.
859
- * @param ttlSeconds - Optional expiry in seconds; omit for no expiration.
879
+ * @param ttlSeconds - Optional expiry in seconds, a safe integer of at least 1; omit for
880
+ * no expiration.
860
881
  * @returns Resolves once the write completes.
882
+ * @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
861
883
  */
862
884
  async setRaw(prefix, id, value, ttlSeconds) {
863
885
  const key = this.keyBuilder.build(prefix, id);
864
886
  if (ttlSeconds !== void 0) {
887
+ assertTtlSeconds(ttlSeconds, MIN_WRITE_TTL_SECONDS);
865
888
  await this.connection.getClient().set(key, value, "EX", ttlSeconds);
866
889
  } else {
867
890
  await this.connection.getClient().set(key, value);
@@ -874,13 +897,16 @@ var CacheService = class {
874
897
  * @param prefix - Entity-group prefix.
875
898
  * @param id - Entity id.
876
899
  * @param value - The value to store (passed through the serializer).
877
- * @param ttlSeconds - Optional expiry in seconds applied on the same atomic write.
900
+ * @param ttlSeconds - Optional expiry in seconds applied on the same atomic write, a safe
901
+ * integer of at least 1.
878
902
  * @returns `true` when the value was stored, `false` when the key already existed.
879
903
  * @throws {CacheException} `SERIALIZATION_FAILED` when `value` cannot be encoded.
904
+ * @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer of at least 1.
880
905
  */
881
906
  async setNx(prefix, id, value, ttlSeconds) {
882
907
  const key = this.keyBuilder.build(prefix, id);
883
908
  const raw = this.serializer.serialize(value);
909
+ if (ttlSeconds !== void 0) assertTtlSeconds(ttlSeconds, MIN_WRITE_TTL_SECONDS);
884
910
  const result = ttlSeconds !== void 0 ? await this.connection.getClient().set(key, raw, "EX", ttlSeconds, "NX") : await this.connection.getClient().set(key, raw, "NX");
885
911
  return result === "OK";
886
912
  }
@@ -952,10 +978,13 @@ var CacheService = class {
952
978
  *
953
979
  * @param prefix - Entity-group prefix.
954
980
  * @param id - Entity id.
955
- * @param ttlSeconds - Expiry in seconds.
981
+ * @param ttlSeconds - Expiry in seconds, a safe integer. As in Redis, zero or a negative
982
+ * value deletes the key.
956
983
  * @returns `true` when the timeout was set, `false` when the key does not exist.
984
+ * @throws {CacheException} `INVALID_TTL` when `ttlSeconds` is not a safe integer.
957
985
  */
958
986
  async expire(prefix, id, ttlSeconds) {
987
+ assertTtlSeconds(ttlSeconds, void 0);
959
988
  const result = await this.connection.getClient().expire(this.keyBuilder.build(prefix, id), ttlSeconds);
960
989
  return result === 1;
961
990
  }
@@ -1163,6 +1192,9 @@ var CacheService = class {
1163
1192
  * @param prefix - Entity-group prefix.
1164
1193
  * @param pattern - Glob pattern for the id segment, e.g. `'*'`.
1165
1194
  * @returns The matching fully-namespaced keys.
1195
+ * @throws {CacheException} `UNSUPPORTED_IN_CLUSTER` when called in cluster mode:
1196
+ * `KEYS` carries no key, so the cluster client sends it to one sampled node and the
1197
+ * reply would cover only that node's share of the keyspace.
1166
1198
  * @example
1167
1199
  * ```ts
1168
1200
  * await cache.keys('users', '*') // ['app:users:u_1', 'app:users:u_2']
@@ -1170,7 +1202,11 @@ var CacheService = class {
1170
1202
  */
1171
1203
  async keys(prefix, pattern) {
1172
1204
  const fullPattern = this.keyBuilder.build(prefix, pattern);
1173
- return this.connection.getClient().keys(fullPattern);
1205
+ const client = this.connection.getClient();
1206
+ if (!isScannableClient(client)) {
1207
+ throw new CacheException(CACHE_ERROR_CODES.UNSUPPORTED_IN_CLUSTER, { operation: "keys" });
1208
+ }
1209
+ return client.keys(fullPattern);
1174
1210
  }
1175
1211
  /**
1176
1212
  * Iterates keys matching a pattern under a prefix using a non-blocking cursor.
@@ -20,7 +20,8 @@ var CACHE_ERROR_CODES = {
20
20
  INVALID_SCOPE: "cache.invalid_scope",
21
21
  SCOPE_NOT_FOUND: "cache.scope_not_found",
22
22
  SCOPE_NOT_READABLE: "cache.scope_not_readable",
23
- KEY_NOT_IN_SCOPE: "cache.key_not_in_scope"
23
+ KEY_NOT_IN_SCOPE: "cache.key_not_in_scope",
24
+ INVALID_TTL: "cache.invalid_ttl"
24
25
  };
25
26
 
26
27
  // src/shared/constants/event-names.ts
@@ -97,6 +97,7 @@ declare const CACHE_ERROR_CODES: {
97
97
  readonly SCOPE_NOT_FOUND: "cache.scope_not_found";
98
98
  readonly SCOPE_NOT_READABLE: "cache.scope_not_readable";
99
99
  readonly KEY_NOT_IN_SCOPE: "cache.key_not_in_scope";
100
+ readonly INVALID_TTL: "cache.invalid_ttl";
100
101
  };
101
102
  /** Union of every cache error code string value. */
102
103
  type CacheErrorCode = (typeof CACHE_ERROR_CODES)[keyof typeof CACHE_ERROR_CODES];
@@ -97,6 +97,7 @@ declare const CACHE_ERROR_CODES: {
97
97
  readonly SCOPE_NOT_FOUND: "cache.scope_not_found";
98
98
  readonly SCOPE_NOT_READABLE: "cache.scope_not_readable";
99
99
  readonly KEY_NOT_IN_SCOPE: "cache.key_not_in_scope";
100
+ readonly INVALID_TTL: "cache.invalid_ttl";
100
101
  };
101
102
  /** Union of every cache error code string value. */
102
103
  type CacheErrorCode = (typeof CACHE_ERROR_CODES)[keyof typeof CACHE_ERROR_CODES];
@@ -18,7 +18,8 @@ var CACHE_ERROR_CODES = {
18
18
  INVALID_SCOPE: "cache.invalid_scope",
19
19
  SCOPE_NOT_FOUND: "cache.scope_not_found",
20
20
  SCOPE_NOT_READABLE: "cache.scope_not_readable",
21
- KEY_NOT_IN_SCOPE: "cache.key_not_in_scope"
21
+ KEY_NOT_IN_SCOPE: "cache.key_not_in_scope",
22
+ INVALID_TTL: "cache.invalid_ttl"
22
23
  };
23
24
 
24
25
  // src/shared/constants/event-names.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-cache",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Typed Redis cache for NestJS based on ioredis 6, with namespace strategy, Pub/Sub and Lua script management.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",