@common-stack/store-redis 8.3.1-alpha.5 → 8.3.1-alpha.7

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.
@@ -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';
package/lib/index.d.ts CHANGED
@@ -12,3 +12,4 @@
12
12
  export * from './core/index.js';
13
13
  export * from './services';
14
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{__esModule}from'./core/index.js';export{RedisCacheManager}from'./services/RedisCacheManager.js';export{invalidateCachePlugin}from'./plugins/invalidateCachePlugin.js';export{responseCachePlugin}from'./plugins/responseCachePlugin.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
@@ -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,63 @@
1
+ import {uniq}from'lodash-es';import {extractTenantId,getDirectiveArgsFromSchema,CACHE_CONTROL_DIRECTIVE}from'@common-stack/server-core';import {config}from'../config/env-config.js';const invalidateCachePlugin = ({ cache: redisClient, invalidateCacheKeyGenerator, }) => ({
2
+ requestDidStart(requestContext) {
3
+ return {
4
+ willSendResponse: async (responseContext) => {
5
+ // in case of websocket request initially the requestContext comes empty
6
+ if (!requestContext) {
7
+ return;
8
+ }
9
+ try {
10
+ const hasErrors = !!requestContext.errors?.length;
11
+ const { schema } = requestContext;
12
+ const { queriesToInvalidate, user, req } = requestContext.contextValue;
13
+ const tenantId = extractTenantId(req?.currentPageUriSegments?.authority);
14
+ const [{ operation }] = responseContext.document.definitions;
15
+ const isMutation = operation === 'mutation';
16
+ if (hasErrors || !queriesToInvalidate?.length || !isMutation) {
17
+ return;
18
+ }
19
+ const nestedKeys = await Promise.all(queriesToInvalidate.map(async (query) => {
20
+ // Build keys in order of specificity
21
+ let keys = [];
22
+ const cachePolicy = getDirectiveArgsFromSchema({
23
+ schema,
24
+ queryName: query,
25
+ directiveName: CACHE_CONTROL_DIRECTIVE,
26
+ });
27
+ const isPrivate = cachePolicy.scope?.toLowerCase() === 'private';
28
+ // Add tenant-specific key if tenant exists
29
+ if (tenantId) {
30
+ keys.push(`${config.APP_NAME}:${tenantId}:${query}:*`);
31
+ // Add user-specific key if user exists
32
+ if (user?.sub && isPrivate) {
33
+ keys.push(`${config.APP_NAME}:${tenantId}:${user.sub}:${query}:*`);
34
+ }
35
+ }
36
+ // Add global key as fallback
37
+ keys.push(`${config.APP_NAME}:${query}:*`);
38
+ // Allow custom key generation if provided
39
+ if (typeof invalidateCacheKeyGenerator === 'function') {
40
+ keys = keys.map((key) => invalidateCacheKeyGenerator(requestContext, responseContext, key));
41
+ }
42
+ const matchedKeys = await Promise.all(keys.map(async (key) => {
43
+ const matchingKeys = await redisClient.keys(key);
44
+ if (matchingKeys.length) {
45
+ return matchingKeys;
46
+ }
47
+ return [];
48
+ }));
49
+ return matchedKeys.flat();
50
+ }));
51
+ const keys = nestedKeys.flat();
52
+ if (keys?.length) {
53
+ await redisClient.del(uniq(keys));
54
+ }
55
+ }
56
+ catch (e) {
57
+ requestContext.logger.error('Error occurred in invalidateCachePlugin');
58
+ requestContext.logger.error(e);
59
+ }
60
+ },
61
+ };
62
+ },
63
+ });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;\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 const { queriesToInvalidate, user, req } = requestContext.contextValue;\n const tenantId = extractTenantId(req?.currentPageUriSegments?.authority);\n const [{ operation }] = responseContext.document.definitions as OperationDefinitionNode[];\n const isMutation = operation === 'mutation';\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 const cachePolicy: { scope: string } = getDirectiveArgsFromSchema({\n schema,\n queryName: query,\n directiveName: CACHE_CONTROL_DIRECTIVE,\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 const matchedKeys = await Promise.all(\n keys.map(async (key) => {\n const matchingKeys = await redisClient.keys(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":"qLAcO,MAAM,qBAAqB,GAAG,CAAC,EAClC,KAAK,EAAE,WAAW,EAClB,2BAA2B,GAI9B,MACI;AACG,IAAA,eAAe,CAAC,cAA2D,EAAA;QACvE,OAAO;AACH,YAAA,gBAAgB,EAAE,OAAO,eAA+D,KAAI;;gBAExF,IAAI,CAAC,cAAc,EAAE;oBACjB,OAAO;iBACV;AACD,gBAAA,IAAI;oBACA,MAAM,SAAS,GAAG,CAAC,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC;AAClD,oBAAA,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC;oBAClC,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,cAAc,CAAC,YAAY,CAAC;oBACvE,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,EAAE,sBAAsB,EAAE,SAAS,CAAC,CAAC;oBACzE,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC,WAAwC,CAAC;AAC1F,oBAAA,MAAM,UAAU,GAAG,SAAS,KAAK,UAAU,CAAC;oBAC5C,IAAI,SAAS,IAAI,CAAC,mBAAmB,EAAE,MAAM,IAAI,CAAC,UAAU,EAAE;wBAC1D,OAAO;qBACV;AAED,oBAAA,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,mBAAmB,CAAC,GAAG,CAAC,OAAO,KAAa,KAAI;;wBAE5C,IAAI,IAAI,GAAa,EAAE,CAAC;wBACxB,MAAM,WAAW,GAAsB,0BAA0B,CAAC;4BAC9D,MAAM;AACN,4BAAA,SAAS,EAAE,KAAK;AAChB,4BAAA,aAAa,EAAE,uBAAuB;AACzC,yBAAA,CAAC,CAAC;wBACH,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,SAAS,CAAC;;wBAEjE,IAAI,QAAQ,EAAE;AACV,4BAAA,IAAI,CAAC,IAAI,CAAC,CAAA,EAAG,MAAM,CAAC,QAAQ,CAAA,CAAA,EAAI,QAAQ,CAAA,CAAA,EAAI,KAAK,CAAA,EAAA,CAAI,CAAC,CAAC;;AAEvD,4BAAA,IAAI,IAAI,EAAE,GAAG,IAAI,SAAS,EAAE;AACxB,gCAAA,IAAI,CAAC,IAAI,CAAC,CAAG,EAAA,MAAM,CAAC,QAAQ,CAAA,CAAA,EAAI,QAAQ,CAAA,CAAA,EAAI,IAAI,CAAC,GAAG,IAAI,KAAK,CAAA,EAAA,CAAI,CAAC,CAAC;6BACtE;yBACJ;;wBAED,IAAI,CAAC,IAAI,CAAC,CAAG,EAAA,MAAM,CAAC,QAAQ,CAAI,CAAA,EAAA,KAAK,CAAI,EAAA,CAAA,CAAC,CAAC;;AAG3C,wBAAA,IAAI,OAAO,2BAA2B,KAAK,UAAU,EAAE;AACnD,4BAAA,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,KAChB,2BAA2B,CAAC,cAAc,EAAE,eAAe,EAAE,GAAG,CAAC,CACpE,CAAC;yBACL;AACD,wBAAA,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,GAAG,CACjC,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,KAAI;4BACnB,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACjD,4BAAA,IAAI,YAAY,CAAC,MAAM,EAAE;AACrB,gCAAA,OAAO,YAAY,CAAC;6BACvB;AACD,4BAAA,OAAO,EAAE,CAAC;yBACb,CAAC,CACL,CAAC;AACF,wBAAA,OAAO,WAAW,CAAC,IAAI,EAAE,CAAC;qBAC7B,CAAC,CACL,CAAC;AACF,oBAAA,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC;AAC/B,oBAAA,IAAI,IAAI,EAAE,MAAM,EAAE;wBACd,MAAM,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;qBACrC;iBACJ;gBAAC,OAAO,CAAC,EAAE;AACR,oBAAA,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;AACvE,oBAAA,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;iBAClC;aACJ;SACJ,CAAC;KACL;AACJ,CAAA"}
@@ -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\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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"}
@@ -62,7 +62,9 @@ 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;
68
70
  /**
@@ -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,20 @@ 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);
152
- const sanitizedTenantId = sanitizeRedisKeyComponent(tenantId);
153
- const sanitizedUserId = sanitizeRedisKeyComponent(userId);
154
- return `${sanitizedAppName}:${sanitizedTenantId}:${sanitizedUserId}:${legacyKey}`;
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
+ });
155
172
  }
156
173
  /**
157
174
  * Clear all cache entries for a tenant
@@ -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, 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 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 * 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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"}
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@common-stack/store-redis",
3
- "version": "8.3.1-alpha.5",
3
+ "version": "8.3.1-alpha.7",
4
4
  "description": "Redis store utilities and services for common-stack",
5
5
  "license": "UNLICENSED",
6
6
  "author": "CDMBase LLC",
@@ -24,10 +24,10 @@
24
24
  "watch": "npm run build:lib:watch"
25
25
  },
26
26
  "dependencies": {
27
- "@common-stack/core": "8.3.1-alpha.4"
27
+ "@common-stack/core": "8.3.1-alpha.7"
28
28
  },
29
29
  "devDependencies": {
30
- "common": "8.3.1-alpha.4",
30
+ "common": "8.3.1-alpha.7",
31
31
  "ioredis": "^5.3.2"
32
32
  },
33
33
  "peerDependencies": {
@@ -46,14 +46,15 @@
46
46
  "repositories": [
47
47
  "./${libDir}/templates/repositories/RedisKeyBuilder.ts.template",
48
48
  "./${libDir}/templates/repositories/RedisClient.ts.template",
49
- "./${libDir}/templates/repositories/redisCommonTypes.ts.template"
49
+ "./${libDir}/templates/repositories/redisCommonTypes.ts.template",
50
+ "./${libDir}/templates/repositories/GraphQLCacheContext.ts.template"
50
51
  ],
51
52
  "services": [
52
53
  "./${libDir}/templates/services/RedisCacheManager.ts.template"
53
54
  ]
54
55
  }
55
56
  },
56
- "gitHead": "6b32dab86460827a36926af972e051b580992729",
57
+ "gitHead": "4bb669b7168f95f0a0ebe05100545ea5fd353cf4",
57
58
  "typescript": {
58
59
  "definition": "lib/index.d.ts"
59
60
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};