@common-stack/store-redis 8.3.1-alpha.6 → 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":"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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\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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"}
@@ -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.6",
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": "fb84350cf08d0ecab5689c99c8cc03e151fd3ace",
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 {};