strapi-cache 1.10.0 → 1.11.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 +28 -28
- package/dist/_chunks/{index-Bdo3ZcJs.js → index-BI_IgcrO.js} +1 -1
- package/dist/_chunks/{index-BovuTRdq.mjs → index-CYg363QI.mjs} +34 -3
- package/dist/_chunks/{index-EMYe2zF8.js → index-CvGwTcIJ.js} +34 -3
- package/dist/_chunks/{index-DLoQ9I8J.mjs → index-Dlr_QM4N.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/hooks/useCacheConfig.d.ts +2 -1
- package/dist/admin/src/utils/adminButtons.d.ts +12 -0
- package/dist/server/index.js +30 -12
- package/dist/server/index.mjs +30 -12
- package/dist/server/src/utils/key.d.ts +2 -0
- package/package.json +1 -1
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')
|
|
@@ -88,7 +88,7 @@ Full configuration example:
|
|
|
88
88
|
autoPurgeGraphQL: true, // Automatically purge GraphQL cache on content CRUD operations
|
|
89
89
|
autoPurgeCacheOnStart: true, // Automatically purge cache on Strapi startup
|
|
90
90
|
disableAdminPopups: false, // Disable popups in the admin panel
|
|
91
|
-
disableAdminButtons: false, // Disable the purge cache buttons in the admin panel
|
|
91
|
+
disableAdminButtons: false, // Disable the purge cache buttons in the admin panel. Use true for all content types or an array like ['/api/products'] for specific content types
|
|
92
92
|
},
|
|
93
93
|
},
|
|
94
94
|
```
|
|
@@ -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
|
|
101
|
-
| ------------------------- |
|
|
102
|
-
| `debug` | Log cache decisions and operations to the server console
|
|
103
|
-
| `provider` | Where entries are stored
|
|
104
|
-
| `redisConfig` | Redis/Valkey connection: URL string or client options passed to ioredis/iovalkey
|
|
105
|
-
| `redisClusterNodes` | Seed nodes for Redis cluster mode; non-empty list switches to a cluster client
|
|
106
|
-
| `redisClusterOptions` | Options for the cluster client (e.g. `scaleReads`); `redisOptions` often come from `redisConfig`
|
|
107
|
-
| `redisScanDeleteCount` | `COUNT` hint for `SCAN` when purging keys (Redis/Valkey)
|
|
108
|
-
| `max` | Maximum number of entries (in-memory provider only)
|
|
109
|
-
| `ttl` | Time-to-live for each entry, in milliseconds
|
|
110
|
-
| `size` | Approximate max total size in bytes (in-memory provider only)
|
|
111
|
-
| `allowStale` | Whether stale entries may be returned (in-memory provider only)
|
|
112
|
-
| `cacheableRoutes` | Only URLs starting with one of these paths are cached; if empty, every URL under the REST API prefix matches
|
|
113
|
-
| `keyGenerator` | Custom function to build REST cache keys; receives Koa `ctx`; when omitted, default key is `${method}:${url}`
|
|
114
|
-
| `cacheableEntities` | If non-empty, only these API “entity” segments are cached; **when set, this drives eligibility instead of** `cacheableRoutes`
|
|
115
|
-
| `excludeRoutes` | URLs starting with any of these prefixes are **never** cached; evaluated before `cacheableRoutes` / entities
|
|
116
|
-
| `cacheHeaders` | Store and replay response headers with the body
|
|
117
|
-
| `cacheHeadersDenyList` | Header names (lowercase) to strip when `cacheHeaders` is `true`
|
|
118
|
-
| `cacheHeadersAllowList` | If non-empty, only these header names (lowercase) are stored; if empty, all headers are stored (subject to deny list)
|
|
119
|
-
| `cacheAuthorizedRequests` | Whether to cache requests that include an `Authorization` header
|
|
120
|
-
| `cacheGetTimeoutInMs` | Max time to wait for a cache read before treating it as a miss
|
|
121
|
-
| `autoPurgeCache` | Invalidate relevant REST cache entries after content create/update/delete
|
|
122
|
-
| `autoPurgeGraphQL` | Invalidate GraphQL cache after content create/update/delete
|
|
123
|
-
| `autoPurgeCacheOnStart` | Clear the cache when Strapi starts
|
|
124
|
-
| `disableAdminPopups` | Turn off admin UI notifications for cache actions
|
|
125
|
-
| `disableAdminButtons` | Hide manual purge controls in the admin (list and edit views)
|
|
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), either globally or for specific REST content type paths | `true`, `false`, or an array of paths such as `['/api/products', '/api/tags']` (default: `false`) |
|
|
126
126
|
|
|
127
127
|
## 🔍 Routes
|
|
128
128
|
|
|
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
4
4
|
const designSystem = require("@strapi/design-system");
|
|
5
5
|
const reactIntl = require("react-intl");
|
|
6
6
|
const admin = require("@strapi/strapi/admin");
|
|
7
|
-
const index = require("./index-
|
|
7
|
+
const index = require("./index-CvGwTcIJ.js");
|
|
8
8
|
const react = require("react");
|
|
9
9
|
const SettingsPage = () => {
|
|
10
10
|
const { formatMessage } = reactIntl.useIntl();
|
|
@@ -296,12 +296,42 @@ function PurgeModal({
|
|
|
296
296
|
] })
|
|
297
297
|
] });
|
|
298
298
|
}
|
|
299
|
+
const stripTrailingSlashes = (path) => {
|
|
300
|
+
let end = path.length;
|
|
301
|
+
while (end > 1 && path[end - 1] === "/") {
|
|
302
|
+
end--;
|
|
303
|
+
}
|
|
304
|
+
return path.slice(0, end);
|
|
305
|
+
};
|
|
306
|
+
const normalizeApiPath = (path) => {
|
|
307
|
+
const trimmedPath = path.trim();
|
|
308
|
+
const pathWithLeadingSlash = trimmedPath.startsWith("/") ? trimmedPath : `/${trimmedPath}`;
|
|
309
|
+
return stripTrailingSlashes(pathWithLeadingSlash);
|
|
310
|
+
};
|
|
311
|
+
const getContentTypeApiPath = (contentType, isSingleType = false) => {
|
|
312
|
+
const useSingleName = isSingleType || contentType?.kind === "singleType";
|
|
313
|
+
const apiPath = useSingleName ? contentType?.info?.singularName ?? contentType?.apiID : contentType?.info?.pluralName ?? contentType?.apiID;
|
|
314
|
+
return apiPath ? normalizeApiPath(`/api/${apiPath}`) : void 0;
|
|
315
|
+
};
|
|
316
|
+
const shouldDisableAdminButtons = (disableAdminButtons, contentTypeApiPath) => {
|
|
317
|
+
if (typeof disableAdminButtons === "boolean") {
|
|
318
|
+
return disableAdminButtons;
|
|
319
|
+
}
|
|
320
|
+
if (!Array.isArray(disableAdminButtons) || !contentTypeApiPath) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
const normalizedContentTypeApiPath = normalizeApiPath(contentTypeApiPath);
|
|
324
|
+
return disableAdminButtons.some(
|
|
325
|
+
(apiPath) => normalizeApiPath(apiPath) === normalizedContentTypeApiPath
|
|
326
|
+
);
|
|
327
|
+
};
|
|
299
328
|
function PurgeCacheButton() {
|
|
300
329
|
const { contentType } = unstable_useContentManagerContext();
|
|
301
330
|
const { formatMessage } = useIntl();
|
|
302
331
|
const { config } = useCacheConfig();
|
|
303
332
|
const keyToUse = contentType?.info.pluralName;
|
|
304
|
-
|
|
333
|
+
const contentTypeApiPath = getContentTypeApiPath(contentType);
|
|
334
|
+
if (shouldDisableAdminButtons(config?.disableAdminButtons, contentTypeApiPath)) {
|
|
305
335
|
return null;
|
|
306
336
|
}
|
|
307
337
|
return /* @__PURE__ */ jsx(
|
|
@@ -320,10 +350,11 @@ function PurgeEntityButton() {
|
|
|
320
350
|
const { id, isSingleType, contentType } = unstable_useContentManagerContext();
|
|
321
351
|
const { config } = useCacheConfig();
|
|
322
352
|
const apiPath = isSingleType ? contentType?.info.singularName : id;
|
|
353
|
+
const contentTypeApiPath = getContentTypeApiPath(contentType, isSingleType);
|
|
323
354
|
if (!apiPath) {
|
|
324
355
|
return null;
|
|
325
356
|
}
|
|
326
|
-
if (config?.disableAdminButtons) {
|
|
357
|
+
if (shouldDisableAdminButtons(config?.disableAdminButtons, contentTypeApiPath)) {
|
|
327
358
|
return null;
|
|
328
359
|
}
|
|
329
360
|
const keyToUse = encodeURIComponent(apiPath);
|
|
@@ -373,7 +404,7 @@ const index = {
|
|
|
373
404
|
},
|
|
374
405
|
id: "settings",
|
|
375
406
|
to: `${PLUGIN_ID}/settings`,
|
|
376
|
-
Component: () => import("./index-
|
|
407
|
+
Component: () => import("./index-Dlr_QM4N.mjs"),
|
|
377
408
|
permissions: pluginPermissions.viewSettings
|
|
378
409
|
}
|
|
379
410
|
]
|
|
@@ -297,12 +297,42 @@ function PurgeModal({
|
|
|
297
297
|
] })
|
|
298
298
|
] });
|
|
299
299
|
}
|
|
300
|
+
const stripTrailingSlashes = (path) => {
|
|
301
|
+
let end = path.length;
|
|
302
|
+
while (end > 1 && path[end - 1] === "/") {
|
|
303
|
+
end--;
|
|
304
|
+
}
|
|
305
|
+
return path.slice(0, end);
|
|
306
|
+
};
|
|
307
|
+
const normalizeApiPath = (path) => {
|
|
308
|
+
const trimmedPath = path.trim();
|
|
309
|
+
const pathWithLeadingSlash = trimmedPath.startsWith("/") ? trimmedPath : `/${trimmedPath}`;
|
|
310
|
+
return stripTrailingSlashes(pathWithLeadingSlash);
|
|
311
|
+
};
|
|
312
|
+
const getContentTypeApiPath = (contentType, isSingleType = false) => {
|
|
313
|
+
const useSingleName = isSingleType || contentType?.kind === "singleType";
|
|
314
|
+
const apiPath = useSingleName ? contentType?.info?.singularName ?? contentType?.apiID : contentType?.info?.pluralName ?? contentType?.apiID;
|
|
315
|
+
return apiPath ? normalizeApiPath(`/api/${apiPath}`) : void 0;
|
|
316
|
+
};
|
|
317
|
+
const shouldDisableAdminButtons = (disableAdminButtons, contentTypeApiPath) => {
|
|
318
|
+
if (typeof disableAdminButtons === "boolean") {
|
|
319
|
+
return disableAdminButtons;
|
|
320
|
+
}
|
|
321
|
+
if (!Array.isArray(disableAdminButtons) || !contentTypeApiPath) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
const normalizedContentTypeApiPath = normalizeApiPath(contentTypeApiPath);
|
|
325
|
+
return disableAdminButtons.some(
|
|
326
|
+
(apiPath) => normalizeApiPath(apiPath) === normalizedContentTypeApiPath
|
|
327
|
+
);
|
|
328
|
+
};
|
|
300
329
|
function PurgeCacheButton() {
|
|
301
330
|
const { contentType } = admin.unstable_useContentManagerContext();
|
|
302
331
|
const { formatMessage } = reactIntl.useIntl();
|
|
303
332
|
const { config } = useCacheConfig();
|
|
304
333
|
const keyToUse = contentType?.info.pluralName;
|
|
305
|
-
|
|
334
|
+
const contentTypeApiPath = getContentTypeApiPath(contentType);
|
|
335
|
+
if (shouldDisableAdminButtons(config?.disableAdminButtons, contentTypeApiPath)) {
|
|
306
336
|
return null;
|
|
307
337
|
}
|
|
308
338
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -321,10 +351,11 @@ function PurgeEntityButton() {
|
|
|
321
351
|
const { id, isSingleType, contentType } = admin.unstable_useContentManagerContext();
|
|
322
352
|
const { config } = useCacheConfig();
|
|
323
353
|
const apiPath = isSingleType ? contentType?.info.singularName : id;
|
|
354
|
+
const contentTypeApiPath = getContentTypeApiPath(contentType, isSingleType);
|
|
324
355
|
if (!apiPath) {
|
|
325
356
|
return null;
|
|
326
357
|
}
|
|
327
|
-
if (config?.disableAdminButtons) {
|
|
358
|
+
if (shouldDisableAdminButtons(config?.disableAdminButtons, contentTypeApiPath)) {
|
|
328
359
|
return null;
|
|
329
360
|
}
|
|
330
361
|
const keyToUse = encodeURIComponent(apiPath);
|
|
@@ -374,7 +405,7 @@ const index = {
|
|
|
374
405
|
},
|
|
375
406
|
id: "settings",
|
|
376
407
|
to: `${PLUGIN_ID}/settings`,
|
|
377
|
-
Component: () => Promise.resolve().then(() => require("./index-
|
|
408
|
+
Component: () => Promise.resolve().then(() => require("./index-BI_IgcrO.js")),
|
|
378
409
|
permissions: pluginPermissions.viewSettings
|
|
379
410
|
}
|
|
380
411
|
]
|
|
@@ -2,7 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Typography, TextInput } from "@strapi/design-system";
|
|
3
3
|
import { useIntl } from "react-intl";
|
|
4
4
|
import { Page } from "@strapi/strapi/admin";
|
|
5
|
-
import { p as pluginPermissions, P as PurgeModal } from "./index-
|
|
5
|
+
import { p as pluginPermissions, P as PurgeModal } from "./index-CYg363QI.mjs";
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
const SettingsPage = () => {
|
|
8
8
|
const { formatMessage } = useIntl();
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { DisableAdminButtonsConfig } from '../utils/adminButtons';
|
|
1
2
|
export type CacheConfig = {
|
|
2
3
|
cacheableRoutes: string[];
|
|
3
4
|
disableAdminPopups: boolean;
|
|
4
|
-
disableAdminButtons:
|
|
5
|
+
disableAdminButtons: DisableAdminButtonsConfig;
|
|
5
6
|
};
|
|
6
7
|
export declare const useCacheConfig: (enabled?: boolean) => {
|
|
7
8
|
config: CacheConfig | undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type DisableAdminButtonsConfig = boolean | string[];
|
|
2
|
+
type AdminContentType = {
|
|
3
|
+
apiID?: string;
|
|
4
|
+
kind?: string;
|
|
5
|
+
info?: {
|
|
6
|
+
pluralName?: string;
|
|
7
|
+
singularName?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare const getContentTypeApiPath: (contentType?: AdminContentType, isSingleType?: boolean) => string | undefined;
|
|
11
|
+
export declare const shouldDisableAdminButtons: (disableAdminButtons: DisableAdminButtonsConfig | undefined, contentTypeApiPath?: string) => boolean;
|
|
12
|
+
export {};
|
package/dist/server/index.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
78
|
+
const graphqlRegex2 = getGraphqlAllKeysRegex(strapi2);
|
|
76
79
|
await cacheStore.clearByRegexp([graphqlRegex2]);
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
79
|
-
const escapedNames = fieldNames.map((name) => name
|
|
80
|
-
const graphqlRegex = new RegExp(
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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,
|
|
@@ -635,8 +653,8 @@ const config = {
|
|
|
635
653
|
if (typeof config2.disableAdminPopups !== "boolean") {
|
|
636
654
|
throw new Error(`Invalid config: disableAdminPopups must be a boolean`);
|
|
637
655
|
}
|
|
638
|
-
if (typeof config2.disableAdminButtons !== "boolean") {
|
|
639
|
-
throw new Error(`Invalid config: disableAdminButtons must be a boolean`);
|
|
656
|
+
if (typeof config2.disableAdminButtons !== "boolean" && (!Array.isArray(config2.disableAdminButtons) || config2.disableAdminButtons.some((item) => typeof item !== "string"))) {
|
|
657
|
+
throw new Error(`Invalid config: disableAdminButtons must be a boolean or string array`);
|
|
640
658
|
}
|
|
641
659
|
if (typeof config2.redisScanDeleteCount !== "number") {
|
|
642
660
|
throw new Error(`Invalid config: redisScanDeleteCount must be a number`);
|
package/dist/server/index.mjs
CHANGED
|
@@ -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 =
|
|
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 =
|
|
74
|
+
const graphqlRegex2 = getGraphqlAllKeysRegex(strapi2);
|
|
72
75
|
await cacheStore.clearByRegexp([graphqlRegex2]);
|
|
73
76
|
return;
|
|
74
77
|
}
|
|
75
|
-
const escapedNames = fieldNames.map((name) => name
|
|
76
|
-
const graphqlRegex = new RegExp(
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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,
|
|
@@ -631,8 +649,8 @@ const config = {
|
|
|
631
649
|
if (typeof config2.disableAdminPopups !== "boolean") {
|
|
632
650
|
throw new Error(`Invalid config: disableAdminPopups must be a boolean`);
|
|
633
651
|
}
|
|
634
|
-
if (typeof config2.disableAdminButtons !== "boolean") {
|
|
635
|
-
throw new Error(`Invalid config: disableAdminButtons must be a boolean`);
|
|
652
|
+
if (typeof config2.disableAdminButtons !== "boolean" && (!Array.isArray(config2.disableAdminButtons) || config2.disableAdminButtons.some((item) => typeof item !== "string"))) {
|
|
653
|
+
throw new Error(`Invalid config: disableAdminButtons must be a boolean or string array`);
|
|
636
654
|
}
|
|
637
655
|
if (typeof config2.redisScanDeleteCount !== "number") {
|
|
638
656
|
throw new Error(`Invalid config: redisScanDeleteCount must be a number`);
|
|
@@ -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