strapi-cache 1.8.4 → 1.9.0
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 +57 -5
- package/dist/server/index.js +11 -8
- package/dist/server/index.mjs +11 -8
- package/dist/server/src/utils/key.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,9 +35,31 @@ or
|
|
|
35
35
|
yarn add strapi-cache
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
// config/plugins.{js,ts}
|
|
42
|
+
'strapi-cache': {
|
|
43
|
+
enabled: true,
|
|
44
|
+
},
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
To use **Redis** or **Valkey** instead of memory, set `provider` and `redisConfig` (required for those providers):
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// config/plugins.{js,ts}
|
|
51
|
+
'strapi-cache': {
|
|
52
|
+
enabled: true,
|
|
53
|
+
config: {
|
|
54
|
+
provider: 'redis', // or 'valkey'
|
|
55
|
+
redisConfig: env('REDIS_URL', 'redis://127.0.0.1:6379'),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
See [ioredis](https://github.com/redis/ioredis) (Redis) or [iovalkey](https://github.com/valkey-io/iovalkey) (Valkey) for advanced `redisConfig` shapes (URL string or client options object).
|
|
39
61
|
|
|
40
|
-
|
|
62
|
+
Full configuration example:
|
|
41
63
|
|
|
42
64
|
```javascript
|
|
43
65
|
// config/plugins.{js,ts}
|
|
@@ -51,7 +73,7 @@ In your Strapi project, navigate to `config/plugins.js` and add the following co
|
|
|
51
73
|
allowStale: false, // Allow stale cache items (only for memory cache)
|
|
52
74
|
cacheableRoutes: ['/api/products', '/api/categories'], // Caches routes which start with these paths (if empty array, all '/api' routes are cached)
|
|
53
75
|
// 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
|
|
54
|
-
excludeRoutes: ['/api/products/private'], //
|
|
76
|
+
excludeRoutes: ['/api/products/private'], // Exclude routes which start with these paths from being cached (takes precedence over cacheableRoutes). **Note:** `excludeRoutes` takes precedence over `cacheableRoutes`.
|
|
55
77
|
provider: 'memory', // Cache provider ('memory', 'redis' or 'valkey')
|
|
56
78
|
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)
|
|
57
79
|
redisClusterNodes: [], // If provided any cluster node (this list is not empty), initialize cluster client. Each object must have keys 'host' and 'port'
|
|
@@ -70,6 +92,36 @@ In your Strapi project, navigate to `config/plugins.js` and add the following co
|
|
|
70
92
|
},
|
|
71
93
|
```
|
|
72
94
|
|
|
95
|
+
## ⚙️ Configuration
|
|
96
|
+
|
|
97
|
+
Possible configuration keys are listed below; omitted keys keep the plugin defaults.
|
|
98
|
+
|
|
99
|
+
| Key | Description | Possible values |
|
|
100
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `debug` | Log cache decisions and operations to the server console | `true` or `false` (default: `false`) |
|
|
102
|
+
| `provider` | Where entries are stored | `'memory'`, `'redis'`, or `'valkey'` (default: `'memory'`) |
|
|
103
|
+
| `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 |
|
|
104
|
+
| `redisClusterNodes` | Seed nodes for Redis cluster mode; non-empty list switches to a cluster client | Array of `{ host: string, port: number }` (default: `[]`) |
|
|
105
|
+
| `redisClusterOptions` | Options for the cluster client (e.g. `scaleReads`); `redisOptions` often come from `redisConfig` | Object (default: `{}`) |
|
|
106
|
+
| `redisScanDeleteCount` | `COUNT` hint for `SCAN` when purging keys (Redis/Valkey) | Positive number (default: `100`) |
|
|
107
|
+
| `max` | Maximum number of entries (in-memory provider only) | Positive integer (default: `1000`) |
|
|
108
|
+
| `ttl` | Time-to-live for each entry, in milliseconds | Non-negative number (default: `3600000`, i.e. 1 hour) |
|
|
109
|
+
| `size` | Approximate max total size in bytes (in-memory provider only) | Positive integer (default: `10485760`, i.e. 10 MB) |
|
|
110
|
+
| `allowStale` | Whether stale entries may be returned (in-memory provider only) | `true` or `false` (default: `false`) |
|
|
111
|
+
| `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”) |
|
|
112
|
+
| `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` |
|
|
113
|
+
| `excludeRoutes` | URLs starting with any of these prefixes are **never** cached; evaluated before `cacheableRoutes` / entities | Array of path prefix strings (default: `[]`) |
|
|
114
|
+
| `cacheHeaders` | Store and replay response headers with the body | `true` or `false` (default: `true`) |
|
|
115
|
+
| `cacheHeadersDenyList` | Header names (lowercase) to strip when `cacheHeaders` is `true` | Array of strings (default: `[]`) |
|
|
116
|
+
| `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: `[]`) |
|
|
117
|
+
| `cacheAuthorizedRequests` | Whether to cache requests that include an `Authorization` header | `true` or `false` (default: `false`) |
|
|
118
|
+
| `cacheGetTimeoutInMs` | Max time to wait for a cache read before treating it as a miss | Milliseconds (default: `1000`) |
|
|
119
|
+
| `autoPurgeCache` | Invalidate relevant REST cache entries after content create/update/delete | `true` or `false` (default: `true`) |
|
|
120
|
+
| `autoPurgeGraphQL` | Invalidate GraphQL cache after content create/update/delete | `true` or `false` (default: `false` if omitted; set `true` to enable) |
|
|
121
|
+
| `autoPurgeCacheOnStart` | Clear the cache when Strapi starts | `true` or `false` (default: `true`) |
|
|
122
|
+
| `disableAdminPopups` | Turn off admin UI notifications for cache actions | `true` or `false` (default: `false`) |
|
|
123
|
+
| `disableAdminButtons` | Hide manual purge controls in the admin (list and edit views) | `true` or `false` (default: `false`) |
|
|
124
|
+
|
|
73
125
|
## 🔍 Routes
|
|
74
126
|
|
|
75
127
|
The plugin creates three new routes
|
|
@@ -85,9 +137,9 @@ All of these routes are protected by the policies `admin::isAuthenticatedAdmin`
|
|
|
85
137
|
|
|
86
138
|
- **Storage**: The plugin keeps cached data in memory, Redis or Valkey, depending on the configuration.
|
|
87
139
|
- **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.
|
|
88
|
-
- **Automatic Invalidation**:
|
|
140
|
+
- **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).
|
|
89
141
|
- **`no-cache` Header Support**: Respects the `no-cache` header, letting you skip the cache by setting `Cache-Control: no-cache` in your request.
|
|
90
|
-
- **Default Cached Requests**: By default, caches all GET requests to `/api` (or whatever prefix you defined) and POST requests to `/graphql
|
|
142
|
+
- **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.
|
|
91
143
|
|
|
92
144
|
## 🔮 Planned Features
|
|
93
145
|
|
package/dist/server/index.js
CHANGED
|
@@ -63,7 +63,7 @@ async function invalidateGraphqlCache(event, cacheStore, strapi2) {
|
|
|
63
63
|
const contentType = strapi2.contentType(model.uid);
|
|
64
64
|
if (!contentType || !contentType.info) {
|
|
65
65
|
loggy.info(`Content type ${model.uid} not found, purging all GraphQL cache`);
|
|
66
|
-
const graphqlRegex2 = new RegExp(`^(GET|POST)
|
|
66
|
+
const graphqlRegex2 = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:.*`);
|
|
67
67
|
await cacheStore.clearByRegexp([graphqlRegex2]);
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
@@ -72,12 +72,12 @@ async function invalidateGraphqlCache(event, cacheStore, strapi2) {
|
|
|
72
72
|
const fieldNames = [...new Set([singularName, pluralName].filter(Boolean))];
|
|
73
73
|
if (fieldNames.length === 0) {
|
|
74
74
|
loggy.info(`No field names for ${model.uid}, purging all GraphQL cache`);
|
|
75
|
-
const graphqlRegex2 = new RegExp(`^(GET|POST)
|
|
75
|
+
const graphqlRegex2 = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:.*`);
|
|
76
76
|
await cacheStore.clearByRegexp([graphqlRegex2]);
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
79
|
const escapedNames = fieldNames.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
80
|
-
const graphqlRegex = new RegExp(`^(GET|POST)
|
|
80
|
+
const graphqlRegex = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:[^:]*\\b(${escapedNames})\\b[^:]*:`);
|
|
81
81
|
await cacheStore.clearByRegexp([graphqlRegex]);
|
|
82
82
|
loggy.info(`Invalidated GraphQL cache for ${model.uid} (${fieldNames.join(", ")})`);
|
|
83
83
|
} catch (error) {
|
|
@@ -157,10 +157,10 @@ const generateCacheKey = (context) => {
|
|
|
157
157
|
const { method } = context.request;
|
|
158
158
|
return `${method}:${url}`;
|
|
159
159
|
};
|
|
160
|
-
const generateGraphqlCacheKey = (payload, method = "POST", rootFields = []) => {
|
|
160
|
+
const generateGraphqlCacheKey = (payload, method = "POST", rootFields = [], strapi2) => {
|
|
161
161
|
const hash = crypto.createHash("sha256").update(payload).digest("base64url");
|
|
162
162
|
const rootFieldsSegment = rootFields.length > 0 ? [...rootFields].sort((a, b) => a.localeCompare(b)).join(",") : "_";
|
|
163
|
-
return `${method}
|
|
163
|
+
return `${method}:${strapi2?.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:${rootFieldsSegment}:${hash}`;
|
|
164
164
|
};
|
|
165
165
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
166
166
|
const generateEntityKey = (url, restApiPrefix) => {
|
|
@@ -273,6 +273,9 @@ const middleware$1 = async (ctx, next) => {
|
|
|
273
273
|
if (cacheEntry && !noCache) {
|
|
274
274
|
loggy.info(`HIT with key: ${key}`);
|
|
275
275
|
ctx.status = 200;
|
|
276
|
+
if (cacheEntry?.body?.type === "Buffer" && Array.isArray(cacheEntry.body.data)) {
|
|
277
|
+
cacheEntry.body = Buffer.from(cacheEntry.body.data);
|
|
278
|
+
}
|
|
276
279
|
ctx.body = cacheEntry.body;
|
|
277
280
|
if (cacheHeaders) {
|
|
278
281
|
ctx.set(cacheEntry.headers);
|
|
@@ -387,7 +390,7 @@ function getRootFieldsFromQuery(query) {
|
|
|
387
390
|
}
|
|
388
391
|
const middleware = async (ctx, next) => {
|
|
389
392
|
const { url, method } = ctx.request;
|
|
390
|
-
if (!url.startsWith("/graphql")) {
|
|
393
|
+
if (!url.startsWith(strapi.plugin("graphql")?.config("endpoint", "/graphql"))) {
|
|
391
394
|
await next();
|
|
392
395
|
return;
|
|
393
396
|
}
|
|
@@ -421,7 +424,7 @@ const middleware = async (ctx, next) => {
|
|
|
421
424
|
}
|
|
422
425
|
const payload = parseGraphqlPayload(body, isGet);
|
|
423
426
|
const rootFields = getRootFieldsFromQuery(payload.query);
|
|
424
|
-
const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields);
|
|
427
|
+
const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields, strapi);
|
|
425
428
|
loggy.info(
|
|
426
429
|
`GraphQL request: ${JSON.stringify({
|
|
427
430
|
operationName: payload.operationName,
|
|
@@ -475,7 +478,7 @@ const middleware = async (ctx, next) => {
|
|
|
475
478
|
return;
|
|
476
479
|
}
|
|
477
480
|
await next();
|
|
478
|
-
const shouldCache = (ctx.method === "POST" || ctx.method === "GET") && ctx.status >= 200 && ctx.status < 300 && url.startsWith("/graphql");
|
|
481
|
+
const shouldCache = (ctx.method === "POST" || ctx.method === "GET") && ctx.status >= 200 && ctx.status < 300 && url.startsWith(strapi.plugin("graphql")?.config("endpoint", "/graphql"));
|
|
479
482
|
if (shouldCache) {
|
|
480
483
|
loggy.info(`MISS with key: ${key}`);
|
|
481
484
|
const headers = ctx.request.headers;
|
package/dist/server/index.mjs
CHANGED
|
@@ -59,7 +59,7 @@ async function invalidateGraphqlCache(event, cacheStore, strapi2) {
|
|
|
59
59
|
const contentType = strapi2.contentType(model.uid);
|
|
60
60
|
if (!contentType || !contentType.info) {
|
|
61
61
|
loggy.info(`Content type ${model.uid} not found, purging all GraphQL cache`);
|
|
62
|
-
const graphqlRegex2 = new RegExp(`^(GET|POST)
|
|
62
|
+
const graphqlRegex2 = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:.*`);
|
|
63
63
|
await cacheStore.clearByRegexp([graphqlRegex2]);
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
@@ -68,12 +68,12 @@ async function invalidateGraphqlCache(event, cacheStore, strapi2) {
|
|
|
68
68
|
const fieldNames = [...new Set([singularName, pluralName].filter(Boolean))];
|
|
69
69
|
if (fieldNames.length === 0) {
|
|
70
70
|
loggy.info(`No field names for ${model.uid}, purging all GraphQL cache`);
|
|
71
|
-
const graphqlRegex2 = new RegExp(`^(GET|POST)
|
|
71
|
+
const graphqlRegex2 = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:.*`);
|
|
72
72
|
await cacheStore.clearByRegexp([graphqlRegex2]);
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
const escapedNames = fieldNames.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
76
|
-
const graphqlRegex = new RegExp(`^(GET|POST)
|
|
76
|
+
const graphqlRegex = new RegExp(`^(GET|POST):${strapi2.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:[^:]*\\b(${escapedNames})\\b[^:]*:`);
|
|
77
77
|
await cacheStore.clearByRegexp([graphqlRegex]);
|
|
78
78
|
loggy.info(`Invalidated GraphQL cache for ${model.uid} (${fieldNames.join(", ")})`);
|
|
79
79
|
} catch (error) {
|
|
@@ -153,10 +153,10 @@ const generateCacheKey = (context) => {
|
|
|
153
153
|
const { method } = context.request;
|
|
154
154
|
return `${method}:${url}`;
|
|
155
155
|
};
|
|
156
|
-
const generateGraphqlCacheKey = (payload, method = "POST", rootFields = []) => {
|
|
156
|
+
const generateGraphqlCacheKey = (payload, method = "POST", rootFields = [], strapi2) => {
|
|
157
157
|
const hash = createHash("sha256").update(payload).digest("base64url");
|
|
158
158
|
const rootFieldsSegment = rootFields.length > 0 ? [...rootFields].sort((a, b) => a.localeCompare(b)).join(",") : "_";
|
|
159
|
-
return `${method}
|
|
159
|
+
return `${method}:${strapi2?.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:${rootFieldsSegment}:${hash}`;
|
|
160
160
|
};
|
|
161
161
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
162
162
|
const generateEntityKey = (url, restApiPrefix) => {
|
|
@@ -269,6 +269,9 @@ const middleware$1 = async (ctx, next) => {
|
|
|
269
269
|
if (cacheEntry && !noCache) {
|
|
270
270
|
loggy.info(`HIT with key: ${key}`);
|
|
271
271
|
ctx.status = 200;
|
|
272
|
+
if (cacheEntry?.body?.type === "Buffer" && Array.isArray(cacheEntry.body.data)) {
|
|
273
|
+
cacheEntry.body = Buffer.from(cacheEntry.body.data);
|
|
274
|
+
}
|
|
272
275
|
ctx.body = cacheEntry.body;
|
|
273
276
|
if (cacheHeaders) {
|
|
274
277
|
ctx.set(cacheEntry.headers);
|
|
@@ -383,7 +386,7 @@ function getRootFieldsFromQuery(query) {
|
|
|
383
386
|
}
|
|
384
387
|
const middleware = async (ctx, next) => {
|
|
385
388
|
const { url, method } = ctx.request;
|
|
386
|
-
if (!url.startsWith("/graphql")) {
|
|
389
|
+
if (!url.startsWith(strapi.plugin("graphql")?.config("endpoint", "/graphql"))) {
|
|
387
390
|
await next();
|
|
388
391
|
return;
|
|
389
392
|
}
|
|
@@ -417,7 +420,7 @@ const middleware = async (ctx, next) => {
|
|
|
417
420
|
}
|
|
418
421
|
const payload = parseGraphqlPayload(body, isGet);
|
|
419
422
|
const rootFields = getRootFieldsFromQuery(payload.query);
|
|
420
|
-
const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields);
|
|
423
|
+
const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields, strapi);
|
|
421
424
|
loggy.info(
|
|
422
425
|
`GraphQL request: ${JSON.stringify({
|
|
423
426
|
operationName: payload.operationName,
|
|
@@ -471,7 +474,7 @@ const middleware = async (ctx, next) => {
|
|
|
471
474
|
return;
|
|
472
475
|
}
|
|
473
476
|
await next();
|
|
474
|
-
const shouldCache = (ctx.method === "POST" || ctx.method === "GET") && ctx.status >= 200 && ctx.status < 300 && url.startsWith("/graphql");
|
|
477
|
+
const shouldCache = (ctx.method === "POST" || ctx.method === "GET") && ctx.status >= 200 && ctx.status < 300 && url.startsWith(strapi.plugin("graphql")?.config("endpoint", "/graphql"));
|
|
475
478
|
if (shouldCache) {
|
|
476
479
|
loggy.info(`MISS with key: ${key}`);
|
|
477
480
|
const headers = ctx.request.headers;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { Core } from '@strapi/strapi';
|
|
1
2
|
import { Context } from 'koa';
|
|
2
3
|
export declare const generateCacheKey: (context: Context) => string;
|
|
3
|
-
export declare const generateGraphqlCacheKey: (payload: string, method?: 'GET' | 'POST', rootFields?: string[]) => string;
|
|
4
|
+
export declare const generateGraphqlCacheKey: (payload: string, method?: 'GET' | 'POST', rootFields?: string[], strapi?: Core.Strapi) => string;
|
|
4
5
|
export declare const escapeRegExp: (s: string) => string;
|
|
5
6
|
export declare const generateEntityKey: (url: string, restApiPrefix: string) => string;
|
package/package.json
CHANGED