@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.
- package/lib/config/env-config.d.ts +4 -0
- package/lib/config/env-config.js +51 -0
- package/lib/config/env-config.js.map +1 -0
- package/lib/config/index.d.ts +1 -0
- package/lib/core/index.d.ts +0 -5
- package/lib/core/ioredis.d.ts +4 -2
- package/lib/core/ioredis.js +10 -1
- package/lib/core/ioredis.js.map +1 -1
- package/lib/core/upstash-redis.d.ts +5 -2
- package/lib/core/upstash-redis.js +22 -1
- package/lib/core/upstash-redis.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/module.d.ts +6 -1
- package/lib/module.js +6 -2
- package/lib/module.js.map +1 -1
- package/lib/plugins/index.d.ts +2 -0
- package/lib/plugins/invalidateCachePlugin.d.ts +8 -0
- package/lib/plugins/invalidateCachePlugin.js +99 -0
- package/lib/plugins/invalidateCachePlugin.js.map +1 -0
- package/lib/plugins/responseCachePlugin.d.ts +11 -0
- package/lib/plugins/responseCachePlugin.js +63 -0
- package/lib/plugins/responseCachePlugin.js.map +1 -0
- package/lib/services/RedisCacheManager.d.ts +24 -2
- package/lib/services/RedisCacheManager.js +107 -31
- package/lib/services/RedisCacheManager.js.map +1 -1
- package/lib/templates/repositories/GraphQLCacheContext.ts.template +14 -0
- package/lib/{interfaces/redis.d.ts → templates/repositories/RedisClient.ts.template} +2 -3
- package/package.json +12 -10
- package/lib/__tests__/redis-key-builder.test.d.ts +0 -1
- package/lib/__tests__/redis-key-sanitizer.test.d.ts +0 -1
- package/lib/core/index.js +0 -6
- package/lib/core/index.js.map +0 -1
- package/lib/interfaces/index.d.ts +0 -1
- /package/lib/templates/repositories/{IRedisKeyBuilder.ts.template → RedisKeyBuilder.ts.template} +0 -0
|
@@ -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';
|
package/lib/core/index.d.ts
CHANGED
package/lib/core/ioredis.d.ts
CHANGED
|
@@ -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
|
}
|
package/lib/core/ioredis.js
CHANGED
|
@@ -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
|
package/lib/core/ioredis.js.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{
|
|
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
|
-
|
|
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';
|
|
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":"
|
|
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,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
|
|
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:
|
|
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
|
-
|
|
39
|
-
|
|
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 ${
|
|
42
|
-
await this.redisClient.del(
|
|
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
|
|
46
|
-
const
|
|
47
|
+
// Build wildcard patterns for both PUBLIC and PRIVATE
|
|
48
|
+
const privatePattern = shouldRemoveAll
|
|
47
49
|
? this.getWildCardQueryKey(query, ctx)
|
|
48
|
-
: `${
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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:
|
|
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
|
-
|
|
150
|
-
//
|
|
151
|
-
const
|
|
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
|
-
|
|
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(
|
|
175
|
-
__param(1, inject(
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
48
|
-
"./${libDir}/templates/repositories/
|
|
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
|
package/lib/core/index.js.map
DELETED
|
@@ -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';
|
/package/lib/templates/repositories/{IRedisKeyBuilder.ts.template → RedisKeyBuilder.ts.template}
RENAMED
|
File without changes
|