⚠️ This PR introduces **breaking changes** if you use `withKoaContext` or `withS…trapiMiddleware` options
_no breaking changes are present in middleware options_
_strapi > 3.4.0 required_
## New features
### Associate custom `routes` to a model cache configuration
```javascript
// config/middleware.js
module.exports = ({ env }) => ({
// ...
settings: {
// ...
cache: {
enabled: true,
models: [
"review", // same as { model: "review" },
{
model: "restaurant",
routes: [
"/restaurants/:id/orders", // same as { path: "/restaurants/:id/orders", method: "GET" },
{ path: "/restaurants/orders", method: "POST" },
{ path: "/restaurants/:id/orders", method: "PUT" },
{ path: "/restaurants/:id/orders", method: "PATCH" },
{ path: "/restaurants/:id/orders", method: "DELETE" },
],
},
},
},
},
})
```
- `GET` requests are cached
- `POST`, `PUT`, `PATCH` and `DELETE` requests are used to purge related cache
By default `routes` are populated with the public api routes.
To disable this behaviour add `injectDefaultRoutes: false` to the model cache configuration
Cache is not looked up if `Authorization` or `Cookie` header are present.
To dissable this behaviour add `hitpass: false` to the model cache configuration
### Associate a model with a `plugin`
```javascript
// config/middleware.js
module.exports = ({ env }) => ({
// ...
settings: {
// ...
cache: {
enabled: true,
models: [
{
model: "my-plugin-model",
plugin: "plugin-test",
routes: [
{ path: "/plugin-test/controller/:id", method: "GET" },
{ path: "/plugin-test/controller/:slug", method: "GET" },
{ path: "/plugin-test/controller", method: "POST" },
{ path: "/plugin-test/controller/:id", method: "PUT" },
{ path: "/plugin-test/controller/:id", method: "DELETE" },
{ path: "/plugin-test/controller/:slug", method: "DELETE" },
],
},
},
},
},
})
```
### Internal refactor
- Improved configuration resolving
- Remove lodash usage as es6 is better in memory footprint and execution time
- Inspired by [Varnish](https://varnish-cache.org/docs/6.5/reference/states.html#client-side)
- Enable Typescript type checking (with JSDoc) for better maintainability and developer experience:
```javascript
// config/middleware.js
/**
* @type {import('strapi-middleware-cache').UserMiddlewareCacheConfig}
*/
const cache = {
enabled: true,
models: [
'footer',
'universal',
]
}
module.exports = ({env}) => ({
settings: {
cache,
}
})
```
### Options
Middleware default options
```javascript
/**
* @typedef {Object} UserMiddlewareCacheConfig
* @property {'mem'|'redis'=} type
* @property {boolean=} enabled
* @property {boolean=} logs
* @property {boolean=} populateContext
* @property {boolean=} populateStrapiMiddleware
* @property {boolean=} enableEtagSupport
* @property {boolean=} enableXCacheHeaders
* @property {boolean=} clearRelatedCache
* @property {boolean=} withKoaContext
* @property {boolean=} withStrapiMiddleware
* @property {string[]=} headers
* @property {number=} max
* @property {number=} maxAge
* @property {number=} cacheTimeout
* @property {(UserModelCacheConfig | string)[]=} models
* @property {Object=} redisConfig
*/
const defaultOptions = {
type: "mem",
enabled: false,
logs: true,
populateContext: false,
populateStrapiMiddleware: false,
enableEtagSupport: false,
enableXCacheHeaders: false,
clearRelatedCache: false,
withKoaContext: false,
withStrapiMiddleware: false,
headers: [],
max: 500,
maxAge: 3600000,
cacheTimeout: 500,
models: [],
redisConfig: undefined,
};
```
Model default options
```javascript
/**
* @typedef {Object} UserModelCacheConfig
* @property {string} model
* @property {string=} plugin
* @property {boolean=} singleType
* @property {Hitpass|boolean=} hitpass
* @property {boolean=} injectDefaultRoutes
* @property {string[]=} headers
* @property {number=} maxAge
* @property {(string | CustomRoute)[]=} routes
*/
const modelOptions = {
model: '<required>',
plugin: undefined,
singleType: false,
hitpass: (ctx) => ctx.request.headers.authorization || ctx.request.headers.cookie,
injectDefaultRoutes: true,
headers: userOptions.headers ?? defaultOptions.headers,
maxAge: userOptions.maxAge ?? defaultOptions.maxAge,
routes: [],
};
```
### New API
```javascript
// options.withStrapiMiddleware = true
strapi.middleware.cache = {
/**
* @typedef {Object} CacheStore
* @property {function(string): any} get
* @property {function(string): any} peek
* @property {function(string, any, number?): boolean} set
* @property {function(string): void} del
* @property {function(): any[]} keys
* @property {function(): void} reset
*/
store,
/**
* @typedef {Object} MiddlewareCacheConfig
* @property {'mem'|'redis'} type
* @property {boolean} enabled
* @property {boolean} logs
* @property {boolean} populateContext
* @property {boolean} populateStrapiMiddleware
* @property {boolean} enableEtagSupport
* @property {boolean} enableXCacheHeaders
* @property {boolean} clearRelatedCache
* @property {boolean} withKoaContext
* @property {boolean} withStrapiMiddleware
* @property {number} max
* @property {number} maxAge
* @property {number} cacheTimeout
* @property {string[]} headers
* @property {ModelCacheConfig[]} models
* @property {Object=} redisConfig
*/
options,
/**
* Clear cache with uri parameters
*
* @param {ModelCacheConfig} cacheConf
* @param {{ [key: string]: string; }=} params
*/
clearCache,
/**
* Get related ModelCacheConfig
*
* @param {string} model
* @param {string=} plugin
* @returns {ModelCacheConfig=}
*/
getCacheConfig,
/**
* Get related ModelCacheConfig with an uid
*
* uid:
* - application::sport.sport
* - plugins::users-permissions.user
*
* @param {string} uid
* @returns {ModelCacheConfig=}
*/
getCacheConfigByUid,
/**
* Get models uid that is related to a ModelCacheConfig
*
* @param {ModelCacheConfig} cacheConf The model used to find related caches to purge
* @return {string[]} Array of related models uid
*/
getRelatedModelsUid,
/**
* Get regexs to match all ModelCacheConfig keys with given params
*
* @param {ModelCacheConfig} cacheConf
* @param {{ [key: string]: string; }=} params
* @returns {RegExp[]}
*/
getCacheConfRegExp,
/**
* Get regexs to match CustomRoute keys with given params
*
* @param {CustomRoute} route
* @param {{ [key: string]: string; }=} params
* @returns {RegExp[]}
*/
getRouteRegExp,
};
```
🚧 Request for comments
_Feel free to provide feedback_
### Todo
- [x] `clearCache` RegExp refactor
- [x] `getRelatedModels` refactor
- [x] improve misconfiguration warning / errors
- [ ] check `cache-control` response header ?
- [x] find a way for `hitpass` requests ? _(see [Varnish](https://varnish-cache.org/docs/6.5/reference/states.html#client-side) diagram)_
- [ ] tests