strapi-cache 1.11.1 → 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 +2 -1
- package/dist/admin/src/components/PluginIcon.d.ts +2 -1
- package/dist/admin/src/components/PurgeCacheButton/index.d.ts +2 -1
- package/dist/admin/src/components/PurgeEntityButton/index.d.ts +2 -1
- package/dist/admin/src/components/PurgeModal/index.d.ts +2 -1
- package/dist/admin/src/components/SettingsPage/index.d.ts +2 -1
- package/dist/server/index.js +43 -18
- package/dist/server/index.mjs +43 -18
- package/package.json +5 -5
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,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
|
|
10
|
+
declare function PurgeModal({ buttonText, keyToUse, buttonWidth, contentTypeName, isSettingsPage, isPurgeAll, }: PurgeProps): import("react").JSX.Element | null;
|
|
10
11
|
export default PurgeModal;
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
138
|
+
afterCreate(event) {
|
|
139
|
+
runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
|
|
124
140
|
},
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
afterUpdate(event) {
|
|
142
|
+
runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
|
|
127
143
|
},
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
136
|
-
|
|
151
|
+
afterCreate(event) {
|
|
152
|
+
runInBackground(
|
|
153
|
+
"GraphQL cache invalidation",
|
|
154
|
+
() => invalidateGraphqlCache(event, cacheStore, strapi2)
|
|
155
|
+
);
|
|
137
156
|
},
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
afterUpdate(event) {
|
|
158
|
+
runInBackground(
|
|
159
|
+
"GraphQL cache invalidation",
|
|
160
|
+
() => invalidateGraphqlCache(event, cacheStore, strapi2)
|
|
161
|
+
);
|
|
140
162
|
},
|
|
141
|
-
|
|
142
|
-
|
|
163
|
+
afterDelete(event) {
|
|
164
|
+
runInBackground(
|
|
165
|
+
"GraphQL cache invalidation",
|
|
166
|
+
() => invalidateGraphqlCache(event, cacheStore, strapi2)
|
|
167
|
+
);
|
|
143
168
|
}
|
|
144
169
|
});
|
|
145
170
|
}
|
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
134
|
+
afterCreate(event) {
|
|
135
|
+
runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
|
|
120
136
|
},
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
afterUpdate(event) {
|
|
138
|
+
runInBackground("Cache invalidation", () => invalidateCache(event, cacheStore, strapi2));
|
|
123
139
|
},
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
132
|
-
|
|
147
|
+
afterCreate(event) {
|
|
148
|
+
runInBackground(
|
|
149
|
+
"GraphQL cache invalidation",
|
|
150
|
+
() => invalidateGraphqlCache(event, cacheStore, strapi2)
|
|
151
|
+
);
|
|
133
152
|
},
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
afterUpdate(event) {
|
|
154
|
+
runInBackground(
|
|
155
|
+
"GraphQL cache invalidation",
|
|
156
|
+
() => invalidateGraphqlCache(event, cacheStore, strapi2)
|
|
157
|
+
);
|
|
136
158
|
},
|
|
137
|
-
|
|
138
|
-
|
|
159
|
+
afterDelete(event) {
|
|
160
|
+
runInBackground(
|
|
161
|
+
"GraphQL cache invalidation",
|
|
162
|
+
() => invalidateGraphqlCache(event, cacheStore, strapi2)
|
|
163
|
+
);
|
|
139
164
|
}
|
|
140
165
|
});
|
|
141
166
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.11.
|
|
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": "
|
|
43
|
-
"@strapi/icons": "
|
|
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": "^
|
|
56
|
-
"@types/react-dom": "^
|
|
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",
|