strapi-cache 1.10.0 → 1.10.1

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/README.md CHANGED
@@ -72,7 +72,7 @@ Full configuration example:
72
72
  size: 1024 * 1024 * 1024, // Maximum size of the cache (1 GB) (only for memory cache)
73
73
  allowStale: false, // Allow stale cache items (only for memory cache)
74
74
  cacheableRoutes: ['/api/products', '/api/categories'], // Caches routes which start with these paths (if empty array, all '/api' routes are cached)
75
- keyGenerator: (ctx) => `${ctx.request.method}:${ctx.request.url}`, // Optional custom cache key for REST requests; receives koa ctx
75
+ // keyGenerator: (ctx) => `${ctx.request.method}:${ctx.request.url}`, // (Optional) custom cache key for REST + GraphQL requests; receives koa ctx
76
76
  // cacheableEntities: ['products', 'categories'], // (Optional) Specify which entities to cache. When set, only these entities will be cached (ignores cacheableRoutes). If not set (undefined), cacheableRoutes logic is used
77
77
  excludeRoutes: ['/api/products/private'], // Exclude routes which start with these paths from being cached (takes precedence over cacheableRoutes). **Note:** `excludeRoutes` takes precedence over `cacheableRoutes`.
78
78
  provider: 'memory', // Cache provider ('memory', 'redis' or 'valkey')
@@ -97,32 +97,32 @@ Full configuration example:
97
97
 
98
98
  Possible configuration keys are listed below; omitted keys keep the plugin defaults.
99
99
 
100
- | Key | Description | Possible values |
101
- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
102
- | `debug` | Log cache decisions and operations to the server console | `true` or `false` (default: `false`) |
103
- | `provider` | Where entries are stored | `'memory'`, `'redis'`, or `'valkey'` (default: `'memory'`) |
104
- | `redisConfig` | Redis/Valkey connection: URL string or client options passed to ioredis/iovalkey | String or object; **required** when `provider` is `'redis'` or `'valkey'`. Default: value of `REDIS_URL` from the environment |
105
- | `redisClusterNodes` | Seed nodes for Redis cluster mode; non-empty list switches to a cluster client | Array of `{ host: string, port: number }` (default: `[]`) |
106
- | `redisClusterOptions` | Options for the cluster client (e.g. `scaleReads`); `redisOptions` often come from `redisConfig` | Object (default: `{}`) |
107
- | `redisScanDeleteCount` | `COUNT` hint for `SCAN` when purging keys (Redis/Valkey) | Positive number (default: `100`) |
108
- | `max` | Maximum number of entries (in-memory provider only) | Positive integer (default: `1000`) |
109
- | `ttl` | Time-to-live for each entry, in milliseconds | Non-negative number (default: `3600000`, i.e. 1 hour) |
110
- | `size` | Approximate max total size in bytes (in-memory provider only) | Positive integer (default: `10485760`, i.e. 10 MB) |
111
- | `allowStale` | Whether stale entries may be returned (in-memory provider only) | `true` or `false` (default: `false`) |
112
- | `cacheableRoutes` | Only URLs starting with one of these paths are cached; if empty, every URL under the REST API prefix matches | Array of path prefix strings (default: `[]` meaning “all API routes”) |
113
- | `keyGenerator` | Custom function to build REST cache keys; receives Koa `ctx`; when omitted, default key is `${method}:${url}` | Function `(ctx) => string` (default: unset) |
114
- | `cacheableEntities` | If non-empty, only these API “entity” segments are cached; **when set, this drives eligibility instead of** `cacheableRoutes` | Array of strings (e.g. collection/table names), or omit / leave empty to use `cacheableRoutes` |
115
- | `excludeRoutes` | URLs starting with any of these prefixes are **never** cached; evaluated before `cacheableRoutes` / entities | Array of path prefix strings (default: `[]`) |
116
- | `cacheHeaders` | Store and replay response headers with the body | `true` or `false` (default: `true`) |
117
- | `cacheHeadersDenyList` | Header names (lowercase) to strip when `cacheHeaders` is `true` | Array of strings (default: `[]`) |
118
- | `cacheHeadersAllowList` | If non-empty, only these header names (lowercase) are stored; if empty, all headers are stored (subject to deny list) | Array of strings (default: `[]`) |
119
- | `cacheAuthorizedRequests` | Whether to cache requests that include an `Authorization` header | `true` or `false` (default: `false`) |
120
- | `cacheGetTimeoutInMs` | Max time to wait for a cache read before treating it as a miss | Milliseconds (default: `1000`) |
121
- | `autoPurgeCache` | Invalidate relevant REST cache entries after content create/update/delete | `true` or `false` (default: `true`) |
122
- | `autoPurgeGraphQL` | Invalidate GraphQL cache after content create/update/delete | `true` or `false` (default: `false` if omitted; set `true` to enable) |
123
- | `autoPurgeCacheOnStart` | Clear the cache when Strapi starts | `true` or `false` (default: `true`) |
124
- | `disableAdminPopups` | Turn off admin UI notifications for cache actions | `true` or `false` (default: `false`) |
125
- | `disableAdminButtons` | Hide manual purge controls in the admin (list and edit views) | `true` or `false` (default: `false`) |
100
+ | Key | Description | Possible values |
101
+ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
102
+ | `debug` | Log cache decisions and operations to the server console | `true` or `false` (default: `false`) |
103
+ | `provider` | Where entries are stored | `'memory'`, `'redis'`, or `'valkey'` (default: `'memory'`) |
104
+ | `redisConfig` | Redis/Valkey connection: URL string or client options passed to ioredis/iovalkey | String or object; **required** when `provider` is `'redis'` or `'valkey'`. Default: value of `REDIS_URL` from the environment |
105
+ | `redisClusterNodes` | Seed nodes for Redis cluster mode; non-empty list switches to a cluster client | Array of `{ host: string, port: number }` (default: `[]`) |
106
+ | `redisClusterOptions` | Options for the cluster client (e.g. `scaleReads`); `redisOptions` often come from `redisConfig` | Object (default: `{}`) |
107
+ | `redisScanDeleteCount` | `COUNT` hint for `SCAN` when purging keys (Redis/Valkey) | Positive number (default: `100`) |
108
+ | `max` | Maximum number of entries (in-memory provider only) | Positive integer (default: `1000`) |
109
+ | `ttl` | Time-to-live for each entry, in milliseconds | Non-negative number (default: `3600000`, i.e. 1 hour) |
110
+ | `size` | Approximate max total size in bytes (in-memory provider only) | Positive integer (default: `10485760`, i.e. 10 MB) |
111
+ | `allowStale` | Whether stale entries may be returned (in-memory provider only) | `true` or `false` (default: `false`) |
112
+ | `cacheableRoutes` | Only URLs starting with one of these paths are cached; if empty, every URL under the REST API prefix matches | Array of path prefix strings (default: `[]` meaning “all API routes”) |
113
+ | `keyGenerator` | Custom function to build REST cache keys; receives Koa `ctx`; when omitted, default key is `${method}:${url}`; for graphql, if set, the ctx gets additional fields: `rootFields` and `operationName` which may be useful for invalidation logic | Function `(ctx) => string` (default: unset) |
114
+ | `cacheableEntities` | If non-empty, only these API “entity” segments are cached; **when set, this drives eligibility instead of** `cacheableRoutes` | Array of strings (e.g. collection/table names), or omit / leave empty to use `cacheableRoutes` |
115
+ | `excludeRoutes` | URLs starting with any of these prefixes are **never** cached; evaluated before `cacheableRoutes` / entities | Array of path prefix strings (default: `[]`) |
116
+ | `cacheHeaders` | Store and replay response headers with the body | `true` or `false` (default: `true`) |
117
+ | `cacheHeadersDenyList` | Header names (lowercase) to strip when `cacheHeaders` is `true` | Array of strings (default: `[]`) |
118
+ | `cacheHeadersAllowList` | If non-empty, only these header names (lowercase) are stored; if empty, all headers are stored (subject to deny list) | Array of strings (default: `[]`) |
119
+ | `cacheAuthorizedRequests` | Whether to cache requests that include an `Authorization` header | `true` or `false` (default: `false`) |
120
+ | `cacheGetTimeoutInMs` | Max time to wait for a cache read before treating it as a miss | Milliseconds (default: `1000`) |
121
+ | `autoPurgeCache` | Invalidate relevant REST cache entries after content create/update/delete | `true` or `false` (default: `true`) |
122
+ | `autoPurgeGraphQL` | Invalidate GraphQL cache after content create/update/delete | `true` or `false` (default: `false` if omitted; set `true` to enable) |
123
+ | `autoPurgeCacheOnStart` | Clear the cache when Strapi starts | `true` or `false` (default: `true`) |
124
+ | `disableAdminPopups` | Turn off admin UI notifications for cache actions | `true` or `false` (default: `false`) |
125
+ | `disableAdminButtons` | Hide manual purge controls in the admin (list and edit views) | `true` or `false` (default: `false`) |
126
126
 
127
127
  ## 🔍 Routes
128
128
 
@@ -31,6 +31,9 @@ const loggy = {
31
31
  strapi.log.warn(`[STRAPI CACHE] ${msg}`);
32
32
  }
33
33
  };
34
+ const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35
+ const getGraphqlEndpoint = (strapi2) => strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql";
36
+ const getGraphqlAllKeysRegex = (strapi2) => new RegExp(`^(GET|POST):${escapeRegex(getGraphqlEndpoint(strapi2))}.*`);
34
37
  async function invalidateCache(event, cacheStore, strapi2) {
35
38
  const { model } = event;
36
39
  const uid = model.uid;
@@ -63,7 +66,7 @@ async function invalidateGraphqlCache(event, cacheStore, strapi2) {
63
66
  const contentType = strapi2.contentType(model.uid);
64
67
  if (!contentType || !contentType.info) {
65
68
  loggy.info(`Content type ${model.uid} not found, purging all GraphQL cache`);
66
- const graphqlRegex2 = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:.*`);
69
+ const graphqlRegex2 = getGraphqlAllKeysRegex(strapi2);
67
70
  await cacheStore.clearByRegexp([graphqlRegex2]);
68
71
  return;
69
72
  }
@@ -72,12 +75,14 @@ async function invalidateGraphqlCache(event, cacheStore, strapi2) {
72
75
  const fieldNames = [...new Set([singularName, pluralName].filter(Boolean))];
73
76
  if (fieldNames.length === 0) {
74
77
  loggy.info(`No field names for ${model.uid}, purging all GraphQL cache`);
75
- const graphqlRegex2 = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:.*`);
78
+ const graphqlRegex2 = getGraphqlAllKeysRegex(strapi2);
76
79
  await cacheStore.clearByRegexp([graphqlRegex2]);
77
80
  return;
78
81
  }
79
- const escapedNames = fieldNames.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
80
- const graphqlRegex = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:[^:]*\\b(${escapedNames})\\b[^:]*:`);
82
+ const escapedNames = fieldNames.map((name) => escapeRegex(name)).join("|");
83
+ const graphqlRegex = new RegExp(
84
+ `^(GET|POST):${escapeRegex(getGraphqlEndpoint(strapi2))}:[^:]*\\b(${escapedNames})\\b[^:]*:`
85
+ );
81
86
  await cacheStore.clearByRegexp([graphqlRegex]);
82
87
  loggy.info(`Invalidated GraphQL cache for ${model.uid} (${fieldNames.join(", ")})`);
83
88
  } catch (error) {
@@ -152,12 +157,21 @@ const bootstrap = ({ strapi: strapi2 }) => {
152
157
  loggy.info("Plugin initialized");
153
158
  strapi2.admin.services.permission.actionProvider.registerMany(actions);
154
159
  };
160
+ const getCustomCacheKey = (context, keyGenerator) => {
161
+ if (typeof keyGenerator !== "function") {
162
+ return void 0;
163
+ }
164
+ const customKey = keyGenerator(context);
165
+ if (typeof customKey === "string") {
166
+ return customKey;
167
+ }
168
+ return void 0;
169
+ };
170
+ const resolveGraphqlCacheKey = (context, fallbackKey, keyGenerator) => getCustomCacheKey(context, keyGenerator) ?? fallbackKey;
155
171
  const generateCacheKey = (context, keyGenerator) => {
156
- if (typeof keyGenerator === "function") {
157
- const customKey = keyGenerator(context);
158
- if (typeof customKey === "string") {
159
- return customKey;
160
- }
172
+ const customKey = getCustomCacheKey(context, keyGenerator);
173
+ if (customKey !== void 0) {
174
+ return customKey;
161
175
  }
162
176
  const { url } = context.request;
163
177
  const { method } = context.request;
@@ -402,6 +416,7 @@ const middleware = async (ctx, next) => {
402
416
  return;
403
417
  }
404
418
  const cacheService = strapi.plugin("strapi-cache").services.service;
419
+ const keyGenerator = strapi.plugin("strapi-cache").config("keyGenerator");
405
420
  const { cacheHeaders, cacheHeadersDenyList, cacheHeadersAllowList, cacheAuthorizedRequests } = getCacheHeaderConfig();
406
421
  const cacheStore = cacheService.getCacheInstance();
407
422
  const isGet = method === "GET";
@@ -431,7 +446,10 @@ const middleware = async (ctx, next) => {
431
446
  }
432
447
  const payload = parseGraphqlPayload(body, isGet);
433
448
  const rootFields = getRootFieldsFromQuery(payload.query);
434
- const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields, strapi);
449
+ const graphqlKey = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields, strapi);
450
+ ctx.rootFields = rootFields.length ? rootFields : void 0;
451
+ ctx.operationName = payload.operationName;
452
+ const key = resolveGraphqlCacheKey(ctx, graphqlKey, keyGenerator);
435
453
  loggy.info(
436
454
  `GraphQL request: ${JSON.stringify({
437
455
  operationName: payload.operationName,
@@ -27,6 +27,9 @@ const loggy = {
27
27
  strapi.log.warn(`[STRAPI CACHE] ${msg}`);
28
28
  }
29
29
  };
30
+ const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ const getGraphqlEndpoint = (strapi2) => strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql";
32
+ const getGraphqlAllKeysRegex = (strapi2) => new RegExp(`^(GET|POST):${escapeRegex(getGraphqlEndpoint(strapi2))}.*`);
30
33
  async function invalidateCache(event, cacheStore, strapi2) {
31
34
  const { model } = event;
32
35
  const uid = model.uid;
@@ -59,7 +62,7 @@ async function invalidateGraphqlCache(event, cacheStore, strapi2) {
59
62
  const contentType = strapi2.contentType(model.uid);
60
63
  if (!contentType || !contentType.info) {
61
64
  loggy.info(`Content type ${model.uid} not found, purging all GraphQL cache`);
62
- const graphqlRegex2 = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:.*`);
65
+ const graphqlRegex2 = getGraphqlAllKeysRegex(strapi2);
63
66
  await cacheStore.clearByRegexp([graphqlRegex2]);
64
67
  return;
65
68
  }
@@ -68,12 +71,14 @@ async function invalidateGraphqlCache(event, cacheStore, strapi2) {
68
71
  const fieldNames = [...new Set([singularName, pluralName].filter(Boolean))];
69
72
  if (fieldNames.length === 0) {
70
73
  loggy.info(`No field names for ${model.uid}, purging all GraphQL cache`);
71
- const graphqlRegex2 = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:.*`);
74
+ const graphqlRegex2 = getGraphqlAllKeysRegex(strapi2);
72
75
  await cacheStore.clearByRegexp([graphqlRegex2]);
73
76
  return;
74
77
  }
75
- const escapedNames = fieldNames.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
76
- const graphqlRegex = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:[^:]*\\b(${escapedNames})\\b[^:]*:`);
78
+ const escapedNames = fieldNames.map((name) => escapeRegex(name)).join("|");
79
+ const graphqlRegex = new RegExp(
80
+ `^(GET|POST):${escapeRegex(getGraphqlEndpoint(strapi2))}:[^:]*\\b(${escapedNames})\\b[^:]*:`
81
+ );
77
82
  await cacheStore.clearByRegexp([graphqlRegex]);
78
83
  loggy.info(`Invalidated GraphQL cache for ${model.uid} (${fieldNames.join(", ")})`);
79
84
  } catch (error) {
@@ -148,12 +153,21 @@ const bootstrap = ({ strapi: strapi2 }) => {
148
153
  loggy.info("Plugin initialized");
149
154
  strapi2.admin.services.permission.actionProvider.registerMany(actions);
150
155
  };
156
+ const getCustomCacheKey = (context, keyGenerator) => {
157
+ if (typeof keyGenerator !== "function") {
158
+ return void 0;
159
+ }
160
+ const customKey = keyGenerator(context);
161
+ if (typeof customKey === "string") {
162
+ return customKey;
163
+ }
164
+ return void 0;
165
+ };
166
+ const resolveGraphqlCacheKey = (context, fallbackKey, keyGenerator) => getCustomCacheKey(context, keyGenerator) ?? fallbackKey;
151
167
  const generateCacheKey = (context, keyGenerator) => {
152
- if (typeof keyGenerator === "function") {
153
- const customKey = keyGenerator(context);
154
- if (typeof customKey === "string") {
155
- return customKey;
156
- }
168
+ const customKey = getCustomCacheKey(context, keyGenerator);
169
+ if (customKey !== void 0) {
170
+ return customKey;
157
171
  }
158
172
  const { url } = context.request;
159
173
  const { method } = context.request;
@@ -398,6 +412,7 @@ const middleware = async (ctx, next) => {
398
412
  return;
399
413
  }
400
414
  const cacheService = strapi.plugin("strapi-cache").services.service;
415
+ const keyGenerator = strapi.plugin("strapi-cache").config("keyGenerator");
401
416
  const { cacheHeaders, cacheHeadersDenyList, cacheHeadersAllowList, cacheAuthorizedRequests } = getCacheHeaderConfig();
402
417
  const cacheStore = cacheService.getCacheInstance();
403
418
  const isGet = method === "GET";
@@ -427,7 +442,10 @@ const middleware = async (ctx, next) => {
427
442
  }
428
443
  const payload = parseGraphqlPayload(body, isGet);
429
444
  const rootFields = getRootFieldsFromQuery(payload.query);
430
- const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields, strapi);
445
+ const graphqlKey = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields, strapi);
446
+ ctx.rootFields = rootFields.length ? rootFields : void 0;
447
+ ctx.operationName = payload.operationName;
448
+ const key = resolveGraphqlCacheKey(ctx, graphqlKey, keyGenerator);
431
449
  loggy.info(
432
450
  `GraphQL request: ${JSON.stringify({
433
451
  operationName: payload.operationName,
@@ -1,6 +1,8 @@
1
1
  import { Core } from '@strapi/strapi';
2
2
  import { Context } from 'koa';
3
3
  export type CacheKeyGenerator = (context: Context) => string;
4
+ export declare const getCustomCacheKey: (context: Context, keyGenerator?: CacheKeyGenerator) => string;
5
+ export declare const resolveGraphqlCacheKey: (context: Context, fallbackKey: string, keyGenerator?: CacheKeyGenerator) => string;
4
6
  export declare const generateCacheKey: (context: Context, keyGenerator?: CacheKeyGenerator) => string;
5
7
  export declare const generateGraphqlCacheKey: (payload: string, method?: 'GET' | 'POST', rootFields?: string[], strapi?: Core.Strapi) => string;
6
8
  export declare const escapeRegExp: (s: string) => string;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.10.0",
2
+ "version": "1.10.1",
3
3
  "keywords": [
4
4
  "strapi cache",
5
5
  "strapi rest cache",