├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── package-lock.json ├── package.json ├── src └── index.js └── test └── indexSpec.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | }, 4 | "env": { 5 | "node": true, 6 | "es6": true 7 | }, 8 | 9 | "extends": "eslint:recommended", 10 | 11 | "rules": { 12 | "no-cond-assign": 0, 13 | "no-constant-condition": 0, 14 | "no-empty": 0, 15 | "no-fallthrough": 0, 16 | "no-unused-vars": 1, 17 | "no-console": 1, 18 | 19 | "semi": 2, 20 | "curly": 2, 21 | "consistent-this": [2, "self"], 22 | "indent": [ 2, 4, { "SwitchCase": 1 } ], 23 | "linebreak-style": [2, "unix"], 24 | "no-nested-ternary": 2, 25 | 26 | "new-parens": 2, 27 | "no-dupe-class-members": 2, 28 | "require-yield": 2, 29 | "arrow-spacing": 1, 30 | "no-var": 2, 31 | 32 | "no-multi-spaces": 1, 33 | "space-infix-ops": [1, {"int32Hint": false}], 34 | "brace-style": 1, 35 | "space-before-blocks": 1, 36 | "operator-linebreak": [1, "before"], 37 | "no-unneeded-ternary": 1, 38 | "no-lonely-if": 1, 39 | "key-spacing": 1, 40 | "quotes": [1, "double", "avoid-escape"], 41 | "no-trailing-spaces": [1, { "skipBlankLines": true }] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "4" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright 2021 Konstantin Pogorelov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | default: test 4 | 5 | test: 6 | npx mocha 7 | lint: 8 | npx eslint 9 | clean: 10 | rm -rf coverage 11 | coverage: 12 | npx istanbul cover -i 'src/*.js' --dir coverage ./node_modules/.bin/mocha 13 | coveralls: coverage 14 | ifndef COVERALLS_REPO_TOKEN 15 | $(error COVERALLS_REPO_TOKEN is undefined) 16 | endif 17 | npx coveralls < coverage/lcov.info 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://api.travis-ci.org/disjunction/node-cache-manager-mongoose.png)](https://travis-ci.org/disjunction/node-cache-manager-mongoose) [![NPM version](https://badge.fury.io/js/cache-manager-mongoose.png)](http://badge.fury.io/js/cache-manager-mongoose) [![Coverage Status](https://coveralls.io/repos/github/disjunction/node-cache-manager-mongoose/badge.svg?branch=master)](https://coveralls.io/github/disjunction/node-cache-manager-mongoose?branch=master) 2 | 3 | # cache-manager-mongoose 4 | 5 | Mongoose store for node-cache-manager (See https://github.com/BryanDonovan/node-cache-manager) 6 | 7 | Originally created as an alternative to node-cache-manager-mongodb store 8 | to be able to use an existing mongoose connection. 9 | 10 | 11 | ## Usage examples 12 | 13 | This store expects that the mongoose instance is provided explicitely. 14 | 15 | It is not a concern of this module to establish a connection, 16 | but it rather assumes that you already have one and use it in your project. 17 | 18 | 19 | ```javascript 20 | const 21 | mongoose = require("mongoose"), 22 | cacheManager = require("cache-manager"), 23 | mongooseStore = require("cache-manager-mongoose"); 24 | 25 | mongoose.connect("mongodb://127.0.0.1/test"); 26 | 27 | const cache = cacheManager.caching({ 28 | store: mongooseStore, 29 | mongoose: mongoose 30 | }); 31 | 32 | // now you can use cache as any other cache-manager cache 33 | 34 | ``` 35 | 36 | ### Options 37 | 38 | All optionas are **optional**, except for `mongoose` instance. 39 | 40 | The store creates a new Model on initialization, which you can partially customize. See example: 41 | 42 | ```javascript 43 | const cache = cacheManager.caching({ 44 | store: mongooseStore, 45 | mongoose: mongoose, // mongoose instance 46 | modelName: "MyModelName", // model name in mongoose registry 47 | 48 | // options for model creation 49 | modelOptions: { 50 | collection: "cacheman_rcp" // mongodb collection name 51 | versionKey: false // do not create __v field 52 | }, 53 | 54 | ttl: 300 // time to live - 5 minutes (default is 1 minute), 55 | 56 | connection: connection, // provide only when using mongoose.createConnection() 57 | }); 58 | ``` 59 | 60 | If you want to keep your cache **forever**, set TTL to zero (`0`) 61 | 62 | The default modelOptions are: 63 | ```json 64 | { 65 | "collection": "MongooseCache", 66 | "versionKey": false, 67 | "read": "secondaryPreferred" 68 | } 69 | ``` 70 | 71 | ### Custom model 72 | 73 | You can also provide your own model as long as it has the same 74 | fields as the one used by default. 75 | 76 | In this case you don't need to provide a `mongoose` instance, 77 | as all it boils down to is a model object. 78 | 79 | Here is an example: 80 | 81 | ```javascript 82 | const schema = new mongoose.Schema( 83 | { 84 | // standard fields 85 | _id: String, 86 | val: mongoose.Schema.Types.Mixed, 87 | exp: Date, 88 | 89 | // all other fields you like 90 | foo: String, 91 | bar: Number 92 | }, 93 | { 94 | collection: "my_collection", 95 | versionKey: false 96 | } 97 | ); 98 | 99 | schema.index( 100 | {exp: 1}, 101 | {expireAfterSeconds: 0} 102 | ); 103 | schema.index({foo: 1}); 104 | 105 | const model = mongoose.model("MyModel", schema); 106 | 107 | const cache = cacheManager.caching({ 108 | store: mongooseStore, 109 | model: model 110 | }); 111 | ``` 112 | 113 | ## License 114 | 115 | MIT No Attribution 116 | 117 | https://github.com/aws/mit-0 118 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # just a handy way to try out different node versions 2 | 3 | cmm: 4 | image: node:4 5 | volumes: 6 | - ./src:/app/src 7 | - ./test:/app/test 8 | - ./package.json:/app/package.json 9 | command: sleep 1000000000 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache-manager-mongoose", 3 | "version": "1.0.1", 4 | "description": "mongoose store for node-cache-manager", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "npx mocha" 8 | }, 9 | "keywords": [ 10 | "cache", 11 | "mongoose", 12 | "cache manager" 13 | ], 14 | "files": [ 15 | "src" 16 | ], 17 | "author": "Konstantin Pogorelov ", 18 | "license": "MIT-0", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/disjunction/node-cache-manager-mongoose.git" 22 | }, 23 | "devDependencies": { 24 | "cache-manager": "^2.2.0", 25 | "chai": "^4.3.4", 26 | "eslint": "^7.32.0", 27 | "mocha": "^9.1.3", 28 | "mock-mongoose": "^8.0.1-a", 29 | "sinon": "^11.1.2", 30 | "sinon-test": "^3.1.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class MongooseStoreError extends Error {} 4 | 5 | class MongooseStore { 6 | constructor(args) { 7 | if (!args || !(args.mongoose || args.model)) { 8 | throw new MongooseStoreError( 9 | "you MUST provide either mongoose or model instance in store args" 10 | ); 11 | } 12 | 13 | this.mongoose = args.mongoose; 14 | this.modelProvider = args.connection || args.mongoose; 15 | 16 | if (args.model) { 17 | switch (typeof args.model) { 18 | case "object": 19 | this.model = args.model; 20 | break; 21 | case "string": 22 | this.model = this.modelProvider.model(args.model); 23 | break; 24 | default: 25 | throw new MongooseStoreError("unexpected type of args.model in constructor"); 26 | } 27 | } else { 28 | this.model = this.makeModel(args); 29 | } 30 | this.ttl = args.ttl === undefined ? 60 : args.ttl; 31 | } 32 | 33 | makeModel(args) { 34 | const schemaTemplate = { 35 | obj: { 36 | _id: String, 37 | val: this.mongoose.Schema.Types.Mixed, 38 | exp: Date 39 | }, 40 | options: { 41 | collection: "MongooseCache", 42 | versionKey: false, 43 | read: "secondaryPreferred" 44 | } 45 | }; 46 | 47 | const options = Object.assign({}, schemaTemplate.options, args.modelOptions); 48 | const schema = new this.mongoose.Schema( 49 | schemaTemplate.obj, 50 | options 51 | ); 52 | 53 | schema.index( 54 | {exp: 1}, 55 | {expireAfterSeconds: 0} 56 | ); 57 | 58 | return this.modelProvider.model(args.modelName || "MongooseCache", schema); 59 | } 60 | 61 | result(fn, error, result) { 62 | if (fn) { 63 | fn(error, result); 64 | } 65 | return result; 66 | } 67 | 68 | get(key, options, fn) { 69 | try { 70 | return this.model.findOne( 71 | {_id: key} 72 | ) 73 | .then(record => { 74 | if (!record) { 75 | return this.result(fn); 76 | } 77 | 78 | // this is necessary, since mongoose autoclean is not accurate 79 | if (record.exp && record.exp < new Date()) { 80 | return this.del(key, null, fn); 81 | } else { 82 | return this.result(fn, null, record.val); 83 | } 84 | }) 85 | .catch(e => this.result(fn, e)); 86 | } catch (e) { 87 | this.result(fn, e); 88 | } 89 | } 90 | 91 | set(key, val, options, fn) { 92 | try { 93 | options = options || {}; 94 | let ttl = options.ttl || this.ttl; 95 | 96 | const doc = {val: val}; 97 | if (this.ttl > 0) { 98 | doc.exp = new Date(Date.now() + ttl * 1000); 99 | } 100 | 101 | return this.model.updateOne( 102 | {_id: key}, 103 | doc, 104 | {upsert: true} 105 | ) 106 | .then(() => this.result(fn)) 107 | .catch(e => this.result(fn, e)); 108 | } catch (e) { 109 | this.result(fn, e); 110 | } 111 | } 112 | 113 | del(key, options, fn) { 114 | try { 115 | return this.model.deleteOne( 116 | {_id: key} 117 | ) 118 | .then(() => this.result(fn)); 119 | } catch (e) { 120 | this.result(fn, e); 121 | } 122 | } 123 | 124 | reset(key, fn) { 125 | try { 126 | if ("function" === typeof key) { 127 | fn = key; 128 | key = null; 129 | } 130 | return this.model.deleteMany({}) 131 | .then(() => { 132 | if (fn) { 133 | fn(); 134 | } 135 | }); 136 | } catch (e) { 137 | this.result(fn, e); 138 | } 139 | } 140 | 141 | keys(fn) { 142 | try { 143 | let now = new Date(); 144 | 145 | return this.model 146 | .find({}) 147 | .then(records => { 148 | records = records.filter(function(record) { 149 | return (!record.exp || record.exp > now); 150 | }).map(record => record._id); 151 | 152 | this.result(fn, null, records); 153 | }) 154 | .catch(e => this.result(fn, e)); 155 | } catch (e) { 156 | this.result(fn, e); 157 | } 158 | } 159 | } 160 | 161 | module.exports = { 162 | create: function (args) { 163 | return new MongooseStore(args); 164 | } 165 | }; 166 | -------------------------------------------------------------------------------- /test/indexSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | "use strict"; 3 | 4 | let mongoose = require("mock-mongoose").MockMongoose, 5 | cmm = require("../src/index"), 6 | cm = require("cache-manager"), 7 | sinon = require("sinon"), 8 | sinonTest = require("sinon-test"), 9 | expect = require("chai").expect; 10 | 11 | const test = sinonTest(sinon); 12 | 13 | // extend mock-mongoose with missing methods 14 | mongoose.model = () => ({}); 15 | mongoose.Schema = function () { 16 | this.index = sinon.stub(); 17 | }; 18 | mongoose.Schema.Types = {}; 19 | 20 | describe("cache-manager-mongoose", function() { 21 | let modelStub = { 22 | testProp: true, 23 | updateOne: () => Promise.resolve(), 24 | findOne: () => Promise.resolve(), 25 | deleteOne: () => Promise.resolve(), 26 | deleteMany: () => Promise.resolve() 27 | }; 28 | 29 | it("throws on no mongoose provided", function() { 30 | expect(() => { 31 | cm.caching({store: cmm}); 32 | }).to.throw(); 33 | }); 34 | 35 | it("throws on strange model type", function() { 36 | expect(() => { 37 | cm.caching({ 38 | store: cmm, 39 | mongoose: mongoose, 40 | model: true 41 | }); 42 | }).to.throw(); 43 | }); 44 | 45 | it("accepts model passed directly", function() { 46 | let cache = cm.caching({ 47 | store: cmm, 48 | model: modelStub 49 | }); 50 | expect(cache.store.model.testProp).to.be.true; 51 | }); 52 | 53 | it("accepts model passed by name", test(function() { 54 | this.stub(mongoose, "model").callsFake(() => modelStub); 55 | let cache = cm.caching({ 56 | store: cmm, 57 | mongoose: mongoose, 58 | model: "MyModel" 59 | }); 60 | expect(cache.store.model.testProp).to.be.true; 61 | })); 62 | 63 | it("finds model if passed as string", test(function() { 64 | this.stub(mongoose, "model").returns({dummyField: true}); 65 | let cache = cm.caching({ 66 | store: cmm, 67 | modelName: "dummyModel", 68 | mongoose: mongoose 69 | }); 70 | sinon.assert.calledOnce(mongoose.model); 71 | expect(cache.store.model.dummyField).to.be.true; 72 | })); 73 | 74 | it("creates new model if model not provided", function() { 75 | let cache = cm.caching({ 76 | store: cmm, 77 | mongoose: mongoose 78 | }); 79 | expect(cache.store.model).not.to.be.undefined; 80 | }); 81 | 82 | it("set calls update", test(function(done) { 83 | let cache = cm.caching({ 84 | store: cmm, 85 | model: modelStub 86 | }); 87 | let spy = this.stub(modelStub, "updateOne"); 88 | cache.set("someKey", "someValue", null, function () { 89 | sinon.assert.calledOnce(spy); 90 | done(); 91 | }); 92 | })); 93 | 94 | it("set supports infinite ttl", test(function(done) { 95 | let cache = cm.caching({ 96 | store: cmm, 97 | model: modelStub, 98 | ttl: 0 99 | }); 100 | let spy = this.stub(modelStub, "updateOne"); 101 | cache.set("someKey", "someValue", null, function () { 102 | sinon.assert.calledOnce(spy); 103 | done(); 104 | }); 105 | })); 106 | 107 | it("get calls findOne", test(function(done) { 108 | let cache = cm.caching({ 109 | store: cmm, 110 | model: modelStub 111 | }); 112 | let spy = this.stub(modelStub, "findOne"); 113 | cache.get("someKey", null, function () { 114 | sinon.assert.calledOnce(spy); 115 | done(); 116 | }); 117 | })); 118 | 119 | it("del calls deleteOne", test(function(done) { 120 | let cache = cm.caching({ 121 | store: cmm, 122 | model: modelStub 123 | }); 124 | let spy = this.stub(modelStub, "deleteOne"); 125 | cache.del("someKey", null, function () { 126 | sinon.assert.calledOnce(spy); 127 | done(); 128 | }); 129 | })); 130 | 131 | it("reset calls deleteMany", test(function(done) { 132 | let cache = cm.caching({ 133 | store: cmm, 134 | model: modelStub 135 | }); 136 | let spy = this.stub(modelStub, "deleteMany"); 137 | cache.reset(function () { 138 | sinon.assert.calledOnce(spy); 139 | done(); 140 | }); 141 | })); 142 | 143 | it("keys calls find", test(function(done) { 144 | let cache = cm.caching({ 145 | store: cmm, 146 | model: modelStub 147 | }); 148 | 149 | modelStub.find = () => Promise.resolve([ 150 | {_id: "apple"}, 151 | {_id: "banana"} 152 | ]); 153 | 154 | cache.keys(function(error, keys) { 155 | expect(keys).to.deep.eql(["apple", "banana"]); 156 | done(); 157 | }); 158 | })); 159 | }); 160 | --------------------------------------------------------------------------------