strapi-cache 1.8.8 → 1.10.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 +3 -1
- package/dist/server/index.js +21 -10
- package/dist/server/index.mjs +21 -10
- package/dist/server/src/config/index.d.ts +1 -0
- package/dist/server/src/index.d.ts +1 -0
- package/dist/server/src/utils/key.d.ts +4 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -72,6 +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
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
|
|
76
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`.
|
|
77
78
|
provider: 'memory', // Cache provider ('memory', 'redis' or 'valkey')
|
|
@@ -109,6 +110,7 @@ Possible configuration keys are listed below; omitted keys keep the plugin defau
|
|
|
109
110
|
| `size` | Approximate max total size in bytes (in-memory provider only) | Positive integer (default: `10485760`, i.e. 10 MB) |
|
|
110
111
|
| `allowStale` | Whether stale entries may be returned (in-memory provider only) | `true` or `false` (default: `false`) |
|
|
111
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) |
|
|
112
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` |
|
|
113
115
|
| `excludeRoutes` | URLs starting with any of these prefixes are **never** cached; evaluated before `cacheableRoutes` / entities | Array of path prefix strings (default: `[]`) |
|
|
114
116
|
| `cacheHeaders` | Store and replay response headers with the body | `true` or `false` (default: `true`) |
|
|
@@ -139,7 +141,7 @@ All of these routes are protected by the policies `admin::isAuthenticatedAdmin`
|
|
|
139
141
|
- **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.
|
|
140
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).
|
|
141
143
|
- **`no-cache` Header Support**: Respects the `no-cache` header, letting you skip the cache by setting `Cache-Control: no-cache` in your request.
|
|
142
|
-
- **Default Cached Requests**: By default, caches all GET requests to `/api` (or whatever prefix you defined) and POST requests to `/graphql
|
|
144
|
+
- **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.
|
|
143
145
|
|
|
144
146
|
## 🔮 Planned Features
|
|
145
147
|
|
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) {
|
|
@@ -152,15 +152,21 @@ const bootstrap = ({ strapi: strapi2 }) => {
|
|
|
152
152
|
loggy.info("Plugin initialized");
|
|
153
153
|
strapi2.admin.services.permission.actionProvider.registerMany(actions);
|
|
154
154
|
};
|
|
155
|
-
const generateCacheKey = (context) => {
|
|
155
|
+
const generateCacheKey = (context, keyGenerator) => {
|
|
156
|
+
if (typeof keyGenerator === "function") {
|
|
157
|
+
const customKey = keyGenerator(context);
|
|
158
|
+
if (typeof customKey === "string") {
|
|
159
|
+
return customKey;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
156
162
|
const { url } = context.request;
|
|
157
163
|
const { method } = context.request;
|
|
158
164
|
return `${method}:${url}`;
|
|
159
165
|
};
|
|
160
|
-
const generateGraphqlCacheKey = (payload, method = "POST", rootFields = []) => {
|
|
166
|
+
const generateGraphqlCacheKey = (payload, method = "POST", rootFields = [], strapi2) => {
|
|
161
167
|
const hash = crypto.createHash("sha256").update(payload).digest("base64url");
|
|
162
168
|
const rootFieldsSegment = rootFields.length > 0 ? [...rootFields].sort((a, b) => a.localeCompare(b)).join(",") : "_";
|
|
163
|
-
return `${method}
|
|
169
|
+
return `${method}:${strapi2?.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:${rootFieldsSegment}:${hash}`;
|
|
164
170
|
};
|
|
165
171
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
166
172
|
const generateEntityKey = (url, restApiPrefix) => {
|
|
@@ -238,10 +244,11 @@ const middleware$1 = async (ctx, next) => {
|
|
|
238
244
|
const cacheableEntities = strapi.plugin("strapi-cache").config("cacheableEntities");
|
|
239
245
|
const cacheableRoutes = strapi.plugin("strapi-cache").config("cacheableRoutes");
|
|
240
246
|
const excludeRoutes = strapi.plugin("strapi-cache").config("excludeRoutes");
|
|
247
|
+
const keyGenerator = strapi.plugin("strapi-cache").config("keyGenerator");
|
|
241
248
|
const { cacheHeaders, cacheHeadersDenyList, cacheHeadersAllowList, cacheAuthorizedRequests } = getCacheHeaderConfig();
|
|
242
249
|
const cacheStore = cacheService.getCacheInstance();
|
|
243
250
|
const { url } = ctx.request;
|
|
244
|
-
const key = generateCacheKey(ctx);
|
|
251
|
+
const key = generateCacheKey(ctx, keyGenerator);
|
|
245
252
|
const cacheEntry = await cacheStore.get(key);
|
|
246
253
|
const cacheControlHeader = ctx.request.headers["cache-control"];
|
|
247
254
|
const noCache = cacheControlHeader && cacheControlHeader.includes("no-cache");
|
|
@@ -390,7 +397,7 @@ function getRootFieldsFromQuery(query) {
|
|
|
390
397
|
}
|
|
391
398
|
const middleware = async (ctx, next) => {
|
|
392
399
|
const { url, method } = ctx.request;
|
|
393
|
-
if (!url.startsWith("/graphql")) {
|
|
400
|
+
if (!url.startsWith(strapi.plugin("graphql")?.config("endpoint", "/graphql"))) {
|
|
394
401
|
await next();
|
|
395
402
|
return;
|
|
396
403
|
}
|
|
@@ -424,7 +431,7 @@ const middleware = async (ctx, next) => {
|
|
|
424
431
|
}
|
|
425
432
|
const payload = parseGraphqlPayload(body, isGet);
|
|
426
433
|
const rootFields = getRootFieldsFromQuery(payload.query);
|
|
427
|
-
const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields);
|
|
434
|
+
const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields, strapi);
|
|
428
435
|
loggy.info(
|
|
429
436
|
`GraphQL request: ${JSON.stringify({
|
|
430
437
|
operationName: payload.operationName,
|
|
@@ -478,7 +485,7 @@ const middleware = async (ctx, next) => {
|
|
|
478
485
|
return;
|
|
479
486
|
}
|
|
480
487
|
await next();
|
|
481
|
-
const shouldCache = (ctx.method === "POST" || ctx.method === "GET") && ctx.status >= 200 && ctx.status < 300 && url.startsWith("/graphql");
|
|
488
|
+
const shouldCache = (ctx.method === "POST" || ctx.method === "GET") && ctx.status >= 200 && ctx.status < 300 && url.startsWith(strapi.plugin("graphql")?.config("endpoint", "/graphql"));
|
|
482
489
|
if (shouldCache) {
|
|
483
490
|
loggy.info(`MISS with key: ${key}`);
|
|
484
491
|
const headers = ctx.request.headers;
|
|
@@ -533,6 +540,7 @@ const config = {
|
|
|
533
540
|
size: 1024 * 1024 * 10,
|
|
534
541
|
allowStale: false,
|
|
535
542
|
cacheableRoutes: [],
|
|
543
|
+
keyGenerator: void 0,
|
|
536
544
|
provider: "memory",
|
|
537
545
|
excludeRoutes: [],
|
|
538
546
|
redisConfig: env("REDIS_URL"),
|
|
@@ -568,6 +576,9 @@ const config = {
|
|
|
568
576
|
if (!Array.isArray(config2.cacheableRoutes) || config2.cacheableRoutes.some((item) => typeof item !== "string")) {
|
|
569
577
|
throw new Error(`Invalid config: cacheableRoutes must be an string array`);
|
|
570
578
|
}
|
|
579
|
+
if (config2.keyGenerator !== void 0 && typeof config2.keyGenerator !== "function") {
|
|
580
|
+
throw new Error(`Invalid config: keyGenerator must be a function`);
|
|
581
|
+
}
|
|
571
582
|
if (config2.cacheableEntities !== void 0 && (!Array.isArray(config2.cacheableEntities) || config2.cacheableEntities.some((item) => typeof item !== "string"))) {
|
|
572
583
|
throw new Error(`Invalid config: cacheableEntities must be a string array`);
|
|
573
584
|
}
|
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) {
|
|
@@ -148,15 +148,21 @@ const bootstrap = ({ strapi: strapi2 }) => {
|
|
|
148
148
|
loggy.info("Plugin initialized");
|
|
149
149
|
strapi2.admin.services.permission.actionProvider.registerMany(actions);
|
|
150
150
|
};
|
|
151
|
-
const generateCacheKey = (context) => {
|
|
151
|
+
const generateCacheKey = (context, keyGenerator) => {
|
|
152
|
+
if (typeof keyGenerator === "function") {
|
|
153
|
+
const customKey = keyGenerator(context);
|
|
154
|
+
if (typeof customKey === "string") {
|
|
155
|
+
return customKey;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
152
158
|
const { url } = context.request;
|
|
153
159
|
const { method } = context.request;
|
|
154
160
|
return `${method}:${url}`;
|
|
155
161
|
};
|
|
156
|
-
const generateGraphqlCacheKey = (payload, method = "POST", rootFields = []) => {
|
|
162
|
+
const generateGraphqlCacheKey = (payload, method = "POST", rootFields = [], strapi2) => {
|
|
157
163
|
const hash = createHash("sha256").update(payload).digest("base64url");
|
|
158
164
|
const rootFieldsSegment = rootFields.length > 0 ? [...rootFields].sort((a, b) => a.localeCompare(b)).join(",") : "_";
|
|
159
|
-
return `${method}
|
|
165
|
+
return `${method}:${strapi2?.plugin("graphql")?.config("endpoint", "/graphql") ?? "/graphql"}:${rootFieldsSegment}:${hash}`;
|
|
160
166
|
};
|
|
161
167
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
162
168
|
const generateEntityKey = (url, restApiPrefix) => {
|
|
@@ -234,10 +240,11 @@ const middleware$1 = async (ctx, next) => {
|
|
|
234
240
|
const cacheableEntities = strapi.plugin("strapi-cache").config("cacheableEntities");
|
|
235
241
|
const cacheableRoutes = strapi.plugin("strapi-cache").config("cacheableRoutes");
|
|
236
242
|
const excludeRoutes = strapi.plugin("strapi-cache").config("excludeRoutes");
|
|
243
|
+
const keyGenerator = strapi.plugin("strapi-cache").config("keyGenerator");
|
|
237
244
|
const { cacheHeaders, cacheHeadersDenyList, cacheHeadersAllowList, cacheAuthorizedRequests } = getCacheHeaderConfig();
|
|
238
245
|
const cacheStore = cacheService.getCacheInstance();
|
|
239
246
|
const { url } = ctx.request;
|
|
240
|
-
const key = generateCacheKey(ctx);
|
|
247
|
+
const key = generateCacheKey(ctx, keyGenerator);
|
|
241
248
|
const cacheEntry = await cacheStore.get(key);
|
|
242
249
|
const cacheControlHeader = ctx.request.headers["cache-control"];
|
|
243
250
|
const noCache = cacheControlHeader && cacheControlHeader.includes("no-cache");
|
|
@@ -386,7 +393,7 @@ function getRootFieldsFromQuery(query) {
|
|
|
386
393
|
}
|
|
387
394
|
const middleware = async (ctx, next) => {
|
|
388
395
|
const { url, method } = ctx.request;
|
|
389
|
-
if (!url.startsWith("/graphql")) {
|
|
396
|
+
if (!url.startsWith(strapi.plugin("graphql")?.config("endpoint", "/graphql"))) {
|
|
390
397
|
await next();
|
|
391
398
|
return;
|
|
392
399
|
}
|
|
@@ -420,7 +427,7 @@ const middleware = async (ctx, next) => {
|
|
|
420
427
|
}
|
|
421
428
|
const payload = parseGraphqlPayload(body, isGet);
|
|
422
429
|
const rootFields = getRootFieldsFromQuery(payload.query);
|
|
423
|
-
const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields);
|
|
430
|
+
const key = generateGraphqlCacheKey(body, isGet ? "GET" : "POST", rootFields, strapi);
|
|
424
431
|
loggy.info(
|
|
425
432
|
`GraphQL request: ${JSON.stringify({
|
|
426
433
|
operationName: payload.operationName,
|
|
@@ -474,7 +481,7 @@ const middleware = async (ctx, next) => {
|
|
|
474
481
|
return;
|
|
475
482
|
}
|
|
476
483
|
await next();
|
|
477
|
-
const shouldCache = (ctx.method === "POST" || ctx.method === "GET") && ctx.status >= 200 && ctx.status < 300 && url.startsWith("/graphql");
|
|
484
|
+
const shouldCache = (ctx.method === "POST" || ctx.method === "GET") && ctx.status >= 200 && ctx.status < 300 && url.startsWith(strapi.plugin("graphql")?.config("endpoint", "/graphql"));
|
|
478
485
|
if (shouldCache) {
|
|
479
486
|
loggy.info(`MISS with key: ${key}`);
|
|
480
487
|
const headers = ctx.request.headers;
|
|
@@ -529,6 +536,7 @@ const config = {
|
|
|
529
536
|
size: 1024 * 1024 * 10,
|
|
530
537
|
allowStale: false,
|
|
531
538
|
cacheableRoutes: [],
|
|
539
|
+
keyGenerator: void 0,
|
|
532
540
|
provider: "memory",
|
|
533
541
|
excludeRoutes: [],
|
|
534
542
|
redisConfig: env("REDIS_URL"),
|
|
@@ -564,6 +572,9 @@ const config = {
|
|
|
564
572
|
if (!Array.isArray(config2.cacheableRoutes) || config2.cacheableRoutes.some((item) => typeof item !== "string")) {
|
|
565
573
|
throw new Error(`Invalid config: cacheableRoutes must be an string array`);
|
|
566
574
|
}
|
|
575
|
+
if (config2.keyGenerator !== void 0 && typeof config2.keyGenerator !== "function") {
|
|
576
|
+
throw new Error(`Invalid config: keyGenerator must be a function`);
|
|
577
|
+
}
|
|
567
578
|
if (config2.cacheableEntities !== void 0 && (!Array.isArray(config2.cacheableEntities) || config2.cacheableEntities.some((item) => typeof item !== "string"))) {
|
|
568
579
|
throw new Error(`Invalid config: cacheableEntities must be a string array`);
|
|
569
580
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Core } from '@strapi/strapi';
|
|
1
2
|
import { Context } from 'koa';
|
|
2
|
-
export
|
|
3
|
-
export declare const
|
|
3
|
+
export type CacheKeyGenerator = (context: Context) => string;
|
|
4
|
+
export declare const generateCacheKey: (context: Context, keyGenerator?: CacheKeyGenerator) => string;
|
|
5
|
+
export declare const generateGraphqlCacheKey: (payload: string, method?: 'GET' | 'POST', rootFields?: string[], strapi?: Core.Strapi) => string;
|
|
4
6
|
export declare const escapeRegExp: (s: string) => string;
|
|
5
7
|
export declare const generateEntityKey: (url: string, restApiPrefix: string) => string;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.10.0",
|
|
3
3
|
"keywords": [
|
|
4
4
|
"strapi cache",
|
|
5
5
|
"strapi rest cache",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"test:ts:front": "run -T tsc -p admin/tsconfig.json",
|
|
36
36
|
"test:ts:back": "run -T tsc -p server/tsconfig.json",
|
|
37
37
|
"test": "vitest run --exclude 'test/integration/**'",
|
|
38
|
-
"test:integration": "npm run build && rm -rf playground/.cache && mkdir -p playground/.yalc/strapi-cache && cp -r dist package.json LICENSE README.md playground/.yalc/strapi-cache/ && cd playground && npm
|
|
38
|
+
"test:integration": "npm run build && rm -rf playground/.cache playground/node_modules playground/.yalc && mkdir -p playground/.yalc/strapi-cache && cp -r dist package.json LICENSE README.md playground/.yalc/strapi-cache/ && node -e \"const fs=require('fs');const p='playground/.yalc/strapi-cache/package.json';const pkg=JSON.parse(fs.readFileSync(p,'utf8'));delete pkg.devDependencies;delete pkg.optionalDependencies;delete pkg.scripts;fs.writeFileSync(p,JSON.stringify(pkg,null,2));\" && cd playground && npm ci --ignore-scripts && npm rebuild better-sqlite3 @swc/core sharp --foreground-scripts && npm run build && cd .. && NODE_ENV=test jest --config jest.integration.config.js --runInBand --forceExit --detectOpenHandles",
|
|
39
39
|
"test:all": "npm test -- --run && npm run test:integration"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|