strapi-cache 1.11.0 → 1.11.2

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
@@ -79,6 +79,7 @@ Full configuration example:
79
79
  redisConfig: env('REDIS_URL', 'redis://localhost:6379'), // Redis/Valkey config: string or object. See https://github.com/redis/ioredis (Redis) or https://github.com/valkey-io/iovalkey (Valkey)
80
80
  redisClusterNodes: [], // If provided any cluster node (this list is not empty), initialize cluster client. Each object must have keys 'host' and 'port'
81
81
  redisClusterOptions: {}, // Options for ioredis redis cluster client. redisOptions key is taken from redisConfig parameter above if not set here. See https://github.com/redis/ioredis for references
82
+ redisScanDeleteCount: 100, // COUNT hint for SCAN when purging keys (Redis/Valkey only)
82
83
  cacheHeaders: true, // Plugin also stores response headers in the cache (set to false if you don't want to cache headers)
83
84
  cacheHeadersDenyList: ['access-control-allow-origin', 'content-encoding'], // Headers to exclude from the cache (must be lowercase, if empty array, no headers are excluded, cacheHeaders must be true)
84
85
  cacheHeadersAllowList: ['content-type', 'content-security-policy'], // Headers to include in the cache (must be lowercase, if empty array, all headers are cached, cacheHeaders must be true)
@@ -139,7 +140,7 @@ All of these routes are protected by the policies `admin::isAuthenticatedAdmin`
139
140
 
140
141
  - **Storage**: The plugin keeps cached data in memory, Redis or Valkey, depending on the configuration.
141
142
  - **Packages**: Uses [lru-cache](https://github.com/isaacs/node-lru-cache) for in-memory cache. Uses [ioredis](https://github.com/redis/ioredis) for Redis and [iovalkey](https://github.com/valkey-io/iovalkey) for Valkey caching.
142
- - **Automatic Invalidation**: When `autoPurgeCache` is enabled (default), relevant REST cache entries are invalidated on content create, update, or delete. When `autoPurgeGraphQL` is enabled, GraphQL cache is invalidated the same way (it is off unless you set it in config).
143
+ - **Automatic Invalidation**: When `autoPurgeCache` is enabled (default), relevant REST cache entries are invalidated on content create, update, or delete. REST invalidation honors `excludeRoutes`, `cacheableEntities`, and `cacheableRoutes`, and runs in the background so content writes do not wait for cache scans to finish. When `autoPurgeGraphQL` is enabled, GraphQL cache is invalidated the same way (it is off unless you set it in config).
143
144
  - **`no-cache` Header Support**: Respects the `no-cache` header, letting you skip the cache by setting `Cache-Control: no-cache` in your request.
144
145
  - **Default Cached Requests**: By default, caches all GET requests to `/api` (or whatever prefix you defined) and POST requests to `/graphql` or predefined graphql route from graphql plugin config. You can customize which routes or entities to cache using `cacheableRoutes` or `cacheableEntities` config options.
145
146
 
@@ -1,2 +1,3 @@
1
- declare const PluginIcon: () => import("react/jsx-runtime").JSX.Element;
1
+ /// <reference types="react" />
2
+ declare const PluginIcon: () => import("react").JSX.Element;
2
3
  export { PluginIcon };
@@ -1,2 +1,3 @@
1
- declare function PurgeCacheButton(): import("react/jsx-runtime").JSX.Element | null;
1
+ /// <reference types="react" />
2
+ declare function PurgeCacheButton(): import("react").JSX.Element | null;
2
3
  export default PurgeCacheButton;
@@ -1,2 +1,3 @@
1
- declare function PurgeEntityButton(): import("react/jsx-runtime").JSX.Element | null;
1
+ /// <reference types="react" />
2
+ declare function PurgeEntityButton(): import("react").JSX.Element | null;
2
3
  export default PurgeEntityButton;
@@ -1,3 +1,4 @@
1
+ /// <reference types="react" />
1
2
  export type PurgeProps = {
2
3
  buttonText: string;
3
4
  buttonWidth?: string;
@@ -6,5 +7,5 @@ export type PurgeProps = {
6
7
  isSettingsPage?: boolean;
7
8
  isPurgeAll?: boolean;
8
9
  };
9
- declare function PurgeModal({ buttonText, keyToUse, buttonWidth, contentTypeName, isSettingsPage, isPurgeAll, }: PurgeProps): import("react/jsx-runtime").JSX.Element | null;
10
+ declare function PurgeModal({ buttonText, keyToUse, buttonWidth, contentTypeName, isSettingsPage, isPurgeAll, }: PurgeProps): import("react").JSX.Element | null;
10
11
  export default PurgeModal;
@@ -1,2 +1,3 @@
1
- declare const SettingsPage: () => import("react/jsx-runtime").JSX.Element;
1
+ /// <reference types="react" />
2
+ declare const SettingsPage: () => import("react").JSX.Element;
2
3
  export default SettingsPage;
@@ -39,11 +39,8 @@ async function invalidateCache(event, cacheStore, strapi2) {
39
39
  const uid = model.uid;
40
40
  const restApiPrefix = strapi2.config.get("api.rest.prefix", "/api");
41
41
  const cacheableEntities = strapi2.plugin("strapi-cache").config("cacheableEntities");
42
- const entityIsCacheable = cacheableEntities?.length ? cacheableEntities.includes(model.tableName) : true;
43
- if (!entityIsCacheable) {
44
- loggy.info(`Not invalidated. ${model.tableName} is not cacheable.`);
45
- return;
46
- }
42
+ const cacheableRoutes = strapi2.plugin("strapi-cache").config("cacheableRoutes") ?? [];
43
+ const excludeRoutes = strapi2.plugin("strapi-cache").config("excludeRoutes") ?? [];
47
44
  try {
48
45
  const contentType = strapi2.contentType(uid);
49
46
  if (!contentType || !contentType.kind) {
@@ -52,7 +49,20 @@ async function invalidateCache(event, cacheStore, strapi2) {
52
49
  }
53
50
  const pluralName = contentType.kind === "singleType" ? contentType.info.singularName : contentType.info.pluralName;
54
51
  const apiPath = `${restApiPrefix}/${pluralName}`;
55
- const regex = new RegExp(`^.*:${apiPath}(/.*)?(\\?.*)?$`);
52
+ const entityNames = [
53
+ model.tableName,
54
+ contentType.info.singularName,
55
+ contentType.info.pluralName
56
+ ].filter(Boolean);
57
+ const entityIsCacheable = cacheableEntities?.length ? entityNames.some((name) => cacheableEntities.includes(name)) : void 0;
58
+ const routeIsExcluded = excludeRoutes.some((route) => apiPath.startsWith(route));
59
+ const routeIsCacheable = cacheableRoutes.some((route) => apiPath.startsWith(route)) || cacheableRoutes.length === 0 && apiPath.startsWith(restApiPrefix);
60
+ const isCacheable = !routeIsExcluded && (entityIsCacheable ?? routeIsCacheable);
61
+ if (!isCacheable) {
62
+ loggy.info(`Not invalidated. ${apiPath} is not cacheable.`);
63
+ return;
64
+ }
65
+ const regex = new RegExp(`^.*:${escapeRegex(apiPath)}(/.*)?(\\?.*)?$`);
56
66
  await cacheStore.clearByRegexp([regex]);
57
67
  loggy.info(`Invalidated cache for ${apiPath}`);
58
68
  } catch (error) {
@@ -104,6 +114,12 @@ const actions = [
104
114
  pluginName: "strapi-cache"
105
115
  }
106
116
  ];
117
+ const runInBackground = (label, task) => {
118
+ void Promise.resolve().then(task).catch((error) => {
119
+ loggy.error(`${label} error:`);
120
+ loggy.error(error);
121
+ });
122
+ };
107
123
  const bootstrap = ({ strapi: strapi2 }) => {
108
124
  loggy.info("Initializing");
109
125
  try {
@@ -119,27 +135,36 @@ const bootstrap = ({ strapi: strapi2 }) => {
119
135
  cacheStore.init();
120
136
  if (autoPurgeCache) {
121
137
  strapi2.db.lifecycles.subscribe({
122
- async afterCreate(event) {
123
- await invalidateCache(event, cacheStore, strapi2);
138
+ afterCreate(event) {
139
+ runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
124
140
  },
125
- async afterUpdate(event) {
126
- await invalidateCache(event, cacheStore, strapi2);
141
+ afterUpdate(event) {
142
+ runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
127
143
  },
128
- async afterDelete(event) {
129
- await invalidateCache(event, cacheStore, strapi2);
144
+ afterDelete(event) {
145
+ runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
130
146
  }
131
147
  });
132
148
  }
133
149
  if (autoPurgeGraphQL) {
134
150
  strapi2.db.lifecycles.subscribe({
135
- async afterCreate(event) {
136
- await invalidateGraphqlCache(event, cacheStore, strapi2);
151
+ afterCreate(event) {
152
+ runInBackground(
153
+ "GraphQL cache invalidation",
154
+ () => invalidateGraphqlCache(event, cacheStore, strapi2)
155
+ );
137
156
  },
138
- async afterUpdate(event) {
139
- await invalidateGraphqlCache(event, cacheStore, strapi2);
157
+ afterUpdate(event) {
158
+ runInBackground(
159
+ "GraphQL cache invalidation",
160
+ () => invalidateGraphqlCache(event, cacheStore, strapi2)
161
+ );
140
162
  },
141
- async afterDelete(event) {
142
- await invalidateGraphqlCache(event, cacheStore, strapi2);
163
+ afterDelete(event) {
164
+ runInBackground(
165
+ "GraphQL cache invalidation",
166
+ () => invalidateGraphqlCache(event, cacheStore, strapi2)
167
+ );
143
168
  }
144
169
  });
145
170
  }
@@ -943,7 +968,7 @@ class RedisCacheProvider {
943
968
  async del(key) {
944
969
  if (!this.ready) return null;
945
970
  try {
946
- const relativeKey = key.slice(this.keyPrefix.length);
971
+ const relativeKey = this.keyPrefix && key.startsWith(this.keyPrefix) ? key.slice(this.keyPrefix.length) : key;
947
972
  loggy.info(`Redis PURGING KEY: ${relativeKey}`);
948
973
  await this.client.del(relativeKey);
949
974
  return true;
@@ -982,18 +1007,32 @@ class RedisCacheProvider {
982
1007
  }
983
1008
  }
984
1009
  async deleteBatch(client, batch, regExps) {
985
- const toDelete = batch.filter((key) => regExps.some((re) => re.test(key)));
1010
+ const toDelete = batch.filter((key) => {
1011
+ if (!this.hasConfiguredPrefix(key)) {
1012
+ return false;
1013
+ }
1014
+ const relativeKey = this.toRelativeKey(key);
1015
+ return regExps.some((re) => re.test(key) || re.test(relativeKey));
1016
+ });
986
1017
  if (toDelete.length === 0) return;
987
1018
  const pipeline = client.pipeline();
988
1019
  for (const key of toDelete) {
989
- const relativeKey = key.startsWith(this.keyPrefix) ? key.slice(this.keyPrefix.length) : key;
990
- pipeline.del(relativeKey);
1020
+ pipeline.del(this.toRelativeKey(key));
991
1021
  }
992
1022
  await pipeline.exec();
993
1023
  }
1024
+ hasConfiguredPrefix(key) {
1025
+ return !this.keyPrefix || key.startsWith(this.keyPrefix);
1026
+ }
1027
+ toRelativeKey(key) {
1028
+ return this.keyPrefix && key.startsWith(this.keyPrefix) ? key.slice(this.keyPrefix.length) : key;
1029
+ }
994
1030
  scanAndDelete(client, regExps) {
995
1031
  return new Promise((resolve, reject) => {
996
- const stream = client.scanStream({ match: `${this.keyPrefix}*`, count: this.redisScanDeleteCount });
1032
+ const stream = client.scanStream({
1033
+ match: `${this.keyPrefix}*`,
1034
+ count: this.redisScanDeleteCount
1035
+ });
997
1036
  stream.on("data", (batch) => {
998
1037
  stream.pause();
999
1038
  this.deleteBatch(client, batch, regExps).then(() => stream.resume()).catch(reject);
@@ -35,11 +35,8 @@ async function invalidateCache(event, cacheStore, strapi2) {
35
35
  const uid = model.uid;
36
36
  const restApiPrefix = strapi2.config.get("api.rest.prefix", "/api");
37
37
  const cacheableEntities = strapi2.plugin("strapi-cache").config("cacheableEntities");
38
- const entityIsCacheable = cacheableEntities?.length ? cacheableEntities.includes(model.tableName) : true;
39
- if (!entityIsCacheable) {
40
- loggy.info(`Not invalidated. ${model.tableName} is not cacheable.`);
41
- return;
42
- }
38
+ const cacheableRoutes = strapi2.plugin("strapi-cache").config("cacheableRoutes") ?? [];
39
+ const excludeRoutes = strapi2.plugin("strapi-cache").config("excludeRoutes") ?? [];
43
40
  try {
44
41
  const contentType = strapi2.contentType(uid);
45
42
  if (!contentType || !contentType.kind) {
@@ -48,7 +45,20 @@ async function invalidateCache(event, cacheStore, strapi2) {
48
45
  }
49
46
  const pluralName = contentType.kind === "singleType" ? contentType.info.singularName : contentType.info.pluralName;
50
47
  const apiPath = `${restApiPrefix}/${pluralName}`;
51
- const regex = new RegExp(`^.*:${apiPath}(/.*)?(\\?.*)?$`);
48
+ const entityNames = [
49
+ model.tableName,
50
+ contentType.info.singularName,
51
+ contentType.info.pluralName
52
+ ].filter(Boolean);
53
+ const entityIsCacheable = cacheableEntities?.length ? entityNames.some((name) => cacheableEntities.includes(name)) : void 0;
54
+ const routeIsExcluded = excludeRoutes.some((route) => apiPath.startsWith(route));
55
+ const routeIsCacheable = cacheableRoutes.some((route) => apiPath.startsWith(route)) || cacheableRoutes.length === 0 && apiPath.startsWith(restApiPrefix);
56
+ const isCacheable = !routeIsExcluded && (entityIsCacheable ?? routeIsCacheable);
57
+ if (!isCacheable) {
58
+ loggy.info(`Not invalidated. ${apiPath} is not cacheable.`);
59
+ return;
60
+ }
61
+ const regex = new RegExp(`^.*:${escapeRegex(apiPath)}(/.*)?(\\?.*)?$`);
52
62
  await cacheStore.clearByRegexp([regex]);
53
63
  loggy.info(`Invalidated cache for ${apiPath}`);
54
64
  } catch (error) {
@@ -100,6 +110,12 @@ const actions = [
100
110
  pluginName: "strapi-cache"
101
111
  }
102
112
  ];
113
+ const runInBackground = (label, task) => {
114
+ void Promise.resolve().then(task).catch((error) => {
115
+ loggy.error(`${label} error:`);
116
+ loggy.error(error);
117
+ });
118
+ };
103
119
  const bootstrap = ({ strapi: strapi2 }) => {
104
120
  loggy.info("Initializing");
105
121
  try {
@@ -115,27 +131,36 @@ const bootstrap = ({ strapi: strapi2 }) => {
115
131
  cacheStore.init();
116
132
  if (autoPurgeCache) {
117
133
  strapi2.db.lifecycles.subscribe({
118
- async afterCreate(event) {
119
- await invalidateCache(event, cacheStore, strapi2);
134
+ afterCreate(event) {
135
+ runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
120
136
  },
121
- async afterUpdate(event) {
122
- await invalidateCache(event, cacheStore, strapi2);
137
+ afterUpdate(event) {
138
+ runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
123
139
  },
124
- async afterDelete(event) {
125
- await invalidateCache(event, cacheStore, strapi2);
140
+ afterDelete(event) {
141
+ runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
126
142
  }
127
143
  });
128
144
  }
129
145
  if (autoPurgeGraphQL) {
130
146
  strapi2.db.lifecycles.subscribe({
131
- async afterCreate(event) {
132
- await invalidateGraphqlCache(event, cacheStore, strapi2);
147
+ afterCreate(event) {
148
+ runInBackground(
149
+ "GraphQL cache invalidation",
150
+ () => invalidateGraphqlCache(event, cacheStore, strapi2)
151
+ );
133
152
  },
134
- async afterUpdate(event) {
135
- await invalidateGraphqlCache(event, cacheStore, strapi2);
153
+ afterUpdate(event) {
154
+ runInBackground(
155
+ "GraphQL cache invalidation",
156
+ () => invalidateGraphqlCache(event, cacheStore, strapi2)
157
+ );
136
158
  },
137
- async afterDelete(event) {
138
- await invalidateGraphqlCache(event, cacheStore, strapi2);
159
+ afterDelete(event) {
160
+ runInBackground(
161
+ "GraphQL cache invalidation",
162
+ () => invalidateGraphqlCache(event, cacheStore, strapi2)
163
+ );
139
164
  }
140
165
  });
141
166
  }
@@ -939,7 +964,7 @@ class RedisCacheProvider {
939
964
  async del(key) {
940
965
  if (!this.ready) return null;
941
966
  try {
942
- const relativeKey = key.slice(this.keyPrefix.length);
967
+ const relativeKey = this.keyPrefix && key.startsWith(this.keyPrefix) ? key.slice(this.keyPrefix.length) : key;
943
968
  loggy.info(`Redis PURGING KEY: ${relativeKey}`);
944
969
  await this.client.del(relativeKey);
945
970
  return true;
@@ -978,18 +1003,32 @@ class RedisCacheProvider {
978
1003
  }
979
1004
  }
980
1005
  async deleteBatch(client, batch, regExps) {
981
- const toDelete = batch.filter((key) => regExps.some((re) => re.test(key)));
1006
+ const toDelete = batch.filter((key) => {
1007
+ if (!this.hasConfiguredPrefix(key)) {
1008
+ return false;
1009
+ }
1010
+ const relativeKey = this.toRelativeKey(key);
1011
+ return regExps.some((re) => re.test(key) || re.test(relativeKey));
1012
+ });
982
1013
  if (toDelete.length === 0) return;
983
1014
  const pipeline = client.pipeline();
984
1015
  for (const key of toDelete) {
985
- const relativeKey = key.startsWith(this.keyPrefix) ? key.slice(this.keyPrefix.length) : key;
986
- pipeline.del(relativeKey);
1016
+ pipeline.del(this.toRelativeKey(key));
987
1017
  }
988
1018
  await pipeline.exec();
989
1019
  }
1020
+ hasConfiguredPrefix(key) {
1021
+ return !this.keyPrefix || key.startsWith(this.keyPrefix);
1022
+ }
1023
+ toRelativeKey(key) {
1024
+ return this.keyPrefix && key.startsWith(this.keyPrefix) ? key.slice(this.keyPrefix.length) : key;
1025
+ }
990
1026
  scanAndDelete(client, regExps) {
991
1027
  return new Promise((resolve, reject) => {
992
- const stream = client.scanStream({ match: `${this.keyPrefix}*`, count: this.redisScanDeleteCount });
1028
+ const stream = client.scanStream({
1029
+ match: `${this.keyPrefix}*`,
1030
+ count: this.redisScanDeleteCount
1031
+ });
993
1032
  stream.on("data", (batch) => {
994
1033
  stream.pause();
995
1034
  this.deleteBatch(client, batch, regExps).then(() => stream.resume()).catch(reject);
@@ -16,6 +16,8 @@ export declare class RedisCacheProvider implements CacheProvider {
16
16
  keys(): Promise<string[] | null>;
17
17
  reset(): Promise<any | null>;
18
18
  private deleteBatch;
19
+ private hasConfiguredPrefix;
20
+ private toRelativeKey;
19
21
  private scanAndDelete;
20
22
  /**
21
23
  * ScanStream keys and batch delete for Redis,
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.11.0",
2
+ "version": "1.11.2",
3
3
  "keywords": [
4
4
  "strapi cache",
5
5
  "strapi rest cache",
@@ -39,8 +39,8 @@
39
39
  "test:all": "npm test -- --run && npm run test:integration"
40
40
  },
41
41
  "dependencies": {
42
- "@strapi/design-system": "^2.0.1",
43
- "@strapi/icons": "^2.0.1",
42
+ "@strapi/design-system": "2.2.1",
43
+ "@strapi/icons": "2.2.1",
44
44
  "ioredis": "^5.6.1",
45
45
  "lru-cache": "^11.1.0",
46
46
  "raw-body": "^3.0.0",
@@ -52,8 +52,8 @@
52
52
  "@strapi/typescript-utils": "^5.0.0",
53
53
  "@types/jest": "^30.0.0",
54
54
  "@types/koa": "^2.15.0",
55
- "@types/react": "^19.1.0",
56
- "@types/react-dom": "^19.1.1",
55
+ "@types/react": "^18.3.0",
56
+ "@types/react-dom": "^18.3.0",
57
57
  "@types/supertest": "^6.0.3",
58
58
  "better-sqlite3": "^12.6.2",
59
59
  "jest": "^30.2.0",
@@ -69,7 +69,6 @@
69
69
  "vitest": "^3.1.1"
70
70
  },
71
71
  "peerDependencies": {
72
- "@strapi/sdk-plugin": "^5.3.2",
73
72
  "@strapi/strapi": "^5.0.0",
74
73
  "react": "^18.3.1",
75
74
  "react-dom": "^18.3.1",
@@ -81,6 +80,18 @@
81
80
  "@rollup/rollup-linux-x64-gnu": "*",
82
81
  "@swc/core-linux-x64-gnu": "*"
83
82
  },
83
+ "overrides": {
84
+ "@ai-sdk/gateway": "3.0.131",
85
+ "@ai-sdk/provider-utils": "4.0.29",
86
+ "@ai-sdk/react": "3.0.207",
87
+ "@vitejs/plugin-react-swc": "3.11.0",
88
+ "ai": "6.0.205",
89
+ "ajv": "8.20.0",
90
+ "esbuild": "0.28.1",
91
+ "esbuild-loader": "4.5.0",
92
+ "lodash": "4.18.1",
93
+ "vite": "6.4.3"
94
+ },
84
95
  "strapi": {
85
96
  "kind": "plugin",
86
97
  "name": "strapi-cache",