patrixr / strapi-middleware-cache

:electric_plug: A cache middleware for https://strapi.io
MIT License
290 stars 58 forks source link

Bust cache of specific endpoint when PUT/DELETE request with other endpoint #48

Open ViVa98 opened 3 years ago

ViVa98 commented 3 years ago

I have a model with 3 fields containing {id(auto-generated), username, email}

I have modified the default GET endpoint to "username" instead of "id" to search specific record based on username like GET /profiles/someusername

But for PUT/DELETE I'm using "id" as the endpoint PUT/DELETE /profiles/random_generated_id

Whenever I use PUT/DELETE, data changes in the database, the previously cached data of different endpoint is not busting. I'm thinking of extending those PUT/DELETE API and manually deleting the specific endpoint's cache.

Help me get out of this situation.

patrixr commented 3 years ago

Hi @ViVa98

The version of the middleware that you are using does not support anything like that, however @stafyniaksacha has been working on a v2 (you can check out the Beta branch, which contains a great amount of new features, including the ability to set custom routes, which might help you fix your issue !

You can try installing it by using npm install --save strapi-middleware-cache@2.0.1-beta.2

However note that this version's configuration is not backwards compatible with v1, so do take a look at the documentation on that branch to learn how to configure it.

Hope this helps !

ViVa98 commented 3 years ago

@patrixr thanks for the reply and info about the beta version. There is an option to specify a particular API endpoint to bust when a PUT/DELETE request comes for the same endpoint.

What I'm looking for is to bust a specific endpoint cache when PUT/DELETE request for another endpoint.

Requesting @stafyniaksacha to please go through the above-mentioned use case and also to provide a brief/one-line description for the beta version.

stafyniaksacha commented 3 years ago

Hello @ViVa98 You can use the internal cache middleware by setting the withStrapiMiddleware to true and use it in lifecycle methods

Let's say you have this cache config:

// file: config/cache.js

/**
 * @type {import('strapi-middleware-cache').UserMiddlewareCacheConfig}
 */
module.exports = {
  enabled: true,
  clearRelatedCache: true,
  withStrapiMiddleware: true,
  models: [
    {
      model: "profile",
      injectDefaultRoutes: false,
      routes: [
        "/profiles/:slug",
      ],
    },
  ],
};

this will register only /profiles/:slug route to be cached, you have to clear it manually then with lifecycles:

// file: api/profile/models/profile.js

/**
 * Lifecycle callbacks for the `profile` model.
 */
async function clearProfileCache(data) {
  const cache = strapi?.middleware?.cache || {};

  if (cache && typeof cache.clearCache === "function") {
    const profileCache = cache.getCacheConfig("profile");

    if (profileCache && typeof data.slug === "string") {
      await cache.clearCache(profileCache, { slug: data.slug });
      return;
    }
  }
}

module.exports = {
  lifecycles: {
    async afterDelete(result, data) {
      try {
        await clearProfileCache(result);
      } catch (error) {
        strapi.log.error("profile afterDelete:clearProfileCache");
        strapi.log.error(error);

        if (
          typeof strapi.plugins?.sentry?.services?.sentry?.sendError ===
          "function"
        ) {
          strapi.plugins.sentry.services.sentry.sendError(error);
        }
      }
    },

    async afterUpdate(result, params, data) {
      try {
        await clearProfileCache(result);
      } catch (error) {
        strapi.log.error("profile afterUpdate:clearProfileCache");
        strapi.log.error(error);

        if (
          typeof strapi.plugins?.sentry?.services?.sentry?.sendError ===
          "function"
        ) {
          strapi.plugins.sentry.services.sentry.sendError(error);
        }
      }
    },
  },
};
ViVa98 commented 3 years ago
[cache] GET /shops/shop_name **MISS**
GET /shops/shop_name (358 ms) 200

PUT /shops/id (867 ms) 200

[cache] GET /shops/shop_name **HIT**
GET /shops/shop_name (3 ms) 200
[cache] GET /profiles/username **MISS**
GET /profiles/username (280 ms) 200

PUT /profiles/id (1023 ms) 200

[cache] GET /profiles/username **MISS**
GET /profiles/username (324 ms) 200

And also for the shops when logged shopCache the paramNames array is empty [] instead of ['shop_name']. But for profiles, paramNames array is ['username']

stafyniaksacha commented 3 years ago

Sorry, I don't understand what you are expecting. Can you provide more information of what you need and your current configuration?

paramNames are populated from cache config (and not gathered from strapi), on the example I sent there is only one route /profiles/:slug registered with a slug param on profile collection

ViVa98 commented 3 years ago

paramNames are populated from cache config (and not gathered from strapi), on the example I sent there is only one route /profiles/:slug registered with a slug param on profile collection

You're right about populating paramNames from the cache config but in my case, I configured two models with a route in each one.

// file: config/middleware.js

module.exports = ({ env }) => ({
  settings: {
    cache: {
      enabled: true,
      clearRelatedCache: true,
      withStrapiMiddleware: true,
      models: [
        {
          model: "profile",
          injectDefaultRoutes: false,
          routes: ["/profiles/:username"],
        },
        {
          model: "shop",
          injectDefaultRoutes: false,
          routes: ["/shops/:shop_name"],
        },
      ],
    },
  },
});

After the below line in file: api/shop/models/shop.js const shopCache = cache.getCacheConfig("shop"); I tried logging shopCache and the result is

{
  singleType: false,
  hitpass: [Function: hitpass],
  injectDefaultRoutes: false,
  headers: [],
  maxAge: 3600000,
  model: 'shop',
  routes: [ { path: '/shops/:shop_name', method: 'GET', paramNames: [] } ]
}

If you observe paramNames above, it is empty. It has to be filled like ['shop_name'].

ViVa98 commented 3 years ago

For some reason, paramNames is not populating for my second model configured in file: config/middleware.js. Tried hardcoding the shopCache variable like below instead of assigning from cache.getCacheConfig("shop")

const shopCache = {
  singleType: false,
  hitpass: [Function: hitpass],
  injectDefaultRoutes: false,
  headers: [],
  maxAge: 3600000,
  model: 'shop',
  routes: [ { path: '/shops/:shop_name', method: 'GET', paramNames: ["shop_name"] } ]
}

After this modification cache is busting when PUT/DELETE request is completed. @stafyniaksacha thank you.

stafyniaksacha commented 3 years ago

Hum, this is wired.

The paramNames should be resolved here (with /:([^/]+)/g regex) And the model entry should be shop in this case (not link)

Can you log the cache.options In your file: api/shop/models/shop.js?
So we can check the resolved configuration.

Also, we will have more information using debug log level:

// file: config/middleware.js

module.exports = ({ env }) => ({
  settings: {
    logger: {
      level: "debug",
      exposeInContext: true,
    },
    cache: {
      // ...
    },
  },
});
ViVa98 commented 3 years ago

Response from the cache.options using basic console.log

{
  type: 'mem',
  logs: true,
  enabled: true,
  populateContext: false,
  populateStrapiMiddleware: false,
  enableEtagSupport: false,
  enableXCacheHeaders: false,
  clearRelatedCache: true,
  withKoaContext: false,
  withStrapiMiddleware: true,
  headers: [],
  max: 500,
  maxAge: 3600000,
  cacheTimeout: 500,
  models: [
    {
      singleType: false,
      hitpass: [Function: hitpass],
      injectDefaultRoutes: false,
      headers: [],
      maxAge: 3600000,
      model: 'profile',
      routes: [Array]
    },
    {
      singleType: false,
      hitpass: [Function: hitpass],
      injectDefaultRoutes: false,
      headers: [],
      maxAge: 3600000,
      model: 'shop',
      routes: [Array]
    }
  ]
}

Response using strapi.log.debug after adding logger to middleware file

{
   "type":"mem",
   "logs":true,
   "enabled":true,
   "populateContext":false,
   "populateStrapiMiddleware":false,
   "enableEtagSupport":false,
   "enableXCacheHeaders":false,
   "clearRelatedCache":true,
   "withKoaContext":false,
   "withStrapiMiddleware":true,
   "headers":[],
   "max":500,
   "maxAge":3600000,
   "cacheTimeout":500,
   "models":[
      {
         "singleType":false,
         "injectDefaultRoutes":false,
         "headers":[],
         "maxAge":3600000,
         "model":"profile",
         "routes":[
            {
               "path":"/profiles/:username",
               "method":"GET",
               "paramNames":["username"]
            }
         ]
      },
      {
         "singleType":false,
         "injectDefaultRoutes":false,
         "headers":[],
         "maxAge":3600000,
         "model":"shop",
         "routes":[
            {
               "path":"/shops/:shop_name",
               "method":"GET",
               "paramNames":[]
            }
         ]
      }
   ]
}