@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.
- 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/index.d.ts +1 -0
- package/lib/index.js +1 -1
- package/lib/plugins/index.d.ts +2 -0
- package/lib/plugins/invalidateCachePlugin.d.ts +8 -0
- package/lib/plugins/invalidateCachePlugin.js +63 -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/templates/repositories/GraphQLCacheContext.ts.template +14 -0
- package/package.json +6 -5
- package/lib/__tests__/redis-key-builder.test.d.ts +0 -1
- package/lib/__tests__/redis-key-sanitizer.test.d.ts +0 -1
|
@@ -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
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,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.
|
|
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.
|
|
27
|
+
"@common-stack/core": "8.3.1-alpha.7"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"common": "8.3.1-alpha.
|
|
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": "
|
|
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 {};
|