@common-stack/store-redis 8.3.1-alpha.0 → 8.3.1-alpha.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.
Files changed (35) hide show
  1. package/lib/config/env-config.d.ts +4 -0
  2. package/lib/config/env-config.js +51 -0
  3. package/lib/config/env-config.js.map +1 -0
  4. package/lib/config/index.d.ts +1 -0
  5. package/lib/core/index.d.ts +0 -5
  6. package/lib/core/ioredis.d.ts +4 -2
  7. package/lib/core/ioredis.js +10 -1
  8. package/lib/core/ioredis.js.map +1 -1
  9. package/lib/core/upstash-redis.d.ts +5 -2
  10. package/lib/core/upstash-redis.js +22 -1
  11. package/lib/core/upstash-redis.js.map +1 -1
  12. package/lib/index.d.ts +1 -1
  13. package/lib/index.js +1 -1
  14. package/lib/module.d.ts +6 -1
  15. package/lib/module.js +6 -2
  16. package/lib/module.js.map +1 -1
  17. package/lib/plugins/index.d.ts +2 -0
  18. package/lib/plugins/invalidateCachePlugin.d.ts +8 -0
  19. package/lib/plugins/invalidateCachePlugin.js +99 -0
  20. package/lib/plugins/invalidateCachePlugin.js.map +1 -0
  21. package/lib/plugins/responseCachePlugin.d.ts +11 -0
  22. package/lib/plugins/responseCachePlugin.js +63 -0
  23. package/lib/plugins/responseCachePlugin.js.map +1 -0
  24. package/lib/services/RedisCacheManager.d.ts +24 -2
  25. package/lib/services/RedisCacheManager.js +107 -31
  26. package/lib/services/RedisCacheManager.js.map +1 -1
  27. package/lib/templates/repositories/GraphQLCacheContext.ts.template +14 -0
  28. package/lib/{interfaces/redis.d.ts → templates/repositories/RedisClient.ts.template} +2 -3
  29. package/package.json +12 -10
  30. package/lib/__tests__/redis-key-builder.test.d.ts +0 -1
  31. package/lib/__tests__/redis-key-sanitizer.test.d.ts +0 -1
  32. package/lib/core/index.js +0 -6
  33. package/lib/core/index.js.map +0 -1
  34. package/lib/interfaces/index.d.ts +0 -1
  35. /package/lib/templates/repositories/{IRedisKeyBuilder.ts.template → RedisKeyBuilder.ts.template} +0 -0
@@ -0,0 +1,4 @@
1
+ import * as envalid from 'envalid';
2
+ export declare const config: Readonly<{
3
+ APP_NAME: string;
4
+ } & envalid.CleanedEnvAccessors>;
@@ -0,0 +1,51 @@
1
+ import*as envalid from'envalid';const { str, bool, json, num } = envalid;
2
+ const config = envalid.cleanEnv(process.env, {
3
+ APP_NAME: str({ default: 'CDMBASE' }),
4
+ // NODE_ENV: str({ default: 'production', choices: ['production', 'staging', 'development', 'test'] }),
5
+ // NATS_URL: str(),
6
+ // NATS_USER: str(),
7
+ // NATS_PW: str(),
8
+ // GRAPHQL_ENDPOINT: str({ default: '/graphql' }),
9
+ // BACKEND_URL: str(),
10
+ // GRAPHQL_URL: str(),
11
+ // CLIENT_URL: str(),
12
+ // MONGO_URL: str(),
13
+ // REDIS_CLUSTER_URL: json({
14
+ // devDefault: '[{"port":6379,"host":"localhost"}]',
15
+ // example: '[{"port":6379,"host":"localhost"}]',
16
+ // }),
17
+ // REDIS_URL: str({ devDefault: 'localhost' }),
18
+ // REDIS_CLUSTER_ENABLED: bool({ devDefault: false }),
19
+ // REDIS_SENTINEL_ENABLED: bool({ devDefault: true }),
20
+ // CONNECTION_ID: str({ devDefault: 'CONNECTION_ID' }),
21
+ // NAMESPACE: str({ default: 'default' }),
22
+ // // Comma-separated list of namespaces to enable cross-namespace communication with
23
+ // // e.g., "api-admin,travel-api,billing-api"
24
+ // CROSS_NAMESPACES: str({ default: 'api-admin-12, api-admin' }),
25
+ // LOG_LEVEL: str({ default: 'info', choices: ['info', 'debug', 'trace'] }),
26
+ // METRICS_CONFIG: json({
27
+ // default: JSON.stringify({
28
+ // enabled: false,
29
+ // port: 3030,
30
+ // path: '/metrics',
31
+ // }),
32
+ // devDefault: JSON.stringify({
33
+ // enabled: false,
34
+ // port: 3031,
35
+ // path: '/metrics',
36
+ // }),
37
+ // example: '{"enabled":true,"port":3030,"path":"/metrics"}',
38
+ // }),
39
+ // INNGEST_EVENT_KEY: str({ devDefault: 'dummy' }),
40
+ // INNGEST_SIGNING_KEY: str({ devDefault: 'dummy' }),
41
+ // INNGEST_IS_DEV: bool({ devDefault: true, default: false }),
42
+ // INNGEST_ENVIRONMENT: str({ devDefault: 'development', default: 'production' }),
43
+ // INNGEST_BASE_URL: str({ devDefault: 'http://localhost:8288', default: 'https://api.inngest.com' }),
44
+ // INNGEST_CLIENT_ID: str({ devDefault: '', default: '' }),
45
+ // // Stable application ID used for Inngest client registration across deployments
46
+ // APPLICATION_ID: str({ devDefault: '', default: '' }),
47
+ // // WebSocket connection limits per pod instance
48
+ // // Recommended: ~20000 per 1Gi memory
49
+ // WEBSOCKET_MAX_CONNECTIONS: num({ default: 20000, devDefault: 10000 }),
50
+ // WEBSOCKET_LIMIT_ENABLED: bool({ default: true, devDefault: false }),
51
+ });export{config};//# sourceMappingURL=env-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-config.js","sources":["../../src/config/env-config.ts"],"sourcesContent":["import * as envalid from 'envalid';\n\nconst { str, bool, json, num } = envalid;\n\nexport const config = envalid.cleanEnv(process.env, {\n APP_NAME: str({ default: 'CDMBASE' }),\n\n // NODE_ENV: str({ default: 'production', choices: ['production', 'staging', 'development', 'test'] }),\n // NATS_URL: str(),\n // NATS_USER: str(),\n // NATS_PW: str(),\n // GRAPHQL_ENDPOINT: str({ default: '/graphql' }),\n // BACKEND_URL: str(),\n // GRAPHQL_URL: str(),\n // CLIENT_URL: str(),\n // MONGO_URL: str(),\n // REDIS_CLUSTER_URL: json({\n // devDefault: '[{\"port\":6379,\"host\":\"localhost\"}]',\n // example: '[{\"port\":6379,\"host\":\"localhost\"}]',\n // }),\n // REDIS_URL: str({ devDefault: 'localhost' }),\n // REDIS_CLUSTER_ENABLED: bool({ devDefault: false }),\n // REDIS_SENTINEL_ENABLED: bool({ devDefault: true }),\n // CONNECTION_ID: str({ devDefault: 'CONNECTION_ID' }),\n\n // NAMESPACE: str({ default: 'default' }),\n // // Comma-separated list of namespaces to enable cross-namespace communication with\n // // e.g., \"api-admin,travel-api,billing-api\"\n // CROSS_NAMESPACES: str({ default: 'api-admin-12, api-admin' }),\n // LOG_LEVEL: str({ default: 'info', choices: ['info', 'debug', 'trace'] }),\n // METRICS_CONFIG: json({\n // default: JSON.stringify({\n // enabled: false,\n // port: 3030,\n // path: '/metrics',\n // }),\n // devDefault: JSON.stringify({\n // enabled: false,\n // port: 3031,\n // path: '/metrics',\n // }),\n // example: '{\"enabled\":true,\"port\":3030,\"path\":\"/metrics\"}',\n // }),\n // INNGEST_EVENT_KEY: str({ devDefault: 'dummy' }),\n // INNGEST_SIGNING_KEY: str({ devDefault: 'dummy' }),\n // INNGEST_IS_DEV: bool({ devDefault: true, default: false }),\n // INNGEST_ENVIRONMENT: str({ devDefault: 'development', default: 'production' }),\n // INNGEST_BASE_URL: str({ devDefault: 'http://localhost:8288', default: 'https://api.inngest.com' }),\n // INNGEST_CLIENT_ID: str({ devDefault: '', default: '' }),\n // // Stable application ID used for Inngest client registration across deployments\n // APPLICATION_ID: str({ devDefault: '', default: '' }),\n\n // // WebSocket connection limits per pod instance\n // // Recommended: ~20000 per 1Gi memory\n // WEBSOCKET_MAX_CONNECTIONS: num({ default: 20000, devDefault: 10000 }),\n // WEBSOCKET_LIMIT_ENABLED: bool({ default: true, devDefault: false }),\n});\n"],"names":[],"mappings":"gCAEA,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;AAE5B,MAAA,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE;IAChD,QAAQ,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDxC,CAAA"}
@@ -0,0 +1 @@
1
+ export * from './env-config';
@@ -1,8 +1,3 @@
1
- /**
2
- * Core utilities for Redis operations
3
- * @packageDocumentation
4
- */
5
1
  export * from './ioredis';
6
2
  export * from './upstash-redis';
7
3
  export * from './keyBuilder';
8
- export declare const __esModule = true;
@@ -1,15 +1,17 @@
1
1
  import Redis from 'ioredis';
2
- import { IRedisClient } from 'common/server';
2
+ import type { IRedisClient } from 'common/server';
3
3
  export declare class IORedisClient implements IRedisClient {
4
4
  private redis;
5
5
  constructor(config: {
6
6
  url: string;
7
7
  });
8
- get(key: string): Promise<any | null>;
8
+ get(key: string, callback?: (...args: any[]) => void): Promise<any | null>;
9
9
  set(key: string, value: string, options?: {
10
10
  ex?: number;
11
11
  }): Promise<string>;
12
12
  delMulti(keys: string[]): Promise<any>;
13
13
  del(key: string): Promise<number>;
14
+ keys(pattern: string): Promise<string[]>;
14
15
  on(event: string, cb: (...args: any[]) => void): Redis;
16
+ once(event: string, cb: (...args: any[]) => void): Redis;
15
17
  }
@@ -3,7 +3,10 @@ import Redis from'ioredis';class IORedisClient {
3
3
  constructor(config) {
4
4
  this.redis = new Redis(config.url);
5
5
  }
6
- async get(key) {
6
+ async get(key, callback) {
7
+ if (callback) {
8
+ return this.redis.get(key, callback);
9
+ }
7
10
  const data = await this.redis.get(key);
8
11
  return data;
9
12
  }
@@ -21,7 +24,13 @@ import Redis from'ioredis';class IORedisClient {
21
24
  async del(key) {
22
25
  return await this.redis.del(key);
23
26
  }
27
+ async keys(pattern) {
28
+ return await this.redis.keys(pattern);
29
+ }
24
30
  on(event, cb) {
25
31
  return this.redis.on(event, cb);
26
32
  }
33
+ once(event, cb) {
34
+ return this.redis.once(event, cb);
35
+ }
27
36
  }export{IORedisClient};//# sourceMappingURL=ioredis.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ioredis.js","sources":["../../src/core/ioredis.ts"],"sourcesContent":["import Redis from 'ioredis';\nimport { IRedisClient } from 'common/server';\n\nexport class IORedisClient implements IRedisClient {\n private redis: Redis;\n\n constructor(config: { url: string }) {\n this.redis = new Redis(config.url);\n }\n\n async get(key: string): Promise<any | null> {\n const data = await this.redis.get(key);\n return data;\n }\n\n async set(key: string, value: string, options?: { ex?: number }): Promise<string> {\n if (options?.ex) {\n return await this.redis.set(key, value, 'EX', options?.ex);\n } else {\n return await this.redis.set(key, value);\n }\n }\n\n async delMulti(keys: string[]): Promise<any> {\n return await this.redis.del(keys);\n }\n\n async del(key: string): Promise<number> {\n return await this.redis.del(key);\n }\n\n on(event: string, cb: (...args: any[]) => void) {\n return this.redis.on(event, cb);\n }\n}\n"],"names":[],"mappings":"iCAGa,aAAa,CAAA;AACd,IAAA,KAAK,CAAQ;AAErB,IAAA,WAAA,CAAY,MAAuB,EAAA;QAC/B,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;KACtC;IAED,MAAM,GAAG,CAAC,GAAW,EAAA;QACjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACvC,QAAA,OAAO,IAAI,CAAC;KACf;AAED,IAAA,MAAM,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,OAAyB,EAAA;AAC3D,QAAA,IAAI,OAAO,EAAE,EAAE,EAAE;AACb,YAAA,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;SAC9D;aAAM;YACH,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;SAC3C;KACJ;IAED,MAAM,QAAQ,CAAC,IAAc,EAAA;QACzB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;KACrC;IAED,MAAM,GAAG,CAAC,GAAW,EAAA;QACjB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;KACpC;IAED,EAAE,CAAC,KAAa,EAAE,EAA4B,EAAA;QAC1C,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;KACnC;AACJ"}
1
+ {"version":3,"file":"ioredis.js","sources":["../../src/core/ioredis.ts"],"sourcesContent":["import Redis from 'ioredis';\nimport type { IRedisClient } from 'common/server';\n\nexport class IORedisClient implements IRedisClient {\n private redis: Redis;\n\n constructor(config: { url: string }) {\n this.redis = new Redis(config.url);\n }\n\n async get(key: string, callback?: (...args: any[]) => void): Promise<any | null> {\n if (callback) {\n return this.redis.get(key, callback);\n }\n const data = await this.redis.get(key);\n return data;\n }\n\n async set(key: string, value: string, options?: { ex?: number }): Promise<string> {\n if (options?.ex) {\n return await this.redis.set(key, value, 'EX', options?.ex);\n } else {\n return await this.redis.set(key, value);\n }\n }\n\n async delMulti(keys: string[]): Promise<any> {\n return await this.redis.del(keys);\n }\n\n async del(key: string): Promise<number> {\n return await this.redis.del(key);\n }\n\n async keys(pattern: string): Promise<string[]> {\n return await this.redis.keys(pattern);\n }\n\n on(event: string, cb: (...args: any[]) => void) {\n return this.redis.on(event, cb);\n }\n\n once(event: string, cb: (...args: any[]) => void) {\n return this.redis.once(event, cb);\n }\n}\n"],"names":[],"mappings":"iCAGa,aAAa,CAAA;AACd,IAAA,KAAK,CAAQ;AAErB,IAAA,WAAA,CAAY,MAAuB,EAAA;QAC/B,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;KACtC;AAED,IAAA,MAAM,GAAG,CAAC,GAAW,EAAE,QAAmC,EAAA;QACtD,IAAI,QAAQ,EAAE;YACV,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;SACxC;QACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACvC,QAAA,OAAO,IAAI,CAAC;KACf;AAED,IAAA,MAAM,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,OAAyB,EAAA;AAC3D,QAAA,IAAI,OAAO,EAAE,EAAE,EAAE;AACb,YAAA,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;SAC9D;aAAM;YACH,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;SAC3C;KACJ;IAED,MAAM,QAAQ,CAAC,IAAc,EAAA;QACzB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;KACrC;IAED,MAAM,GAAG,CAAC,GAAW,EAAA;QACjB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;KACpC;IAED,MAAM,IAAI,CAAC,OAAe,EAAA;QACtB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;KACzC;IAED,EAAE,CAAC,KAAa,EAAE,EAA4B,EAAA;QAC1C,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;KACnC;IAED,IAAI,CAAC,KAAa,EAAE,EAA4B,EAAA;QAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;KACrC;AACJ"}
@@ -1,14 +1,17 @@
1
- import { IRedisClient } from 'common/server';
1
+ import type { IRedisClient } from 'common/server';
2
2
  export declare class UpstashRedisClient implements IRedisClient {
3
3
  private redis;
4
4
  constructor(config: {
5
5
  url: string;
6
6
  token?: string;
7
7
  });
8
- get(key: string): Promise<any | null>;
8
+ get(key: string, callback?: (...args: any[]) => void): Promise<any | null>;
9
9
  set(key: string, value: string, options?: {
10
10
  ex?: number;
11
11
  }): Promise<string>;
12
12
  delMulti(keys: string[]): Promise<any>;
13
13
  del(key: string): Promise<number>;
14
+ keys(pattern: string): Promise<string[]>;
15
+ on(event: string, cb: (...args: any[]) => void): this;
16
+ once(event: string, cb: (...args: any[]) => void): this;
14
17
  }
@@ -3,7 +3,13 @@ import {Redis}from'@upstash/redis/cloudflare';class UpstashRedisClient {
3
3
  constructor(config) {
4
4
  this.redis = new Redis({ url: config.url, token: config.token });
5
5
  }
6
- async get(key) {
6
+ async get(key, callback) {
7
+ if (callback) {
8
+ // Upstash doesn't support callbacks, emulate by calling callback with result
9
+ const result = await this.redis.get(key);
10
+ callback(null, result);
11
+ return result;
12
+ }
7
13
  return await this.redis.get(key);
8
14
  }
9
15
  async set(key, value, options) {
@@ -20,4 +26,19 @@ import {Redis}from'@upstash/redis/cloudflare';class UpstashRedisClient {
20
26
  async del(key) {
21
27
  return await this.redis.del(key);
22
28
  }
29
+ async keys(pattern) {
30
+ return await this.redis.keys(pattern);
31
+ }
32
+ on(event, cb) {
33
+ // Upstash Redis doesn't support event listeners like ioredis
34
+ // This is a no-op implementation for interface compatibility
35
+ console.warn('Upstash Redis does not support event listeners');
36
+ return this;
37
+ }
38
+ once(event, cb) {
39
+ // Upstash Redis doesn't support event listeners like ioredis
40
+ // This is a no-op implementation for interface compatibility
41
+ console.warn('Upstash Redis does not support event listeners');
42
+ return this;
43
+ }
23
44
  }export{UpstashRedisClient};//# sourceMappingURL=upstash-redis.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"upstash-redis.js","sources":["../../src/core/upstash-redis.ts"],"sourcesContent":["import { Redis } from '@upstash/redis/cloudflare';\nimport { IRedisClient } from 'common/server';\n\nexport class UpstashRedisClient implements IRedisClient {\n private redis: Redis;\n\n constructor(config: { url: string; token?: string }) {\n this.redis = new Redis({ url: config.url, token: config.token });\n }\n\n async get(key: string): Promise<any | null> {\n return await this.redis.get(key);\n }\n\n async set(key: string, value: string, options?: { ex?: number }): Promise<string> {\n if (options?.ex) {\n return await this.redis.set(key, value, { ex: options?.ex });\n } else {\n return await this.redis.set(key, value);\n }\n }\n\n async delMulti(keys: string[]): Promise<any> {\n return await Promise.all(keys.map((key) => this.redis.del(key)));\n }\n\n async del(key: string): Promise<number> {\n return await this.redis.del(key);\n }\n}\n"],"names":[],"mappings":"oDAGa,kBAAkB,CAAA;AACnB,IAAA,KAAK,CAAQ;AAErB,IAAA,WAAA,CAAY,MAAuC,EAAA;QAC/C,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;KACpE;IAED,MAAM,GAAG,CAAC,GAAW,EAAA;QACjB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;KACpC;AAED,IAAA,MAAM,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,OAAyB,EAAA;AAC3D,QAAA,IAAI,OAAO,EAAE,EAAE,EAAE;AACb,YAAA,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;SAChE;aAAM;YACH,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;SAC3C;KACJ;IAED,MAAM,QAAQ,CAAC,IAAc,EAAA;QACzB,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KACpE;IAED,MAAM,GAAG,CAAC,GAAW,EAAA;QACjB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;KACpC;AACJ"}
1
+ {"version":3,"file":"upstash-redis.js","sources":["../../src/core/upstash-redis.ts"],"sourcesContent":["import { Redis } from '@upstash/redis/cloudflare';\nimport type { IRedisClient } from 'common/server';\n\nexport class UpstashRedisClient implements IRedisClient {\n private redis: Redis;\n\n constructor(config: { url: string; token?: string }) {\n this.redis = new Redis({ url: config.url, token: config.token });\n }\n\n async get(key: string, callback?: (...args: any[]) => void): Promise<any | null> {\n if (callback) {\n // Upstash doesn't support callbacks, emulate by calling callback with result\n const result = await this.redis.get(key);\n callback(null, result);\n return result;\n }\n return await this.redis.get(key);\n }\n\n async set(key: string, value: string, options?: { ex?: number }): Promise<string> {\n if (options?.ex) {\n return await this.redis.set(key, value, { ex: options?.ex });\n } else {\n return await this.redis.set(key, value);\n }\n }\n\n async delMulti(keys: string[]): Promise<any> {\n return await Promise.all(keys.map((key) => this.redis.del(key)));\n }\n\n async del(key: string): Promise<number> {\n return await this.redis.del(key);\n }\n\n async keys(pattern: string): Promise<string[]> {\n return await this.redis.keys(pattern);\n }\n\n on(event: string, cb: (...args: any[]) => void) {\n // Upstash Redis doesn't support event listeners like ioredis\n // This is a no-op implementation for interface compatibility\n console.warn('Upstash Redis does not support event listeners');\n return this;\n }\n\n once(event: string, cb: (...args: any[]) => void) {\n // Upstash Redis doesn't support event listeners like ioredis\n // This is a no-op implementation for interface compatibility\n console.warn('Upstash Redis does not support event listeners');\n return this;\n }\n}\n"],"names":[],"mappings":"oDAGa,kBAAkB,CAAA;AACnB,IAAA,KAAK,CAAQ;AAErB,IAAA,WAAA,CAAY,MAAuC,EAAA;QAC/C,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;KACpE;AAED,IAAA,MAAM,GAAG,CAAC,GAAW,EAAE,QAAmC,EAAA;QACtD,IAAI,QAAQ,EAAE;;YAEV,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACzC,YAAA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACvB,YAAA,OAAO,MAAM,CAAC;SACjB;QACD,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;KACpC;AAED,IAAA,MAAM,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,OAAyB,EAAA;AAC3D,QAAA,IAAI,OAAO,EAAE,EAAE,EAAE;AACb,YAAA,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;SAChE;aAAM;YACH,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;SAC3C;KACJ;IAED,MAAM,QAAQ,CAAC,IAAc,EAAA;QACzB,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KACpE;IAED,MAAM,GAAG,CAAC,GAAW,EAAA;QACjB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;KACpC;IAED,MAAM,IAAI,CAAC,OAAe,EAAA;QACtB,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;KACzC;IAED,EAAE,CAAC,KAAa,EAAE,EAA4B,EAAA;;;AAG1C,QAAA,OAAO,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;AAC/D,QAAA,OAAO,IAAI,CAAC;KACf;IAED,IAAI,CAAC,KAAa,EAAE,EAA4B,EAAA;;;AAG5C,QAAA,OAAO,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;AAC/D,QAAA,OAAO,IAAI,CAAC;KACf;AACJ"}
package/lib/index.d.ts CHANGED
@@ -11,5 +11,5 @@
11
11
  */
12
12
  export * from './core/index.js';
13
13
  export * from './services';
14
- export * from './interfaces';
15
14
  export * from './services';
15
+ export * from './plugins';
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- export{__esModule}from'./core/index.js';export{RedisCacheManager}from'./services/RedisCacheManager.js';export{IORedisClient}from'./core/ioredis.js';export{UpstashRedisClient}from'./core/upstash-redis.js';export{escapeRedisPattern,isHashLikeTenantId,isValidRedisKey,sanitizeRedisKey,sanitizeRedisKeyComponent}from'./core/keyBuilder/sanitize-redis-key.js';export{buildRedisKey,buildRedisKeyPattern}from'./core/keyBuilder/index.js';export{RedisNamespace,buildRedisKeyPatternWithNamespace,buildRedisKeyWithNamespace,extractNamespaceFromRedisKey,extractTenantIdFromRedisKey,isValidRedisKeyWithNamespace,parseRedisKey}from'./core/keyBuilder/redis-key-builder.js';export{generateQueryCacheKey}from'./core/keyBuilder/generate-query-cache-key.js';//# sourceMappingURL=index.js.map
1
+ export{IORedisClient}from'./core/ioredis.js';export{UpstashRedisClient}from'./core/upstash-redis.js';export{buildRedisKey,buildRedisKeyPattern}from'./core/keyBuilder/index.js';export{RedisCacheManager}from'./services/RedisCacheManager.js';export{invalidateCachePlugin}from'./plugins/invalidateCachePlugin.js';export{responseCachePlugin}from'./plugins/responseCachePlugin.js';export{escapeRedisPattern,isHashLikeTenantId,isValidRedisKey,sanitizeRedisKey,sanitizeRedisKeyComponent}from'./core/keyBuilder/sanitize-redis-key.js';export{RedisNamespace,buildRedisKeyPatternWithNamespace,buildRedisKeyWithNamespace,extractNamespaceFromRedisKey,extractTenantIdFromRedisKey,isValidRedisKeyWithNamespace,parseRedisKey}from'./core/keyBuilder/redis-key-builder.js';export{generateQueryCacheKey}from'./core/keyBuilder/generate-query-cache-key.js';//# sourceMappingURL=index.js.map
package/lib/module.d.ts CHANGED
@@ -1,2 +1,7 @@
1
- declare const _default: any;
1
+ import { Feature } from '@common-stack/server-core';
2
+ import type { interfaces } from 'inversify';
3
+ export declare const redisServiceGen: (container: interfaces.Container) => {
4
+ redisCacheManager: IRedisCacheManager;
5
+ };
6
+ declare const _default: Feature<any, any>;
2
7
  export default _default;
package/lib/module.js CHANGED
@@ -1,4 +1,8 @@
1
- import {Feature}from'@common-stack/server-core';import {infraContainer}from'./containers/container.js';var module = new Feature({
1
+ import {Feature}from'@common-stack/server-core';import {SERVER_TYPES}from'common/server';import {infraContainer}from'./containers/container.js';const redisServiceGen = (container) => ({
2
+ redisCacheManager: container.get(SERVER_TYPES.RedisCacheManager),
3
+ });
4
+ var module = new Feature({
2
5
  createContainerFunc: [infraContainer],
6
+ createServiceFunc: [redisServiceGen],
3
7
  createMicroServiceContainerFunc: [infraContainer],
4
- });export{module as default};//# sourceMappingURL=module.js.map
8
+ });export{module as default,redisServiceGen};//# sourceMappingURL=module.js.map
package/lib/module.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"module.js","sources":["../src/module.ts"],"sourcesContent":["import { Feature } from '@common-stack/server-core';\nimport { infraContainer } from './containers';\n\nexport default new Feature({\n createContainerFunc: [infraContainer],\n createMicroServiceContainerFunc: [infraContainer],\n});\n"],"names":[],"mappings":"uGAGA,aAAe,IAAI,OAAO,CAAC;IACvB,mBAAmB,EAAE,CAAC,cAAc,CAAC;IACrC,+BAA+B,EAAE,CAAC,cAAc,CAAC;AACpD,CAAA,CAAC"}
1
+ {"version":3,"file":"module.js","sources":["../src/module.ts"],"sourcesContent":["import { Feature } from '@common-stack/server-core';\nimport type { interfaces } from 'inversify';\nimport { type IRedisCacheManager, SERVER_TYPES } from 'common/server';\nimport { infraContainer } from './containers';\n\nexport const redisServiceGen = (container: interfaces.Container) => ({\n redisCacheManager: container.get<IRedisCacheManager>(SERVER_TYPES.RedisCacheManager),\n});\n\nexport default new Feature({\n createContainerFunc: [infraContainer],\n createServiceFunc: [redisServiceGen],\n createMicroServiceContainerFunc: [infraContainer],\n});\n"],"names":[],"mappings":"sJAKa,eAAe,GAAG,CAAC,SAA+B,MAAM;IACjE,iBAAiB,EAAE,SAAS,CAAC,GAAG,CAAqB,YAAY,CAAC,iBAAiB,CAAC;AACvF,CAAA,EAAE;AAEH,aAAe,IAAI,OAAO,CAAC;IACvB,mBAAmB,EAAE,CAAC,cAAc,CAAC;IACrC,iBAAiB,EAAE,CAAC,eAAe,CAAC;IACpC,+BAA+B,EAAE,CAAC,cAAc,CAAC;AACpD,CAAA,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './invalidateCachePlugin';
2
+ export { responseCachePlugin } from './responseCachePlugin';
@@ -0,0 +1,8 @@
1
+ import { Redis, Cluster } from 'ioredis';
2
+ import type { BaseContext, GraphQLRequestContextWillSendResponse } from '@apollo/server';
3
+ import { ApolloServerOptions, GraphQLRequestContext } from '@apollo/server';
4
+ export type InvalidationKeyGenerator = (requestContext: GraphQLRequestContext<unknown>, responseContext: GraphQLRequestContextWillSendResponse<unknown>, cacheKey?: string) => string;
5
+ export declare const invalidateCachePlugin: ({ cache: redisClient, invalidateCacheKeyGenerator, }: {
6
+ cache: Redis | Cluster;
7
+ invalidateCacheKeyGenerator: InvalidationKeyGenerator;
8
+ }) => ApolloServerOptions<BaseContext>["plugins"][0];
@@ -0,0 +1,99 @@
1
+ import {uniq}from'lodash-es';import {extractTenantId,getDirectiveArgsFromSchema,CACHE_CONTROL_DIRECTIVE}from'@common-stack/server-core';import {config}from'../config/env-config.js';/**
2
+ * Scans Redis keys using SCAN command instead of KEYS for better performance.
3
+ * SCAN is non-blocking and safe to use in production environments.
4
+ */
5
+ async function scanKeys(redisClient, pattern) {
6
+ const allKeys = [];
7
+ let cursor = '0';
8
+ do {
9
+ // SCAN returns [cursor, keys[]]
10
+ const [nextCursor, keys] = await redisClient.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
11
+ cursor = nextCursor;
12
+ if (keys.length) {
13
+ allKeys.push(...keys);
14
+ }
15
+ } while (cursor !== '0');
16
+ return allKeys;
17
+ }
18
+ const invalidateCachePlugin = ({ cache: redisClient, invalidateCacheKeyGenerator, }) => ({
19
+ requestDidStart(requestContext) {
20
+ return {
21
+ willSendResponse: async (responseContext) => {
22
+ // in case of websocket request initially the requestContext comes empty
23
+ if (!requestContext) {
24
+ return;
25
+ }
26
+ try {
27
+ const hasErrors = !!requestContext.errors?.length;
28
+ const { schema } = requestContext;
29
+ // Safely access contextValue properties
30
+ const contextValue = requestContext.contextValue;
31
+ if (!contextValue) {
32
+ return;
33
+ }
34
+ const { queriesToInvalidate, user, req } = contextValue;
35
+ const tenantId = extractTenantId(req?.currentPageUriSegments?.authority);
36
+ // Safely extract operation from document definitions
37
+ const definitions = responseContext.document?.definitions;
38
+ if (!definitions?.length) {
39
+ return;
40
+ }
41
+ const firstDefinition = definitions[0];
42
+ const operation = firstDefinition?.operation;
43
+ const isMutation = operation === 'mutation';
44
+ if (hasErrors || !queriesToInvalidate?.length || !isMutation) {
45
+ return;
46
+ }
47
+ const nestedKeys = await Promise.all(queriesToInvalidate.map(async (query) => {
48
+ // Build keys in order of specificity
49
+ let keys = [];
50
+ // Safely get cache policy directive
51
+ let cachePolicy = null;
52
+ try {
53
+ cachePolicy = getDirectiveArgsFromSchema({
54
+ schema,
55
+ queryName: query,
56
+ directiveName: CACHE_CONTROL_DIRECTIVE,
57
+ });
58
+ }
59
+ catch (e) {
60
+ requestContext.logger?.warn?.(`Failed to get cache policy for query: ${query}`);
61
+ }
62
+ const isPrivate = cachePolicy?.scope?.toLowerCase() === 'private';
63
+ // Add tenant-specific key if tenant exists
64
+ if (tenantId) {
65
+ keys.push(`${config.APP_NAME}:${tenantId}:${query}:*`);
66
+ // Add user-specific key if user exists
67
+ if (user?.sub && isPrivate) {
68
+ keys.push(`${config.APP_NAME}:${tenantId}:${user.sub}:${query}:*`);
69
+ }
70
+ }
71
+ // Add global key as fallback
72
+ keys.push(`${config.APP_NAME}:${query}:*`);
73
+ // Allow custom key generation if provided
74
+ if (typeof invalidateCacheKeyGenerator === 'function') {
75
+ keys = keys.map((key) => invalidateCacheKeyGenerator(requestContext, responseContext, key));
76
+ }
77
+ // Use SCAN instead of KEYS for better performance (non-blocking)
78
+ const matchedKeys = await Promise.all(keys.map(async (key) => {
79
+ const matchingKeys = await scanKeys(redisClient, key);
80
+ if (matchingKeys.length) {
81
+ return matchingKeys;
82
+ }
83
+ return [];
84
+ }));
85
+ return matchedKeys.flat();
86
+ }));
87
+ const keys = nestedKeys.flat();
88
+ if (keys?.length) {
89
+ await redisClient.del(uniq(keys));
90
+ }
91
+ }
92
+ catch (e) {
93
+ requestContext.logger.error('Error occurred in invalidateCachePlugin');
94
+ requestContext.logger.error(e);
95
+ }
96
+ },
97
+ };
98
+ },
99
+ });export{invalidateCachePlugin};//# sourceMappingURL=invalidateCachePlugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalidateCachePlugin.js","sources":["../../src/plugins/invalidateCachePlugin.ts"],"sourcesContent":["import { Redis, Cluster } from 'ioredis';\nimport { uniq } from 'lodash-es';\nimport { OperationDefinitionNode } from 'graphql/language/ast.js';\nimport type { BaseContext, GraphQLRequestContextWillSendResponse } from '@apollo/server';\nimport { ApolloServerOptions, GraphQLRequestContext } from '@apollo/server';\nimport { CACHE_CONTROL_DIRECTIVE, extractTenantId, getDirectiveArgsFromSchema } from '@common-stack/server-core';\nimport { IGraphQLCacheContext } from 'common/server';\nimport { config } from '../config';\n\nexport type InvalidationKeyGenerator = (\n requestContext: GraphQLRequestContext<unknown>,\n responseContext: GraphQLRequestContextWillSendResponse<unknown>,\n cacheKey?: string,\n) => string;\n\n/**\n * Scans Redis keys using SCAN command instead of KEYS for better performance.\n * SCAN is non-blocking and safe to use in production environments.\n */\nasync function scanKeys(redisClient: Redis | Cluster, pattern: string): Promise<string[]> {\n const allKeys: string[] = [];\n let cursor = '0';\n\n do {\n // SCAN returns [cursor, keys[]]\n const [nextCursor, keys] = await redisClient.scan(cursor, 'MATCH', pattern, 'COUNT', 100);\n cursor = nextCursor;\n if (keys.length) {\n allKeys.push(...keys);\n }\n } while (cursor !== '0');\n\n return allKeys;\n}\n\nexport const invalidateCachePlugin = ({\n cache: redisClient,\n invalidateCacheKeyGenerator,\n}: {\n cache: Redis | Cluster;\n invalidateCacheKeyGenerator: InvalidationKeyGenerator;\n}) =>\n ({\n requestDidStart(requestContext: GraphQLRequestContext<IGraphQLCacheContext>) {\n return {\n willSendResponse: async (responseContext: GraphQLRequestContextWillSendResponse<unknown>) => {\n // in case of websocket request initially the requestContext comes empty\n if (!requestContext) {\n return;\n }\n try {\n const hasErrors = !!requestContext.errors?.length;\n const { schema } = requestContext;\n\n // Safely access contextValue properties\n const contextValue = requestContext.contextValue;\n if (!contextValue) {\n return;\n }\n const { queriesToInvalidate, user, req } = contextValue;\n const tenantId = extractTenantId(req?.currentPageUriSegments?.authority);\n\n // Safely extract operation from document definitions\n const definitions = responseContext.document?.definitions as OperationDefinitionNode[];\n if (!definitions?.length) {\n return;\n }\n const firstDefinition = definitions[0];\n const operation = firstDefinition?.operation;\n const isMutation = operation === 'mutation';\n\n if (hasErrors || !queriesToInvalidate?.length || !isMutation) {\n return;\n }\n\n const nestedKeys = await Promise.all<string[]>(\n queriesToInvalidate.map(async (query: string) => {\n // Build keys in order of specificity\n let keys: string[] = [];\n\n // Safely get cache policy directive\n let cachePolicy: { scope?: string } | null = null;\n try {\n cachePolicy = getDirectiveArgsFromSchema({\n schema,\n queryName: query,\n directiveName: CACHE_CONTROL_DIRECTIVE,\n });\n } catch (e) {\n requestContext.logger?.warn?.(`Failed to get cache policy for query: ${query}`);\n }\n\n const isPrivate = cachePolicy?.scope?.toLowerCase() === 'private';\n // Add tenant-specific key if tenant exists\n if (tenantId) {\n keys.push(`${config.APP_NAME}:${tenantId}:${query}:*`);\n // Add user-specific key if user exists\n if (user?.sub && isPrivate) {\n keys.push(`${config.APP_NAME}:${tenantId}:${user.sub}:${query}:*`);\n }\n }\n // Add global key as fallback\n keys.push(`${config.APP_NAME}:${query}:*`);\n\n // Allow custom key generation if provided\n if (typeof invalidateCacheKeyGenerator === 'function') {\n keys = keys.map((key) =>\n invalidateCacheKeyGenerator(requestContext, responseContext, key),\n );\n }\n // Use SCAN instead of KEYS for better performance (non-blocking)\n const matchedKeys = await Promise.all(\n keys.map(async (key) => {\n const matchingKeys = await scanKeys(redisClient, key);\n if (matchingKeys.length) {\n return matchingKeys;\n }\n return [];\n }),\n );\n return matchedKeys.flat();\n }),\n );\n const keys = nestedKeys.flat();\n if (keys?.length) {\n await redisClient.del(uniq(keys));\n }\n } catch (e) {\n requestContext.logger.error('Error occurred in invalidateCachePlugin');\n requestContext.logger.error(e);\n }\n },\n };\n },\n }) as unknown as ApolloServerOptions<BaseContext>['plugins'][0];\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { GraphQLRequestContext } from '@apollo/server';
2
+ import { ILogger } from '@cdm-logger/core/lib/interface';
3
+ import { IGraphQLCacheContext, IGraphqlCacheKeyGenerator } from 'common/server';
4
+ type ApolloCachePluginOptions = {
5
+ logger: ILogger;
6
+ cacheKeyGenerator: IGraphqlCacheKeyGenerator;
7
+ };
8
+ export declare const isCacheable: (requestContext: GraphQLRequestContext<IGraphQLCacheContext>) => boolean;
9
+ export declare const generateCacheKey: ({ logger, cacheKeyGenerator }: ApolloCachePluginOptions) => (requestContext: GraphQLRequestContext<IGraphQLCacheContext>) => string;
10
+ export declare const responseCachePlugin: ({ logger, cacheKeyGenerator }: ApolloCachePluginOptions) => any;
11
+ export {};
@@ -0,0 +1,63 @@
1
+ import {getDirectiveArgsFromSchema,CACHE_CONTROL_DIRECTIVE,extractTenantId}from'@common-stack/server-core';import {isEmpty}from'lodash-es';import apolloCachePlugin from'@apollo/server-plugin-response-cache';import {generateQueryCacheKey}from'../core/keyBuilder/generate-query-cache-key.js';const cachePlugin = apolloCachePlugin.default ?? apolloCachePlugin;
2
+ const isCacheable = (requestContext) => {
3
+ const { document, schema } = requestContext;
4
+ const cache = getDirectiveArgsFromSchema({
5
+ schema,
6
+ document,
7
+ directiveName: CACHE_CONTROL_DIRECTIVE,
8
+ });
9
+ if (!cache)
10
+ return false;
11
+ if (cache.scope && !cache.maxAge) {
12
+ cache.maxAge = 86400;
13
+ }
14
+ if (!requestContext.overallCachePolicy) {
15
+ // eslint-disable-next-line no-param-reassign
16
+ // to support test cases
17
+ requestContext.overallCachePolicy = {};
18
+ }
19
+ // eslint-disable-next-line no-param-reassign
20
+ requestContext.overallCachePolicy.maxAge = cache.maxAge;
21
+ return cache.maxAge > 0;
22
+ };
23
+ const generateCacheKey = ({ logger, cacheKeyGenerator }) => (requestContext) => {
24
+ if (!isCacheable(requestContext)) {
25
+ return null;
26
+ }
27
+ const { request, contextValue, document, schema } = requestContext;
28
+ const { user, req } = contextValue ?? {};
29
+ const { query, variables } = request;
30
+ const cacheControlDirective = getDirectiveArgsFromSchema({
31
+ schema,
32
+ document,
33
+ directiveName: CACHE_CONTROL_DIRECTIVE,
34
+ });
35
+ const { scope } = cacheControlDirective ?? {};
36
+ const isPrivate = scope?.toLowerCase() === 'private';
37
+ const tenantId = req.tenant ?? extractTenantId(req?.currentPageUriSegments?.authority);
38
+ const cacheKey = generateQueryCacheKey({
39
+ query,
40
+ variables,
41
+ logger,
42
+ userId: isPrivate ? user?.sub || null : null,
43
+ tenantId,
44
+ });
45
+ try {
46
+ if (typeof cacheKeyGenerator === 'function') {
47
+ const generatedKey = cacheKeyGenerator(requestContext, cacheKey);
48
+ return generatedKey || cacheKey;
49
+ }
50
+ return cacheKey;
51
+ }
52
+ catch (e) {
53
+ console.warn('GenerateCacheKey Failed %s', e.message);
54
+ return cacheKey;
55
+ }
56
+ };
57
+ const responseCachePlugin = ({ logger, cacheKeyGenerator }) => cachePlugin({
58
+ sessionId: ({ contextValue }) => Promise.resolve(contextValue?.user?.sub ?? null),
59
+ generateCacheKey: generateCacheKey({ logger, cacheKeyGenerator }),
60
+ shouldWriteToCache: (ctx) =>
61
+ // Cache only successful responses
62
+ isCacheable(ctx) && isEmpty(ctx?.response?.body?.singleResult.errors),
63
+ });export{generateCacheKey,isCacheable,responseCachePlugin};//# sourceMappingURL=responseCachePlugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responseCachePlugin.js","sources":["../../src/plugins/responseCachePlugin.ts"],"sourcesContent":["/* eslint-disable no-param-reassign */\nimport { GraphQLRequestContext } from '@apollo/server';\nimport { CACHE_CONTROL_DIRECTIVE, extractTenantId, getDirectiveArgsFromSchema } from '@common-stack/server-core';\nimport { isEmpty } from 'lodash-es';\nimport apolloCachePlugin from '@apollo/server-plugin-response-cache';\nimport { ILogger } from '@cdm-logger/core/lib/interface';\nimport { IGraphQLCacheContext, IGraphqlCacheKeyGenerator } from 'common/server';\nimport { generateQueryCacheKey } from '../core/keyBuilder/generate-query-cache-key';\n\nconst cachePlugin = (apolloCachePlugin as any).default ?? apolloCachePlugin;\n\ntype ApolloCachePluginOptions = {\n logger: ILogger;\n cacheKeyGenerator: IGraphqlCacheKeyGenerator;\n};\n\nexport const isCacheable = (requestContext: GraphQLRequestContext<IGraphQLCacheContext>) => {\n const { document, schema } = requestContext;\n const cache = getDirectiveArgsFromSchema({\n schema,\n document,\n directiveName: CACHE_CONTROL_DIRECTIVE,\n });\n if (!cache) return false;\n if (cache.scope && !cache.maxAge) {\n cache.maxAge = 86400;\n }\n if (!requestContext.overallCachePolicy) {\n // eslint-disable-next-line no-param-reassign\n // to support test cases\n (requestContext as any).overallCachePolicy = {};\n }\n // eslint-disable-next-line no-param-reassign\n requestContext.overallCachePolicy.maxAge = cache.maxAge;\n return cache.maxAge > 0;\n};\n\nexport const generateCacheKey =\n ({ logger, cacheKeyGenerator }: ApolloCachePluginOptions) =>\n (requestContext: GraphQLRequestContext<IGraphQLCacheContext>): string => {\n if (!isCacheable(requestContext)) {\n return null;\n }\n const { request, contextValue, document, schema } = requestContext;\n const { user, req } = contextValue ?? {};\n const { query, variables } = request;\n const cacheControlDirective = getDirectiveArgsFromSchema({\n schema,\n document,\n directiveName: CACHE_CONTROL_DIRECTIVE,\n });\n const { scope } = cacheControlDirective ?? {};\n const isPrivate = scope?.toLowerCase() === 'private';\n const tenantId = req.tenant ?? extractTenantId(req?.currentPageUriSegments?.authority);\n const cacheKey = generateQueryCacheKey({\n query,\n variables,\n logger,\n userId: isPrivate ? user?.sub || null : null,\n tenantId,\n });\n try {\n if (typeof cacheKeyGenerator === 'function') {\n const generatedKey = cacheKeyGenerator(requestContext, cacheKey);\n return generatedKey || cacheKey;\n }\n return cacheKey;\n } catch (e) {\n console.warn('GenerateCacheKey Failed %s', e.message);\n return cacheKey;\n }\n };\nexport const responseCachePlugin = ({ logger, cacheKeyGenerator }: ApolloCachePluginOptions) =>\n cachePlugin({\n sessionId: ({ contextValue }) => Promise.resolve(contextValue?.user?.sub ?? null),\n generateCacheKey: generateCacheKey({ logger, cacheKeyGenerator }),\n shouldWriteToCache: (ctx) =>\n // Cache only successful responses\n isCacheable(ctx) && isEmpty(ctx?.response?.body?.singleResult.errors),\n });\n"],"names":[],"mappings":"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"}
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { Redis } from 'ioredis';
15
15
  import type { DocumentNode } from 'graphql';
16
- import type { ICacheContext, ICachePolicy, IRedisCacheManager } from 'common/server';
16
+ import { type ICacheContext, type ICachePolicy, type IRedisCacheManager } from 'common/server';
17
17
  /**
18
18
  * Redis Cache Manager implementation
19
19
  *
@@ -62,9 +62,31 @@ export declare class RedisCacheManager implements IRedisCacheManager {
62
62
  /**
63
63
  * Generate cache key for a GraphQL query
64
64
  *
65
- * Format: APP_NAME:tenantId:userId:queryName:queryHash:variablesHash
65
+ * Format:
66
+ * - With userId (PRIVATE): APP_NAME:tenantId:userId:queryName:queryHash:variablesHash
67
+ * - Without userId (PUBLIC): APP_NAME:tenantId:queryName:queryHash:variablesHash
66
68
  */
67
69
  private getCacheKey;
70
+ /**
71
+ * Clear all cache entries for a tenant
72
+ */
73
+ clearTenant(tenantId: string): Promise<number>;
74
+ /**
75
+ * Clear all cache entries for a user
76
+ */
77
+ clearUser(userId: string, tenantId?: string): Promise<number>;
78
+ /**
79
+ * Clear all cache entries matching a pattern
80
+ */
81
+ clearPattern(pattern: string): Promise<number>;
82
+ /**
83
+ * Get cache statistics
84
+ */
85
+ getStats(tenantId?: string): Promise<{
86
+ totalKeys: number;
87
+ memoryUsage: number;
88
+ hitRate?: number;
89
+ }>;
68
90
  /**
69
91
  * Get application name for key prefix
70
92
  * Override this method to provide custom app name
@@ -1,4 +1,4 @@
1
- import {__decorate,__param,__metadata}from'tslib';import {isHashLikeTenantId,sanitizeRedisKeyComponent}from'../core/keyBuilder/sanitize-redis-key.js';import {generateQueryCacheKey}from'../core/keyBuilder/generate-query-cache-key.js';import {print}from'graphql';import {injectable,inject}from'inversify';/**
1
+ import {__decorate,__param,__metadata}from'tslib';import {isHashLikeTenantId,sanitizeRedisKeyComponent}from'../core/keyBuilder/sanitize-redis-key.js';import {generateQueryCacheKey}from'../core/keyBuilder/generate-query-cache-key.js';import {print}from'graphql';import {injectable,inject}from'inversify';import {SERVER_TYPES}from'common/server';import {CommonType}from'@common-stack/core';/**
2
2
  * @file RedisCacheManager.ts
3
3
  * @description Redis-based cache manager for GraphQL query caching
4
4
  *
@@ -35,22 +35,30 @@ let RedisCacheManager = RedisCacheManager_1 = class RedisCacheManager {
35
35
  * @param shouldRemoveAll - If true, removes all related cache keys
36
36
  */
37
37
  async del(query, variables, ctx, shouldRemoveAll = false) {
38
- const cacheKey = this.getCacheKey(query, variables ?? {}, ctx);
39
- // If variables provided, delete exact match
38
+ // Clear both PUBLIC and PRIVATE versions of the cache
39
+ const privateKey = this.getCacheKey(query, variables ?? {}, ctx, { maxAge: 1, scope: 'PRIVATE' });
40
+ const publicKey = this.getCacheKey(query, variables ?? {}, ctx, { maxAge: 1, scope: 'PUBLIC' });
41
+ // If variables provided, delete exact match (both versions)
40
42
  if (variables && Object.keys(variables).length > 0) {
41
- this.log('debug', `Deleting ${cacheKey} from redis`);
42
- await this.redisClient.del(cacheKey);
43
+ this.log('debug', `Deleting ${privateKey} and ${publicKey} from redis`);
44
+ await this.redisClient.del(privateKey, publicKey);
43
45
  return;
44
46
  }
45
- // Build wildcard pattern
46
- const keysWithWildCard = shouldRemoveAll
47
+ // Build wildcard patterns for both PUBLIC and PRIVATE
48
+ const privatePattern = shouldRemoveAll
47
49
  ? this.getWildCardQueryKey(query, ctx)
48
- : `${cacheKey.substring(0, cacheKey.lastIndexOf(':'))}:*`;
49
- const cacheKeys = await this.redisClient.keys(keysWithWildCard);
50
- this.log('debug', `Found ${cacheKeys.length} keys against pattern ${keysWithWildCard}`);
51
- if (cacheKeys.length) {
52
- this.log('debug', `Deleting ${cacheKeys.length} keys from redis`);
53
- await this.redisClient.del(...cacheKeys);
50
+ : `${privateKey.substring(0, privateKey.lastIndexOf(':'))}:*`;
51
+ const publicPattern = shouldRemoveAll
52
+ ? this.getWildCardQueryKey(query, ctx)
53
+ : `${publicKey.substring(0, publicKey.lastIndexOf(':'))}:*`;
54
+ // Get keys matching both patterns
55
+ const privateCacheKeys = await this.redisClient.keys(privatePattern);
56
+ const publicCacheKeys = await this.redisClient.keys(publicPattern);
57
+ const allKeys = [...new Set([...privateCacheKeys, ...publicCacheKeys])]; // Deduplicate
58
+ this.log('debug', `Found ${allKeys.length} keys against patterns ${privatePattern} and ${publicPattern}`);
59
+ if (allKeys.length) {
60
+ this.log('debug', `Deleting ${allKeys.length} keys from redis`);
61
+ await this.redisClient.del(...allKeys);
54
62
  }
55
63
  }
56
64
  /**
@@ -62,8 +70,14 @@ let RedisCacheManager = RedisCacheManager_1 = class RedisCacheManager {
62
70
  * @returns Cached data or null if cache miss
63
71
  */
64
72
  async get(query, variables, ctx) {
65
- const cacheKey = this.getCacheKey(query, variables, ctx);
66
- const cacheResponse = await this.redisClient.get(cacheKey);
73
+ // Try PRIVATE key first (with userId)
74
+ let cacheKey = this.getCacheKey(query, variables, ctx, { maxAge: 1, scope: 'PRIVATE' });
75
+ let cacheResponse = await this.redisClient.get(cacheKey);
76
+ // If not found, try PUBLIC key (without userId)
77
+ if (!cacheResponse) {
78
+ cacheKey = this.getCacheKey(query, variables, ctx, { maxAge: 1, scope: 'PUBLIC' });
79
+ cacheResponse = await this.redisClient.get(cacheKey);
80
+ }
67
81
  if (cacheResponse) {
68
82
  try {
69
83
  const { data } = JSON.parse(JSON.parse(cacheResponse)?.value) ?? {};
@@ -89,7 +103,7 @@ let RedisCacheManager = RedisCacheManager_1 = class RedisCacheManager {
89
103
  * @param cachePolicy - Cache policy (TTL, scope)
90
104
  */
91
105
  async set(query, variables, data, ctx, cachePolicy = { maxAge: 86400, scope: 'PUBLIC' }) {
92
- const cacheKey = this.getCacheKey(query, variables, ctx);
106
+ const cacheKey = this.getCacheKey(query, variables, ctx, cachePolicy);
93
107
  const cacheTime = Date.now();
94
108
  // Ensure maxAge is not negative or zero
95
109
  const maxAge = Math.max(1, cachePolicy.maxAge);
@@ -129,16 +143,11 @@ let RedisCacheManager = RedisCacheManager_1 = class RedisCacheManager {
129
143
  /**
130
144
  * Generate cache key for a GraphQL query
131
145
  *
132
- * Format: APP_NAME:tenantId:userId:queryName:queryHash:variablesHash
146
+ * Format:
147
+ * - With userId (PRIVATE): APP_NAME:tenantId:userId:queryName:queryHash:variablesHash
148
+ * - Without userId (PUBLIC): APP_NAME:tenantId:queryName:queryHash:variablesHash
133
149
  */
134
- getCacheKey(query, variables, ctx) {
135
- // Generate the legacy key WITHOUT tenantId/userId since we'll add them via manual construction
136
- const legacyKey = generateQueryCacheKey({
137
- query,
138
- variables,
139
- logger: this.logger,
140
- });
141
- // Build key without namespace - format: APP_NAME:tenantId:userId:legacyKey
150
+ getCacheKey(query, variables, ctx, cachePolicy) {
142
151
  const appName = this.getAppName();
143
152
  // Validate tenantId - if it looks like a hash (all hex, 24+ chars), use 'default'
144
153
  let tenantId = ctx?.tenantId || 'default';
@@ -146,12 +155,79 @@ let RedisCacheManager = RedisCacheManager_1 = class RedisCacheManager {
146
155
  this.log('warn', `TenantId appears to be a hash (${tenantId.substring(0, 16)}...), using "default" instead`);
147
156
  tenantId = 'default';
148
157
  }
149
- const userId = ctx?.userId || 'anonymous';
150
- // Sanitize components (handles Auth0 userIds like "auth0|123")
151
- const sanitizedAppName = sanitizeRedisKeyComponent(appName);
158
+ // For PUBLIC scope, omit userId to share cache across all users
159
+ // For PRIVATE scope or when not specified, include userId (or 'anonymous')
160
+ const isPublicScope = cachePolicy?.scope === 'PUBLIC';
161
+ const userId = isPublicScope ? undefined : ctx?.userId || 'anonymous';
162
+ // Use generateQueryCacheKey with all parameters to avoid duplication
163
+ // This will properly sanitize userId (auth0|123 -> auth0-123) and build the complete key
164
+ return generateQueryCacheKey({
165
+ query,
166
+ variables,
167
+ appName,
168
+ tenantId,
169
+ userId,
170
+ logger: this.logger,
171
+ });
172
+ }
173
+ /**
174
+ * Clear all cache entries for a tenant
175
+ */
176
+ async clearTenant(tenantId) {
177
+ const appName = this.getAppName();
152
178
  const sanitizedTenantId = sanitizeRedisKeyComponent(tenantId);
179
+ const pattern = `${appName}:${sanitizedTenantId}:*`;
180
+ return await this.clearPattern(pattern);
181
+ }
182
+ /**
183
+ * Clear all cache entries for a user
184
+ */
185
+ async clearUser(userId, tenantId) {
186
+ const appName = this.getAppName();
153
187
  const sanitizedUserId = sanitizeRedisKeyComponent(userId);
154
- return `${sanitizedAppName}:${sanitizedTenantId}:${sanitizedUserId}:${legacyKey}`;
188
+ const sanitizedTenantId = tenantId ? sanitizeRedisKeyComponent(tenantId) : '*';
189
+ const pattern = `${appName}:${sanitizedTenantId}:${sanitizedUserId}:*`;
190
+ return await this.clearPattern(pattern);
191
+ }
192
+ /**
193
+ * Clear all cache entries matching a pattern
194
+ */
195
+ async clearPattern(pattern) {
196
+ this.log('debug', `Clearing cache entries matching pattern: ${pattern}`);
197
+ const keys = await this.redisClient.keys(pattern);
198
+ if (keys.length === 0) {
199
+ this.log('debug', `No keys found matching pattern: ${pattern}`);
200
+ return 0;
201
+ }
202
+ this.log('debug', `Deleting ${keys.length} keys matching pattern: ${pattern}`);
203
+ await this.redisClient.del(...keys);
204
+ return keys.length;
205
+ }
206
+ /**
207
+ * Get cache statistics
208
+ */
209
+ async getStats(tenantId) {
210
+ const appName = this.getAppName();
211
+ const pattern = tenantId ? `${appName}:${sanitizeRedisKeyComponent(tenantId)}:*` : `${appName}:*`;
212
+ const keys = await this.redisClient.keys(pattern);
213
+ const totalKeys = keys.length;
214
+ // Get memory usage for the Redis instance
215
+ let memoryUsage = 0;
216
+ try {
217
+ const info = await this.redisClient.info('memory');
218
+ const match = info.match(/used_memory:(\d+)/);
219
+ if (match) {
220
+ memoryUsage = parseInt(match[1], 10);
221
+ }
222
+ }
223
+ catch (error) {
224
+ this.log('warn', 'Failed to get memory info from Redis:', error);
225
+ }
226
+ return {
227
+ totalKeys,
228
+ memoryUsage,
229
+ hitRate: undefined, // Hit rate tracking would require additional instrumentation
230
+ };
155
231
  }
156
232
  /**
157
233
  * Get application name for key prefix
@@ -171,7 +247,7 @@ let RedisCacheManager = RedisCacheManager_1 = class RedisCacheManager {
171
247
  };
172
248
  RedisCacheManager = RedisCacheManager_1 = __decorate([
173
249
  injectable(),
174
- __param(0, inject('REDIS_CLIENT')),
175
- __param(1, inject('LOGGER')),
250
+ __param(0, inject(SERVER_TYPES.RedisClient)),
251
+ __param(1, inject(CommonType.LOGGER)),
176
252
  __metadata("design:paramtypes", [Function, Object])
177
253
  ], RedisCacheManager);export{RedisCacheManager};//# sourceMappingURL=RedisCacheManager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RedisCacheManager.js","sources":["../../src/services/RedisCacheManager.ts"],"sourcesContent":["/**\n * @file RedisCacheManager.ts\n * @description Redis-based cache manager for GraphQL query caching\n *\n * This implementation provides sophisticated caching for GraphQL operations with:\n * - Automatic query hashing for cache keys\n * - Multi-tenant and user isolation\n * - Wildcard-based cache invalidation\n * - TTL-based expiration\n * - Automatic key sanitization (handles Auth0 userIds with pipes)\n *\n * Migrated from @adminide-stack/platform-server to be shared across applications\n */\n\nimport { generateQueryCacheKey, sanitizeRedisKeyComponent, isHashLikeTenantId } from '../core/keyBuilder';\nimport type { Redis } from 'ioredis';\nimport { print } from 'graphql';\nimport type { DocumentNode } from 'graphql';\nimport { injectable, inject } from 'inversify';\nimport type { ICacheContext, ICachePolicy, IRedisCacheManager } from 'common/server';\n\n/**\n * Redis Cache Manager implementation\n *\n * Provides GraphQL query caching with automatic key generation and invalidation\n */\n@injectable()\nexport class RedisCacheManager implements IRedisCacheManager {\n protected logger?: any;\n\n constructor(\n @inject('REDIS_CLIENT')\n protected readonly redisClient: Redis,\n @inject('LOGGER')\n logger?: any,\n ) {\n if (logger) {\n this.logger = logger.child ? logger.child({ className: RedisCacheManager.name }) : logger;\n }\n }\n\n /**\n * Delete cached data for a GraphQL query\n *\n * @param query - GraphQL query to invalidate\n * @param variables - Optional variables (if omitted, clears all variants)\n * @param ctx - Cache context for tenant/user isolation\n * @param shouldRemoveAll - If true, removes all related cache keys\n */\n async del(\n query: string | DocumentNode,\n variables?: Record<string, unknown>,\n ctx?: ICacheContext,\n shouldRemoveAll = false,\n ): Promise<void> {\n const cacheKey = this.getCacheKey(query, variables ?? {}, ctx);\n\n // If variables provided, delete exact match\n if (variables && Object.keys(variables).length > 0) {\n this.log('debug', `Deleting ${cacheKey} from redis`);\n await this.redisClient.del(cacheKey);\n return;\n }\n\n // Build wildcard pattern\n const keysWithWildCard = shouldRemoveAll\n ? this.getWildCardQueryKey(query, ctx)\n : `${cacheKey.substring(0, cacheKey.lastIndexOf(':'))}:*`;\n\n const cacheKeys = await this.redisClient.keys(keysWithWildCard);\n this.log('debug', `Found ${cacheKeys.length} keys against pattern ${keysWithWildCard}`);\n\n if (cacheKeys.length) {\n this.log('debug', `Deleting ${cacheKeys.length} keys from redis`);\n await this.redisClient.del(...cacheKeys);\n }\n }\n\n /**\n * Get cached data for a GraphQL query\n *\n * @param query - GraphQL query\n * @param variables - Query variables\n * @param ctx - Cache context\n * @returns Cached data or null if cache miss\n */\n async get<T>(\n query: string | DocumentNode,\n variables: Record<string, unknown>,\n ctx: ICacheContext,\n ): Promise<T | null> {\n const cacheKey = this.getCacheKey(query, variables, ctx);\n const cacheResponse = await this.redisClient.get(cacheKey);\n\n if (cacheResponse) {\n try {\n const { data } = JSON.parse(JSON.parse(cacheResponse)?.value) ?? {};\n const queryName = this.getQueryName(query);\n this.log('debug', `Found cache for ${cacheKey}`);\n return data?.[queryName] ?? null;\n } catch (error) {\n this.log('warn', `Failed to parse cache data for ${cacheKey}:`, error);\n return null;\n }\n }\n\n this.log('debug', `No cache found for key ${cacheKey}`);\n return null;\n }\n\n /**\n * Set cached data for a GraphQL query\n *\n * @param query - GraphQL query\n * @param variables - Query variables\n * @param data - Data to cache\n * @param ctx - Cache context\n * @param cachePolicy - Cache policy (TTL, scope)\n */\n async set<T>(\n query: string | DocumentNode,\n variables: Record<string, unknown>,\n data: T,\n ctx: ICacheContext,\n cachePolicy: ICachePolicy = { maxAge: 86400, scope: 'PUBLIC' },\n ): Promise<void> {\n const cacheKey = this.getCacheKey(query, variables, ctx);\n const cacheTime = Date.now();\n\n // Ensure maxAge is not negative or zero\n const maxAge = Math.max(1, cachePolicy.maxAge);\n\n if (cachePolicy.maxAge <= 0) {\n this.log(\n 'warn',\n `Invalid maxAge (${cachePolicy.maxAge}) for cache key ${cacheKey}, using minimum value of 1 second`,\n );\n }\n\n this.log('debug', `Set cache for key ${cacheKey} with maxAge ${maxAge}`);\n\n await this.redisClient.set(\n cacheKey,\n JSON.stringify({\n value: JSON.stringify({\n data: { [this.getQueryName(query)]: data },\n cachePolicy: { ...cachePolicy, maxAge },\n cacheTime,\n }),\n expires: cacheTime + maxAge * 1000,\n }).trim(),\n 'EX',\n maxAge,\n );\n }\n\n /**\n * Extract query name from GraphQL query\n */\n private getQueryName(query: string | DocumentNode): string {\n const queryStr = typeof query === 'string' ? query : print(query);\n const [, queryName] = queryStr?.match(/{\\s*(\\w+)/) ?? [];\n return queryName;\n }\n\n /**\n * Build wildcard pattern for query invalidation\n */\n private getWildCardQueryKey(query: string | DocumentNode, ctx?: ICacheContext): string {\n const queryStr = typeof query === 'string' ? query : print(query);\n const [, queryName] = queryStr?.match(/{\\s*(\\w+)/) ?? [];\n const { tenantId } = ctx || {};\n\n // Build pattern without namespace - just APP_NAME:tenantId:segments\n const appName = this.getAppName();\n const sanitizedTenantId = tenantId || 'default';\n return `${appName}:${sanitizedTenantId}:*:${queryName || '*'}:*`;\n }\n\n /**\n * Generate cache key for a GraphQL query\n *\n * Format: APP_NAME:tenantId:userId:queryName:queryHash:variablesHash\n */\n private getCacheKey(query: string | DocumentNode, variables: Record<string, unknown>, ctx?: ICacheContext): string {\n // Generate the legacy key WITHOUT tenantId/userId since we'll add them via manual construction\n const legacyKey = generateQueryCacheKey({\n query,\n variables,\n logger: this.logger,\n });\n\n // Build key without namespace - format: APP_NAME:tenantId:userId:legacyKey\n const appName = this.getAppName();\n\n // Validate tenantId - if it looks like a hash (all hex, 24+ chars), use 'default'\n let tenantId = ctx?.tenantId || 'default';\n if (isHashLikeTenantId(tenantId)) {\n this.log(\n 'warn',\n `TenantId appears to be a hash (${tenantId.substring(0, 16)}...), using \"default\" instead`,\n );\n tenantId = 'default';\n }\n\n const userId = ctx?.userId || 'anonymous';\n\n // Sanitize components (handles Auth0 userIds like \"auth0|123\")\n const sanitizedAppName = sanitizeRedisKeyComponent(appName);\n const sanitizedTenantId = sanitizeRedisKeyComponent(tenantId);\n const sanitizedUserId = sanitizeRedisKeyComponent(userId);\n\n return `${sanitizedAppName}:${sanitizedTenantId}:${sanitizedUserId}:${legacyKey}`;\n }\n\n /**\n * Get application name for key prefix\n * Override this method to provide custom app name\n */\n protected getAppName(): string {\n return process.env.APP_NAME || 'COMMON_STACK';\n }\n\n /**\n * Log helper\n */\n private log(level: string, message: string, ...args: any[]): void {\n if (this.logger && typeof this.logger[level] === 'function') {\n this.logger[level](message, ...args);\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RedisCacheManager.js","sources":["../../src/services/RedisCacheManager.ts"],"sourcesContent":["/**\n * @file RedisCacheManager.ts\n * @description Redis-based cache manager for GraphQL query caching\n *\n * This implementation provides sophisticated caching for GraphQL operations with:\n * - Automatic query hashing for cache keys\n * - Multi-tenant and user isolation\n * - Wildcard-based cache invalidation\n * - TTL-based expiration\n * - Automatic key sanitization (handles Auth0 userIds with pipes)\n *\n * Migrated from @adminide-stack/platform-server to be shared across applications\n */\n\nimport { generateQueryCacheKey, sanitizeRedisKeyComponent, isHashLikeTenantId } from '../core/keyBuilder';\nimport type { Redis } from 'ioredis';\nimport { print } from 'graphql';\nimport type { DocumentNode } from 'graphql';\nimport { injectable, inject } from 'inversify';\nimport { type ICacheContext, type ICachePolicy, type IRedisCacheManager, SERVER_TYPES } from 'common/server';\nimport { CommonType } from '@common-stack/core';\n\n/**\n * Redis Cache Manager implementation\n *\n * Provides GraphQL query caching with automatic key generation and invalidation\n */\n@injectable()\nexport class RedisCacheManager implements IRedisCacheManager {\n protected logger?: any;\n\n constructor(\n @inject(SERVER_TYPES.RedisClient)\n protected readonly redisClient: Redis,\n @inject(CommonType.LOGGER)\n logger?: any,\n ) {\n if (logger) {\n this.logger = logger.child ? logger.child({ className: RedisCacheManager.name }) : logger;\n }\n }\n\n /**\n * Delete cached data for a GraphQL query\n *\n * @param query - GraphQL query to invalidate\n * @param variables - Optional variables (if omitted, clears all variants)\n * @param ctx - Cache context for tenant/user isolation\n * @param shouldRemoveAll - If true, removes all related cache keys\n */\n async del(\n query: string | DocumentNode,\n variables?: Record<string, unknown>,\n ctx?: ICacheContext,\n shouldRemoveAll = false,\n ): Promise<void> {\n // Clear both PUBLIC and PRIVATE versions of the cache\n const privateKey = this.getCacheKey(query, variables ?? {}, ctx, { maxAge: 1, scope: 'PRIVATE' });\n const publicKey = this.getCacheKey(query, variables ?? {}, ctx, { maxAge: 1, scope: 'PUBLIC' });\n\n // If variables provided, delete exact match (both versions)\n if (variables && Object.keys(variables).length > 0) {\n this.log('debug', `Deleting ${privateKey} and ${publicKey} from redis`);\n await this.redisClient.del(privateKey, publicKey);\n return;\n }\n\n // Build wildcard patterns for both PUBLIC and PRIVATE\n const privatePattern = shouldRemoveAll\n ? this.getWildCardQueryKey(query, ctx)\n : `${privateKey.substring(0, privateKey.lastIndexOf(':'))}:*`;\n\n const publicPattern = shouldRemoveAll\n ? this.getWildCardQueryKey(query, ctx)\n : `${publicKey.substring(0, publicKey.lastIndexOf(':'))}:*`;\n\n // Get keys matching both patterns\n const privateCacheKeys = await this.redisClient.keys(privatePattern);\n const publicCacheKeys = await this.redisClient.keys(publicPattern);\n const allKeys = [...new Set([...privateCacheKeys, ...publicCacheKeys])]; // Deduplicate\n\n this.log('debug', `Found ${allKeys.length} keys against patterns ${privatePattern} and ${publicPattern}`);\n\n if (allKeys.length) {\n this.log('debug', `Deleting ${allKeys.length} keys from redis`);\n await this.redisClient.del(...allKeys);\n }\n }\n\n /**\n * Get cached data for a GraphQL query\n *\n * @param query - GraphQL query\n * @param variables - Query variables\n * @param ctx - Cache context\n * @returns Cached data or null if cache miss\n */\n async get<T>(\n query: string | DocumentNode,\n variables: Record<string, unknown>,\n ctx: ICacheContext,\n ): Promise<T | null> {\n // Try PRIVATE key first (with userId)\n let cacheKey = this.getCacheKey(query, variables, ctx, { maxAge: 1, scope: 'PRIVATE' });\n let cacheResponse = await this.redisClient.get(cacheKey);\n\n // If not found, try PUBLIC key (without userId)\n if (!cacheResponse) {\n cacheKey = this.getCacheKey(query, variables, ctx, { maxAge: 1, scope: 'PUBLIC' });\n cacheResponse = await this.redisClient.get(cacheKey);\n }\n\n if (cacheResponse) {\n try {\n const { data } = JSON.parse(JSON.parse(cacheResponse)?.value) ?? {};\n const queryName = this.getQueryName(query);\n this.log('debug', `Found cache for ${cacheKey}`);\n return data?.[queryName] ?? null;\n } catch (error) {\n this.log('warn', `Failed to parse cache data for ${cacheKey}:`, error);\n return null;\n }\n }\n\n this.log('debug', `No cache found for key ${cacheKey}`);\n return null;\n }\n\n /**\n * Set cached data for a GraphQL query\n *\n * @param query - GraphQL query\n * @param variables - Query variables\n * @param data - Data to cache\n * @param ctx - Cache context\n * @param cachePolicy - Cache policy (TTL, scope)\n */\n async set<T>(\n query: string | DocumentNode,\n variables: Record<string, unknown>,\n data: T,\n ctx: ICacheContext,\n cachePolicy: ICachePolicy = { maxAge: 86400, scope: 'PUBLIC' },\n ): Promise<void> {\n const cacheKey = this.getCacheKey(query, variables, ctx, cachePolicy);\n const cacheTime = Date.now();\n\n // Ensure maxAge is not negative or zero\n const maxAge = Math.max(1, cachePolicy.maxAge);\n\n if (cachePolicy.maxAge <= 0) {\n this.log(\n 'warn',\n `Invalid maxAge (${cachePolicy.maxAge}) for cache key ${cacheKey}, using minimum value of 1 second`,\n );\n }\n\n this.log('debug', `Set cache for key ${cacheKey} with maxAge ${maxAge}`);\n\n await this.redisClient.set(\n cacheKey,\n JSON.stringify({\n value: JSON.stringify({\n data: { [this.getQueryName(query)]: data },\n cachePolicy: { ...cachePolicy, maxAge },\n cacheTime,\n }),\n expires: cacheTime + maxAge * 1000,\n }).trim(),\n 'EX',\n maxAge,\n );\n }\n\n /**\n * Extract query name from GraphQL query\n */\n private getQueryName(query: string | DocumentNode): string {\n const queryStr = typeof query === 'string' ? query : print(query);\n const [, queryName] = queryStr?.match(/{\\s*(\\w+)/) ?? [];\n return queryName;\n }\n\n /**\n * Build wildcard pattern for query invalidation\n */\n private getWildCardQueryKey(query: string | DocumentNode, ctx?: ICacheContext): string {\n const queryStr = typeof query === 'string' ? query : print(query);\n const [, queryName] = queryStr?.match(/{\\s*(\\w+)/) ?? [];\n const { tenantId } = ctx || {};\n\n // Build pattern without namespace - just APP_NAME:tenantId:segments\n const appName = this.getAppName();\n const sanitizedTenantId = tenantId || 'default';\n return `${appName}:${sanitizedTenantId}:*:${queryName || '*'}:*`;\n }\n\n /**\n * Generate cache key for a GraphQL query\n *\n * Format:\n * - With userId (PRIVATE): APP_NAME:tenantId:userId:queryName:queryHash:variablesHash\n * - Without userId (PUBLIC): APP_NAME:tenantId:queryName:queryHash:variablesHash\n */\n private getCacheKey(\n query: string | DocumentNode,\n variables: Record<string, unknown>,\n ctx?: ICacheContext,\n cachePolicy?: ICachePolicy,\n ): string {\n const appName = this.getAppName();\n\n // Validate tenantId - if it looks like a hash (all hex, 24+ chars), use 'default'\n let tenantId = ctx?.tenantId || 'default';\n if (isHashLikeTenantId(tenantId)) {\n this.log(\n 'warn',\n `TenantId appears to be a hash (${tenantId.substring(0, 16)}...), using \"default\" instead`,\n );\n tenantId = 'default';\n }\n\n // For PUBLIC scope, omit userId to share cache across all users\n // For PRIVATE scope or when not specified, include userId (or 'anonymous')\n const isPublicScope = cachePolicy?.scope === 'PUBLIC';\n const userId = isPublicScope ? undefined : ctx?.userId || 'anonymous';\n\n // Use generateQueryCacheKey with all parameters to avoid duplication\n // This will properly sanitize userId (auth0|123 -> auth0-123) and build the complete key\n return generateQueryCacheKey({\n query,\n variables,\n appName,\n tenantId,\n userId,\n logger: this.logger,\n });\n }\n\n /**\n * Clear all cache entries for a tenant\n */\n async clearTenant(tenantId: string): Promise<number> {\n const appName = this.getAppName();\n const sanitizedTenantId = sanitizeRedisKeyComponent(tenantId);\n const pattern = `${appName}:${sanitizedTenantId}:*`;\n\n return await this.clearPattern(pattern);\n }\n\n /**\n * Clear all cache entries for a user\n */\n async clearUser(userId: string, tenantId?: string): Promise<number> {\n const appName = this.getAppName();\n const sanitizedUserId = sanitizeRedisKeyComponent(userId);\n const sanitizedTenantId = tenantId ? sanitizeRedisKeyComponent(tenantId) : '*';\n const pattern = `${appName}:${sanitizedTenantId}:${sanitizedUserId}:*`;\n\n return await this.clearPattern(pattern);\n }\n\n /**\n * Clear all cache entries matching a pattern\n */\n async clearPattern(pattern: string): Promise<number> {\n this.log('debug', `Clearing cache entries matching pattern: ${pattern}`);\n\n const keys = await this.redisClient.keys(pattern);\n\n if (keys.length === 0) {\n this.log('debug', `No keys found matching pattern: ${pattern}`);\n return 0;\n }\n\n this.log('debug', `Deleting ${keys.length} keys matching pattern: ${pattern}`);\n await this.redisClient.del(...keys);\n\n return keys.length;\n }\n\n /**\n * Get cache statistics\n */\n async getStats(tenantId?: string): Promise<{\n totalKeys: number;\n memoryUsage: number;\n hitRate?: number;\n }> {\n const appName = this.getAppName();\n const pattern = tenantId ? `${appName}:${sanitizeRedisKeyComponent(tenantId)}:*` : `${appName}:*`;\n\n const keys = await this.redisClient.keys(pattern);\n const totalKeys = keys.length;\n\n // Get memory usage for the Redis instance\n let memoryUsage = 0;\n try {\n const info = await this.redisClient.info('memory');\n const match = info.match(/used_memory:(\\d+)/);\n if (match) {\n memoryUsage = parseInt(match[1], 10);\n }\n } catch (error) {\n this.log('warn', 'Failed to get memory info from Redis:', error);\n }\n\n return {\n totalKeys,\n memoryUsage,\n hitRate: undefined, // Hit rate tracking would require additional instrumentation\n };\n }\n\n /**\n * Get application name for key prefix\n * Override this method to provide custom app name\n */\n protected getAppName(): string {\n return process.env.APP_NAME || 'COMMON_STACK';\n }\n\n /**\n * Log helper\n */\n private log(level: string, message: string, ...args: any[]): void {\n if (this.logger && typeof this.logger[level] === 'function') {\n this.logger[level](message, ...args);\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { ServiceBroker } from 'moleculer';
2
+ import { GraphQLRequestContext } from '@apollo/server';
3
+
4
+ export type IGraphQLCacheContext = {
5
+ overallCachePolicy: { scope: string; maxAge: number };
6
+ user: { sub: string };
7
+ req: { currentPageUriSegments: { authority: string }; tenant?: string };
8
+ queriesToInvalidate: string[];
9
+ tenantId?: string;
10
+ accountId?: string;
11
+ moleculerBroker?: ServiceBroker;
12
+ };
13
+
14
+ export type IGraphqlCacheKeyGenerator = (context: GraphQLRequestContext<IGraphQLCacheContext>, cacheKey?: string) => string;
@@ -1,11 +1,10 @@
1
1
  export interface IRedisClient {
2
2
  get?(key: string): Promise<string | null>;
3
3
  get?(key: string, callback?: (...args: any[]) => void): any;
4
- set(key: string, value: string, options?: {
5
- ex?: number;
6
- }): Promise<string>;
4
+ set(key: string, value: string, options?: { ex?: number }): Promise<string>;
7
5
  del(key: string): Promise<number>;
8
6
  delMulti(keys: string[]): Promise<any>;
7
+ keys(pattern: string): Promise<string[]>;
9
8
  on?(event: string | symbol, callback: (...args: any[]) => void): any;
10
9
  once?(event: string | symbol, callback: (...args: any[]) => void): any;
11
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@common-stack/store-redis",
3
- "version": "8.3.1-alpha.0",
3
+ "version": "8.3.1-alpha.10",
4
4
  "description": "Redis store utilities and services for common-stack",
5
5
  "license": "UNLICENSED",
6
6
  "author": "CDMBase LLC",
@@ -24,16 +24,16 @@
24
24
  "watch": "npm run build:lib:watch"
25
25
  },
26
26
  "dependencies": {
27
- "@common-stack/core": "8.3.1-alpha.0"
27
+ "@common-stack/core": "8.3.1-alpha.7",
28
+ "@upstash/redis": "^1.36.1",
29
+ "ioredis": "^5.4.1"
28
30
  },
29
31
  "devDependencies": {
30
- "common": "8.2.5-alpha.35",
31
- "ioredis": "^5.3.2"
32
+ "common": "8.3.1-alpha.7"
32
33
  },
33
34
  "peerDependencies": {
34
35
  "graphql": ">=16.0.0",
35
- "inversify": "*",
36
- "ioredis": ">=5.0.0"
36
+ "inversify": "*"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
@@ -44,16 +44,18 @@
44
44
  "./${libDir}/templates/constants/SERVER_TYPES.ts.template"
45
45
  ],
46
46
  "repositories": [
47
- "./${libDir}/templates/repositories/IRedisKeyBuilder.ts.template",
48
- "./${libDir}/templates/repositories/redisCommonTypes.ts.template"
47
+ "./${libDir}/templates/repositories/RedisKeyBuilder.ts.template",
48
+ "./${libDir}/templates/repositories/RedisClient.ts.template",
49
+ "./${libDir}/templates/repositories/redisCommonTypes.ts.template",
50
+ "./${libDir}/templates/repositories/GraphQLCacheContext.ts.template"
49
51
  ],
50
52
  "services": [
51
53
  "./${libDir}/templates/services/RedisCacheManager.ts.template"
52
54
  ]
53
55
  }
54
56
  },
57
+ "gitHead": "d58b806b46670b76de8f385533a2ea7bb80fb0ea",
55
58
  "typescript": {
56
59
  "definition": "lib/index.d.ts"
57
- },
58
- "gitHead": "896a0a80a57b2ec62925bdda9ed35e61cd5c1e9f"
60
+ }
59
61
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
package/lib/core/index.js DELETED
@@ -1,6 +0,0 @@
1
- import'ioredis';import'@upstash/redis/cloudflare';import'crypto';import'graphql/language/index.js';/**
2
- * Core utilities for Redis operations
3
- * @packageDocumentation
4
- */
5
- // Ensure this file generates a JS output in rollup build
6
- const __esModule = true;export{__esModule};//# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sources":["../../src/core/index.ts"],"sourcesContent":["/**\n * Core utilities for Redis operations\n * @packageDocumentation\n */\n\nexport * from './ioredis';\nexport * from './upstash-redis';\nexport * from './keyBuilder';\n\n// Ensure this file generates a JS output in rollup build\nexport const __esModule = true;\n"],"names":[],"mappings":"mGAAA;;;AAGG;AAMH;AACO,MAAM,UAAU,GAAG"}
@@ -1 +0,0 @@
1
- export * from './redis';