question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Cache with mongoose ObjectId will fail to find the keyFieldName in cache

See original GitHub issue

First just wanted to thank you for this awesome tool, and wanted to inform that when using the cache hook with feathers mongoose as a service in the after method it will return a document with the _id field of type ObjectId not a string, thus will fail to find it when searching the cache index.

Example of a way am calling a service and returning a document

class BlogModel {
    async get(id, params) {
       return app.getService('posts').get(id, {query: {$populate: 'owner'}});
     }
}
module.exports = {
  before: {
    all: cache(cacheMap, '_id')
  },
  after: {
    all: cache(cacheMap, '_id')
  }
};

The fix am using, or should I have used a transformation hook before the caching

'use strict';

const common = require('feathers-hooks-common');
const mongoose = require("mongoose");

module.exports = function (cacheMap, keyFieldName, options = {}) {
  const clone = options.clone || defaultClone;
  return context => {
    keyFieldName = keyFieldName || (context.service || {}).id; // Will be undefined on client

    let items = common.getItems(context);
    items = Array.isArray(items) ? items : [items];

    if (context.type === 'after') {
      if (context.method === 'remove') {
        return;
      }

      items.forEach(item => {
        const idName = getIdName(keyFieldName, item);
        // cacheMap.set(item[idName], clone(item));
        cacheMap.set(objectIdToString(item[idName]), clone(item));
      });
      return;
    }

    switch (context.method) {
      case 'find': // fall through
      case 'create':
        return;
      case 'get': {
        const value = cacheMap.get(context.id);
        if (value) {
          context.result = value;
        }
        return context;
      }
      default: // update, patch, remove
        if (context.id) {
          cacheMap.delete(context.id);
          return;
        }

        items.forEach(item => {
          const idName = getIdName(keyFieldName, item);
          // cacheMap.delete(item[idName]);
          cacheMap.delete(objectIdToString(item[idName]));
        });
    }
  };
};

function getIdName (keyFieldName, item) {
  if (keyFieldName) return keyFieldName;
  return ('_id' in item) ? '_id' : 'id';
}

function defaultClone (obj) {
  return JSON.parse(JSON.stringify(obj));
}

function objectIdToString (id) {
 return (id instanceof mongoose.Types.ObjectId) ? id.toString() : id;
}

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:7 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
eddyystopcommented, Aug 11, 2018

The users service would be one of the better places to use cache if possible.

There are several complications:

  • Things get complicated when hooks make service calls themselves. authenticate and restrictToUser both call the users service if its an external call.
  • Setting context.result, as cache does, does not prevent the following hooks from running. It just prevents the DB call once all the before hooks have run.
  • If you want to stop the remaining hooks after cache from running if cache found a users record, then after cache you would need a hook like iff(ctx => ctx.result && !ctx.provider, skipRemainingHooks()).
  • Most before hooks do not check if context.result is set. They assume they have to process context.data. Such hooks would ignore the record found by cache, process context.data needlessly as context.data found be ignored as the DB call is not made.

So using cache on the users service depends on the circumstances.

The good news is restrictToUser and authenticate return immediately if the call has been made by the server. So if your hooks are only:

before: { get: [ cache(), restrictToUser(), authenticate() ] },
after: { get: [ cache(), discard('password') }

The following hooks execute:

  • on the external call
    • cache before
    • restrictToUser, which makes a get call
      • cache before
      • no DB call
      • cache after
    • authenticate, which makes a get call
      • cache before
      • no DB call
      • cache after
    • no DB call
    • cache after
    • discard

Which looks like it should work. Let me know your results should you decide to try it.

0reactions
axmad22commented, Aug 15, 2018

Yes its working now, also am testing it on the Users service I had to make sure in every place in code where I test for the users ID or any ID to do it using a helper function, since it could be either a string or an ObjectId depending if it came from the cacheMap or DB now. Its nice especially with web-socket it stopped hitting the DB, and that kind of feels wrong with the users case am not sure if its safe but I will keep on testing and see what happens. thanks for the help

// helper function

example testing in code
if (compareIds(context.params.user._id, post.createdBy)) {
}

const compareIds = (idOne, idTwo) => {
  if (idOne && idTwo) {
    let tmpOne = (idOne instanceof mongoose.Types.ObjectId) ? idOne.toString() : idOne;
    let tmpTwo = (idTwo instanceof mongoose.Types.ObjectId) ? idTwo.toString() : idTwo;

    return tmpOne === tmpTwo;
  }
  return false;
};

Reviewed thedocs and the makeCacheKey in the docs should have been

  // it should be key and not id
  const makeCacheKey = key => key instanceof mongoose.Types.ObjectId ? key .toString() : key ;
  const makeCacheKey = key => id instanceof mongoose.Types.ObjectId ? id.toString() : id;
Read more comments on GitHub >

github_iconTop Results From Across the Web

Cache with mongoose ObjectId will fail to find the ... - GitHub
Cache with mongoose ObjectId will fail to find the keyFieldName in cache #424 ... module.exports = function (cacheMap, keyFieldName, ...
Read more >
What's Mongoose error Cast to ObjectId failed for value XXX at ...
Mongoose's findById method casts the id parameter to the type of the model's _id field so that it can properly query for the...
Read more >
Better caching logic by overwriting mongoose functionality
We will overwrite find method in mongoose library to search the Redis cache instead of generating the query directly to DB.
Read more >
mongoose-redis - npm
Best Performance Mongoose caching module that works exactly how you would expect it to, with the latest version of Mongoose. Installation.
Read more >
module keystone function - GitHub Pages
keyToPath(key, true); this.schema = new keystone.mongoose. ... emptyOption; // cached maps for options, labels and values this.map = utils.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found