├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── catbox.js └── hapi.js ├── lib └── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/package-lock.json 3 | 4 | coverage.* 5 | 6 | **/.DS_Store 7 | **/._* 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | - "10" 6 | - "11" 7 | - "node" 8 | 9 | services: 10 | - mongodb 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.2.2](https://github.com/hapijs/catbox-mongodb/compare/v4.2.1...v4.2.2) - 2018-xx-xx 4 | 5 | ### Updated 6 | - bump to lab 18 7 | - readme refinements: rephrase, minor update to code snippets 8 | 9 | 10 | ## [4.2.1](https://github.com/hapijs/catbox-mongodb/compare/v4.2.0...v4.2.1) - 2018-11-01 11 | 12 | ### Changed 13 | - destructure `key` parameter for get, set, drop into `{ id, segment }` 14 | - fix deprecation warnings by updating to the latest implementation of MongoDB driver methods 15 | - Run tests on Node.js v11 (travis) 16 | - bump dependencies 17 | - clean up `.gitignore` 18 | 19 | 20 | ## [4.2.0](https://github.com/hapijs/catbox-mongodb/compare/v4.1.0...v4.2.0) - 2018-03-12 21 | 22 | ### Added 23 | - Example usage in a simple hapi project 24 | 25 | 26 | ### Changed 27 | - Update `mongodb` dependency to 3.x 28 | - Fix flaky test (increase timeout) 29 | - Rename `mongo` example to `catbox` 30 | - Update Readme 31 | 32 | 33 | ## [4.1.0](https://github.com/hapijs/catbox-mongodb/compare/v4.0.0...v4.1.0) - 2018-02-04 34 | 35 | ### Added 36 | - Pass through falsy values from cache to client 37 | 38 | 39 | ### Changed 40 | - Add test for falsy values 41 | - Updated Readme 42 | - Update example to `async/await` 43 | 44 | 45 | ## [4.0.0](https://github.com/hapijs/catbox-mongodb/compare/v3.0.1...v4.0.0) - 2017-11-28 46 | 47 | ### Changed 48 | - Update `catbox-mongodb` to fully `async/await` 49 | - Bump dependencies 50 | - Bump supported Node.js versions to 8 or higher 51 | 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please view our [hapijs contributing guide](https://github.com/hapijs/hapi/blob/master/CONTRIBUTING.md). 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014, Walmart and other contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of any contributors may not be used to endorse or promote 12 | products derived from this software without specific prior written 13 | permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | * * * 27 | 28 | The complete list of contributors can be found at: https://github.com/hapijs/catbox-mongodb/graphs/contributors -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # catbox-mongodb 2 | 3 | MongoDB adapter for [catbox](https://github.com/hapijs/catbox) 4 | 5 | Note: the module has been deprecated and archived due to low usage. The last publish version is known it work well but that may change in the future as breaking changes of catbox are introduced. If you rely on this module, consider forking it and creating your own alternative (it is very little code). You can also ask to take over and the module will be moved to your personal account to maintain. 6 | 7 | **catbox-mongodb** serializes values to BSON using MongoDB driver, therefore following data types are supported for this adapter: Object, Array, Number, String, Date, RegExp. 8 | 9 | 10 | ## Installation 11 | > The lastest `catbox-mongodb` version `4.x` works only with **hapi v17 and v18** 12 | 13 | Install `catbox-mongodb` via NPM. Remember that `catbox-mongodb` requires its parent module [`catbox`](https://github.com/hapijs/catbox): 14 | 15 | ``` 16 | npm install catbox catbox-mongodb 17 | ``` 18 | 19 | --- 20 | 21 | Do you use **hapi v16 or lower**? Install `catbox-mongodb` version `3.x` with a compatible version of `catbox`: 22 | 23 | ``` 24 | # for hapi v16 (or lower) 25 | npm install catbox@9 catbox-mongodb@3 26 | ``` 27 | 28 | 29 | ## Options 30 | `catbox-mongodb` accepts the following options: 31 | 32 | - `uri` - the [MongoDB URI](https://docs.mongodb.com/manual/reference/connection-string/), defaults to `'mongodb://127.0.0.1:27017/?maxPoolSize=5'` 33 | - `partition` - the MongoDB database for cached items 34 | 35 | 36 | ## Usage 37 | Sample catbox cache initialization : 38 | 39 | ```JS 40 | const Catbox = require('catbox'); 41 | 42 | const cache = new Catbox.Client(require('catbox-mongodb'), { 43 | uri: 'your-mongodb-uri', // Defaults to 'mongodb://127.0.0.1:27017/?maxPoolSize=5' 44 | partition: 'cache-users' 45 | }) 46 | ``` 47 | 48 | Or configure your hapi server to use `catbox-mongodb` as the caching strategy (code snippet uses hapi `v17`): 49 | 50 | ```js 51 | const Hapi = require('hapi') 52 | 53 | const server = new Hapi.Server({ 54 | cache : [{ 55 | name: 'mongodb-cache', 56 | engine: require('catbox-mongodb'), 57 | uri: 'your-mongodb-uri', // Defaults to 'mongodb://127.0.0.1:27017/?maxPoolSize=5' 58 | partition: 'cache-posts' 59 | }] 60 | }); 61 | ``` 62 | 63 | For hapi `v18` you need a slightly different config: 64 | 65 | ```js 66 | const Hapi = require('hapi') 67 | 68 | const server = new Hapi.Server({ 69 | cache : [{ 70 | name: 'mongodb-cache', 71 | provider: { 72 | constructor: require('catbox-mongodb'), 73 | options: { 74 | uri : 'your-mongodb-uri', // Defaults to 'mongodb://127.0.0.1:27017/?maxPoolSize=5' 75 | partition : 'cache' 76 | } 77 | } 78 | }] 79 | }); 80 | ``` 81 | -------------------------------------------------------------------------------- /examples/catbox.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // After starting this example load http://localhost:8080 4 | // hit refresh, you will notice that it loads the response 5 | // from cache for the first 5 seconds and then reloads the cache 6 | 7 | 8 | // Load modules 9 | const Catbox = require('catbox'); 10 | const Http = require('http'); 11 | 12 | 13 | // Declare internals 14 | const internals = {}; 15 | 16 | 17 | internals.handler = async (req, res) => { 18 | 19 | try { 20 | const item = await internals.getResponse(); 21 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 22 | res.end(item); 23 | } 24 | catch (ignoreErr) { 25 | res.writeHead(500); 26 | res.end(); 27 | } 28 | }; 29 | 30 | 31 | internals.getResponse = async () => { 32 | 33 | const key = { 34 | segment: 'example', 35 | id: 'myExample' 36 | }; 37 | 38 | const cached = await internals.client.get(key); 39 | 40 | if (cached) { 41 | return `From cache: ${cached.item}`; 42 | } 43 | 44 | await internals.client.set(key, 'my example', 5000); 45 | return 'my example'; 46 | }; 47 | 48 | 49 | internals.startCache = async () => { 50 | 51 | const options = { 52 | partition: 'examples' 53 | }; 54 | 55 | internals.client = new Catbox.Client(require('../'), options); // Replace require('../') with 'catbox-mongodb' in your own code 56 | await internals.client.start(); 57 | }; 58 | 59 | 60 | internals.startServer = () => { 61 | 62 | const server = Http.createServer(internals.handler); 63 | server.listen(8080); 64 | console.log('Server started at http://localhost:8080/'); 65 | }; 66 | 67 | internals.start = async () => { 68 | 69 | await internals.startCache(); 70 | internals.startServer(); 71 | }; 72 | 73 | internals.start(); 74 | -------------------------------------------------------------------------------- /examples/hapi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // After starting this example load http://localhost:8080 4 | // hit refresh, you will notice that it loads the response 5 | // from cache for the first 5 seconds and then reloads the cache 6 | 7 | 8 | // Load modules 9 | const CatboxMongo = require('..'); 10 | const Hapi = require('hapi'); 11 | 12 | 13 | // create server instance with MongoDB cache 14 | const server = new Hapi.Server({ 15 | port: 8080, 16 | cache: [ 17 | { 18 | name: 'mongoCache', 19 | engine: CatboxMongo, 20 | host: '127.0.0.1', 21 | partition: 'example-cache' 22 | } 23 | ] 24 | }); 25 | 26 | 27 | // define a cache segment to store cache items 28 | const Cache = server.cache({ segment: 'examples', expiresIn: 1000 * 5 }); 29 | 30 | 31 | // wildcard route that responds all requests 32 | // either with data from cache or default string 33 | server.route({ 34 | method: 'GET', 35 | path: '/{path*}', 36 | handler: async (request, h) => { 37 | 38 | const key = { 39 | segment: 'examples', 40 | id: 'myExample' 41 | }; 42 | 43 | // get item from cache segment 44 | const cached = await Cache.get(key); 45 | 46 | if (cached) { 47 | return `From cache: ${cached.item}`; 48 | } 49 | 50 | // fill cache with item 51 | await Cache.set(key, { item: 'my example' }, 5000); 52 | 53 | return 'my example'; 54 | } 55 | }); 56 | 57 | 58 | const start = async function () { 59 | 60 | try { 61 | await server.start(); 62 | } 63 | catch (err) { 64 | console.log(err); 65 | process.exit(1); 66 | } 67 | 68 | console.log('Server running at:', server.info.uri); 69 | }; 70 | 71 | start(); 72 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MongoDB = require('mongodb'); 4 | const Hoek = require('hoek'); 5 | const Boom = require('boom'); 6 | 7 | const defaults = { 8 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5' 9 | }; 10 | 11 | exports = module.exports = class Connection { 12 | 13 | constructor(options) { 14 | 15 | Hoek.assert(this instanceof Connection, 'MongoDB cache client must be instantiated using new'); 16 | 17 | this.db = null; 18 | this.client = null; 19 | this.collections = {}; 20 | this.isConnected = false; 21 | this.connectionPromise = null; 22 | this.isConnectionStarted = false; 23 | this.settings = this.getSettings(options); 24 | 25 | return this; 26 | } 27 | 28 | getSettings(options) { 29 | /* 30 | Database names: 31 | 32 | - empty string is not valid 33 | - cannot contain space, "*<>:|? 34 | - limited to 64 bytes (after conversion to UTF-8) 35 | - admin, local and config are reserved 36 | */ 37 | 38 | Hoek.assert(options.partition !== 'admin' && options.partition !== 'local' && options.partition !== 'config', 'Cache partition name cannot be "admin", "local", or "config" when using MongoDB'); 39 | Hoek.assert(options.partition.length < 64, 'Cache partition must be less than 64 bytes when using MongoDB'); 40 | 41 | const settings = Hoek.applyToDefaults(defaults, options); 42 | 43 | settings.uri = settings.uri.replace(/(mongodb:\/\/[^\/]*)([^\?]*)(.*)/,`$1/${settings.partition}$3`); 44 | 45 | return settings; 46 | } 47 | 48 | async start() { 49 | 50 | if (this.isConnected) { 51 | return; 52 | } 53 | 54 | if (this.connectionPromise) { 55 | return this.connectionPromise; 56 | } 57 | 58 | this.isConnectionStarted = true; 59 | 60 | try { 61 | this.connectionPromise = MongoDB.MongoClient.connect(this.settings.uri, { 62 | useNewUrlParser: true 63 | }); 64 | this.client = await this.connectionPromise; 65 | this.db = this.client.db(); 66 | this.isConnected = true; 67 | } 68 | catch (err) { 69 | this.connectionPromise = null; 70 | throw err; 71 | } 72 | } 73 | 74 | stop() { 75 | 76 | if (this.client) { 77 | this.client.close(); 78 | 79 | this.db = null; 80 | this.client = null; 81 | this.collections = {}; 82 | this.isConnected = false; 83 | this.connectionPromise = null; 84 | this.isConnectionStarted = false; 85 | } 86 | } 87 | 88 | isReady() { 89 | 90 | return this.isConnected; 91 | } 92 | 93 | validateSegmentName(name) { 94 | 95 | /* 96 | Collection names: 97 | 98 | - empty string is not valid 99 | - cannot contain "\0" 100 | - avoid creating any collections with "system." prefix 101 | - user created collections should not contain "$" in the name 102 | - database name + collection name < 100 (actual 120) 103 | */ 104 | 105 | if (!name) { 106 | throw new Boom('Empty string'); 107 | } 108 | 109 | if (name.indexOf('\0') !== -1) { 110 | throw new Boom('Includes null character'); 111 | } 112 | 113 | if (name.indexOf('system.') === 0) { 114 | throw new Boom('Begins with "system."'); 115 | } 116 | 117 | if (name.indexOf('$') !== -1) { 118 | throw new Boom('Contains "$"'); 119 | } 120 | 121 | if (name.length + this.settings.partition.length >= 100) { 122 | throw new Boom('Segment and partition name lengths exceeds 100 characters'); 123 | } 124 | 125 | return null; 126 | } 127 | 128 | async getCollection(name) { 129 | 130 | if (!this.isConnected) { 131 | throw new Boom('Connection not ready'); 132 | } 133 | 134 | if (!name) { 135 | throw new Boom('Collection name missing'); 136 | } 137 | 138 | if (this.collections[name]) { 139 | return this.collections[name]; 140 | } 141 | 142 | const collection = await this.db.collection(name); 143 | 144 | await collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }); 145 | this.collections[name] = collection; 146 | 147 | return collection; 148 | } 149 | 150 | async get({ id, segment }) { 151 | 152 | if (!this.isConnectionStarted) { 153 | throw new Boom('Connection not started'); 154 | } 155 | 156 | const collection = await this.getCollection(segment); 157 | const criteria = { _id: id }; 158 | const record = await collection.findOne(criteria); 159 | 160 | if (!record) { 161 | return null; 162 | } 163 | 164 | if (!record.stored) { 165 | throw new Boom('Incorrect record structure'); 166 | } 167 | 168 | const envelope = { 169 | item: record.value, 170 | stored: record.stored.getTime(), 171 | ttl: record.ttl 172 | }; 173 | 174 | return envelope; 175 | } 176 | 177 | async set({ id, segment }, value, ttl) { 178 | 179 | if (!this.isConnectionStarted) { 180 | throw new Boom('Connection not started'); 181 | } 182 | 183 | const collection = await this.getCollection(segment); 184 | const expiresAt = new Date(); 185 | expiresAt.setMilliseconds(expiresAt.getMilliseconds() + ttl); 186 | 187 | const record = { 188 | value, 189 | stored: new Date(), 190 | ttl, 191 | expiresAt 192 | }; 193 | 194 | const criteria = { _id: id }; 195 | 196 | await collection.updateOne(criteria, { $set: record }, { upsert: true, safe: true }); 197 | } 198 | 199 | async drop({ id, segment }) { 200 | 201 | if (!this.isConnectionStarted) { 202 | throw new Boom('Connection not started'); 203 | } 204 | 205 | const collection = await this.getCollection(segment); 206 | 207 | const criteria = { _id: id }; 208 | await collection.deleteOne(criteria, { safe: true }); 209 | } 210 | }; 211 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catbox-mongodb", 3 | "description": "MongoDB adapter for catbox", 4 | "version": "4.2.1", 5 | "author": "Eran Hammer (http://hueniverse.com)", 6 | "contributors": [ 7 | "Marcus Poehls (https://futurestud.io)", 8 | "Wyatt Preul (http://jsgeek.com)", 9 | "Jarda Kotesovec " 10 | ], 11 | "repository": "git://github.com/hapijs/catbox-mongodb", 12 | "main": "lib/index.js", 13 | "keywords": [ 14 | "cache", 15 | "catbox", 16 | "mongodb" 17 | ], 18 | "engines": { 19 | "node": ">=8.9.0" 20 | }, 21 | "dependencies": { 22 | "boom": "7.x.x", 23 | "hoek": "6.x.x", 24 | "mongodb": "3.x.x" 25 | }, 26 | "devDependencies": { 27 | "catbox": "10.x.x", 28 | "code": "5.x.x", 29 | "lab": "18.x.x" 30 | }, 31 | "scripts": { 32 | "test": "lab -r console -t 100 -a code -L", 33 | "test-cov-html": "lab -r html -o coverage.html -a code -L" 34 | }, 35 | "license": "BSD-3-Clause" 36 | } 37 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Code = require('code'); 6 | const Lab = require('lab'); 7 | const Catbox = require('catbox'); 8 | const Mongo = require('../lib'); 9 | const Mongodb = require('mongodb'); 10 | 11 | 12 | // Test shortcuts 13 | 14 | const lab = exports.lab = Lab.script(); 15 | const describe = lab.experiment; 16 | const it = lab.test; 17 | const after = lab.after; 18 | const before = lab.before; 19 | const expect = Code.expect; 20 | 21 | 22 | describe('Mongo', () => { 23 | 24 | before(async () => { 25 | 26 | const client = await Mongodb.MongoClient.connect('mongodb://localhost:27017/unit-testing', { 27 | autoReconnect: false, 28 | poolSize: 4, 29 | useNewUrlParser: true 30 | }); 31 | 32 | const db = client.db(); 33 | await db.dropDatabase(); 34 | await db.addUser('tester', 'secret', { 35 | roles: ['dbAdmin'] 36 | }); 37 | await client.close(); 38 | }); 39 | 40 | after(async () => { 41 | 42 | const client = await Mongodb.MongoClient.connect('mongodb://localhost:27017/unit-testing', { 43 | autoReconnect: false, 44 | poolSize: 4, 45 | useNewUrlParser: true 46 | }); 47 | 48 | const db = client.db(); 49 | await db.dropDatabase(); 50 | await db.removeUser('tester'); 51 | await client.close(); 52 | }); 53 | 54 | it('creates a new connection', async () => { 55 | 56 | const client = new Catbox.Client(Mongo); 57 | await client.start(); 58 | expect(client.isReady()).to.equal(true); 59 | }); 60 | 61 | it('closes the connection', async () => { 62 | 63 | const client = new Catbox.Client(Mongo); 64 | await client.start(); 65 | expect(client.isReady()).to.equal(true); 66 | 67 | client.stop(); 68 | expect(client.isReady()).to.equal(false); 69 | }); 70 | 71 | it('gets an item after setting it', async () => { 72 | 73 | const client = new Catbox.Client(Mongo); 74 | await client.start(); 75 | 76 | const key = { id: 'x', segment: 'test' }; 77 | await client.set(key, '123', 500); 78 | const result = await client.get(key); 79 | 80 | expect(result.item).to.equal('123'); 81 | }); 82 | 83 | it('gets a falsy value like int 0', async () => { 84 | 85 | const client = new Catbox.Client(Mongo); 86 | await client.start(); 87 | 88 | const key = { id: 'falsy', segment: 'test' }; 89 | await client.set(key, 0, 20); 90 | const result = await client.get(key); 91 | 92 | expect(result.item).to.equal(0); 93 | }); 94 | 95 | it('sets/gets following JS data types: Object, Array, Number, String, Date, RegExp', async () => { 96 | 97 | const client = new Catbox.Client(Mongo); 98 | await client.start(); 99 | 100 | const key = { id: 'x', segment: 'test' }; 101 | const value = { 102 | object: { a: 'b' }, 103 | array: [1, 2, 3], 104 | number: 5.85, 105 | string: 'hapi', 106 | date: new Date('2014-03-07'), 107 | regexp: /[a-zA-Z]+/, 108 | boolean: false 109 | }; 110 | 111 | await client.set(key, value, 500); 112 | const result = await client.get(key); 113 | 114 | expect(result.item).to.equal(value); 115 | }); 116 | 117 | it('fails setting an item circular references', async () => { 118 | 119 | const client = new Catbox.Client(Mongo); 120 | await client.start(); 121 | 122 | const key = { id: 'x', segment: 'test' }; 123 | const value = { a: 1 }; 124 | value.b = value; 125 | 126 | await expect(client.set(key, value, 10)).to.reject(); 127 | }); 128 | 129 | it('ignored starting a connection twice on same event', () => { 130 | 131 | const client = new Catbox.Client(Mongo); 132 | const start = async function () { 133 | 134 | await client.start(); 135 | expect(client.isReady()).to.equal(true); 136 | }; 137 | 138 | start(); 139 | start(); 140 | }); 141 | 142 | it('ignored starting a connection twice chained', async () => { 143 | 144 | const client = new Catbox.Client(Mongo); 145 | await client.start(); 146 | expect(client.isReady()).to.equal(true); 147 | 148 | await client.start(); 149 | expect(client.isReady()).to.equal(true); 150 | }); 151 | 152 | it('connects successfully after a failed connect attempt', { timeout: 4000 }, async () => { 153 | 154 | const options = { 155 | uri: 'mongodb://wrong-uri', 156 | partition: 'unit-testing' 157 | }; 158 | 159 | const client = new Mongo(options); 160 | 161 | try { 162 | await client.start(); 163 | } 164 | catch (err) { 165 | expect(err).to.exist(); 166 | expect(err.message).to.include('getaddrinfo ENOTFOUND wrong-uri'); 167 | } 168 | 169 | client.settings.uri = 'mongodb://127.0.0.1:27017/unit-testing?maxPoolSize=5'; 170 | await client.start(); 171 | expect(client.isReady()).to.equal(true); 172 | }); 173 | 174 | it('returns not found on get when using null key', async () => { 175 | 176 | const client = new Catbox.Client(Mongo); 177 | await client.start(); 178 | 179 | const result = await client.get(null); 180 | expect(result).to.equal(null); 181 | }); 182 | 183 | it('returns not found on get when item expired', async () => { 184 | 185 | const client = new Catbox.Client(Mongo); 186 | await client.start(); 187 | 188 | const key = { id: 'x', segment: 'test' }; 189 | 190 | await client.set(key, 'x', 1); 191 | await new Promise((resolve) => { 192 | 193 | setTimeout(async () => { 194 | 195 | const result = await client.get(key); 196 | expect(result).to.equal(null); 197 | resolve(); 198 | }, 2); 199 | }); 200 | }); 201 | 202 | it('returns error on set when using null key', async () => { 203 | 204 | const client = new Catbox.Client(Mongo); 205 | await client.start(); 206 | 207 | await expect(client.set(null, {}, 1000)).to.reject(); 208 | }); 209 | 210 | it('returns error on get when using invalid key', async () => { 211 | 212 | const client = new Catbox.Client(Mongo); 213 | await client.start(); 214 | 215 | await expect(client.get({})).to.reject(); 216 | }); 217 | 218 | it('returns error on drop when using invalid key', async () => { 219 | 220 | const client = new Catbox.Client(Mongo); 221 | await client.start(); 222 | 223 | await expect(client.drop({})).to.reject(); 224 | }); 225 | 226 | it('returns error on set when using invalid key', async () => { 227 | 228 | const client = new Catbox.Client(Mongo); 229 | await client.start(); 230 | 231 | await expect(client.set({}, {}, 1000)).to.reject(); 232 | }); 233 | 234 | it('ignores set when using non-positive ttl value', async () => { 235 | 236 | const client = new Catbox.Client(Mongo); 237 | await client.start(); 238 | 239 | const key = { id: 'x', segment: 'test' }; 240 | await client.set(key, 'y', 0); 241 | }); 242 | 243 | it('returns error on drop when using null key', async () => { 244 | 245 | const client = new Catbox.Client(Mongo); 246 | await client.start(); 247 | 248 | await expect(client.drop(null)).to.reject(); 249 | }); 250 | 251 | it('returns error on get when stopped', async () => { 252 | 253 | const client = new Catbox.Client(Mongo); 254 | client.stop(); 255 | const key = { id: 'x', segment: 'test' }; 256 | 257 | await expect(client.get(key)).to.reject(); 258 | }); 259 | 260 | it('returns error on set when stopped', async () => { 261 | 262 | const client = new Catbox.Client(Mongo); 263 | client.stop(); 264 | const key = { id: 'x', segment: 'test' }; 265 | 266 | await expect(client.set(key, 'y', 1)).to.reject(); 267 | }); 268 | 269 | it('returns error on drop when stopped', async () => { 270 | 271 | const client = new Catbox.Client(Mongo); 272 | client.stop(); 273 | const key = { id: 'x', segment: 'test' }; 274 | 275 | await expect(client.drop(key)).to.reject(); 276 | }); 277 | 278 | it('returns error on missing segment name', () => { 279 | 280 | const config = { 281 | expiresIn: 50000 282 | }; 283 | 284 | const fn = () => { 285 | 286 | const client = new Catbox.Client(Mongo); 287 | new Catbox.Policy(config, client, ''); 288 | }; 289 | 290 | expect(fn).to.throw(Error); 291 | }); 292 | 293 | it('returns error on bad segment name', () => { 294 | 295 | const config = { 296 | expiresIn: 50000 297 | }; 298 | 299 | const fn = () => { 300 | 301 | const client = new Catbox.Client(Mongo); 302 | new Catbox.Policy(config, client, 'a\0b'); 303 | }; 304 | 305 | expect(fn).to.throw(Error); 306 | }); 307 | 308 | it('returns error when cache item dropped while stopped', async () => { 309 | 310 | const client = new Catbox.Client(Mongo); 311 | client.stop(); 312 | 313 | await expect(client.drop('a')).to.reject(); 314 | }); 315 | 316 | it('throws an error if not created with new', () => { 317 | 318 | const fn = () => { 319 | 320 | Mongo(); 321 | }; 322 | 323 | expect(fn).to.throw(Error); 324 | }); 325 | 326 | it('throws an error when using a reserved partition name (admin)', () => { 327 | 328 | const fn = () => { 329 | 330 | const options = { 331 | partition: 'admin' 332 | }; 333 | 334 | new Mongo(options); 335 | }; 336 | 337 | expect(fn).to.throw(Error, 'Cache partition name cannot be "admin", "local", or "config" when using MongoDB'); 338 | }); 339 | 340 | it('throws an error when using a reserved partition name (local)', () => { 341 | 342 | const fn = () => { 343 | 344 | const options = { 345 | partition: 'local' 346 | }; 347 | 348 | new Mongo(options); 349 | }; 350 | 351 | expect(fn).to.throw(Error, 'Cache partition name cannot be "admin", "local", or "config" when using MongoDB'); 352 | }); 353 | 354 | describe('getSettings', () => { 355 | 356 | it('parse single host connection string without db', () => { 357 | 358 | const options = { 359 | uri: 'mongodb://bob:password@127.0.0.1:27017', 360 | partition: 'unit-testing' 361 | }; 362 | 363 | const mongo = new Mongo(options); 364 | const settings = mongo.getSettings(options); 365 | 366 | expect(settings.uri).to.equal('mongodb://bob:password@127.0.0.1:27017/unit-testing'); 367 | }); 368 | 369 | it('parse single host connection string without db with slash', () => { 370 | 371 | const options = { 372 | uri: 'mongodb://bob:password@127.0.0.1:27017/', 373 | partition: 'unit-testing' 374 | }; 375 | 376 | const mongo = new Mongo(options); 377 | const settings = mongo.getSettings(options); 378 | 379 | expect(settings.uri).to.equal('mongodb://bob:password@127.0.0.1:27017/unit-testing'); 380 | }); 381 | 382 | it('parse single host connection string with credentials', () => { 383 | 384 | const options = { 385 | uri: 'mongodb://bob:password@127.0.0.1:27017/?maxPoolSize=5', 386 | partition: 'unit-testing' 387 | }; 388 | 389 | const mongo = new Mongo(options); 390 | const settings = mongo.getSettings(options); 391 | 392 | expect(settings.uri).to.equal('mongodb://bob:password@127.0.0.1:27017/unit-testing?maxPoolSize=5'); 393 | }); 394 | 395 | it('parse single host connection string without credentials', () => { 396 | 397 | const options = { 398 | uri: 'mongodb://127.0.0.1:27017/test?maxPoolSize=5', 399 | partition: 'unit-testing' 400 | }; 401 | 402 | const mongo = new Mongo(options); 403 | const settings = mongo.getSettings(options); 404 | 405 | expect(settings.uri).to.equal('mongodb://127.0.0.1:27017/unit-testing?maxPoolSize=5'); 406 | }); 407 | 408 | it('parse replica set in connection string without database', () => { 409 | 410 | const options = { 411 | uri: 'mongodb://bob:password@127.0.0.1:27017,127.0.0.2:27017,127.0.0.3:27017', 412 | partition: 'unit-testing' 413 | }; 414 | 415 | const mongo = new Mongo(options); 416 | const settings = mongo.getSettings(options); 417 | 418 | expect(settings.uri).to.equal('mongodb://bob:password@127.0.0.1:27017,127.0.0.2:27017,127.0.0.3:27017/unit-testing'); 419 | }); 420 | 421 | it('parse replica set in connection string without database 2', () => { 422 | 423 | const options = { 424 | uri: 'mongodb://bob:password@127.0.0.1:27017,127.0.0.2:27017,127.0.0.3:27017/', 425 | partition: 'unit-testing' 426 | }; 427 | 428 | const mongo = new Mongo(options); 429 | const settings = mongo.getSettings(options); 430 | 431 | expect(settings.uri).to.equal('mongodb://bob:password@127.0.0.1:27017,127.0.0.2:27017,127.0.0.3:27017/unit-testing'); 432 | }); 433 | 434 | it('parse replica set in connection string with database', () => { 435 | 436 | const options = { 437 | uri: 'mongodb://bob:password@127.0.0.1:27017,127.0.0.2:27017,127.0.0.3:27017/test', 438 | partition: 'unit-testing' 439 | }; 440 | 441 | const mongo = new Mongo(options); 442 | const settings = mongo.getSettings(options); 443 | 444 | expect(settings.uri).to.equal('mongodb://bob:password@127.0.0.1:27017,127.0.0.2:27017,127.0.0.3:27017/unit-testing'); 445 | }); 446 | 447 | it('parse replica set in connection string', () => { 448 | 449 | const options = { 450 | uri: 'mongodb://bob:password@127.0.0.1:27017,127.0.0.2:27017,127.0.0.3:27017/?maxPoolSize=5&replicaSet=rs', 451 | partition: 'unit-testing' 452 | }; 453 | 454 | const mongo = new Mongo(options); 455 | const settings = mongo.getSettings(options); 456 | 457 | expect(settings.uri).to.equal('mongodb://bob:password@127.0.0.1:27017,127.0.0.2:27017,127.0.0.3:27017/unit-testing?maxPoolSize=5&replicaSet=rs'); 458 | }); 459 | 460 | }); 461 | 462 | describe('start()', () => { 463 | 464 | it('returns a rejected promise when authentication fails', async () => { 465 | 466 | const options = { 467 | uri: 'mongodb://bob:password@127.0.0.1:27017/?maxPoolSize=5', 468 | partition: 'unit-testing' 469 | }; 470 | 471 | const mongo = new Mongo(options); 472 | 473 | await expect(mongo.start()).to.reject(); 474 | }); 475 | 476 | it('connects with authentication', async () => { 477 | 478 | const options = { 479 | uri: 'mongodb://tester:secret@127.0.0.1:27017/?maxPoolSize=5', 480 | partition: 'unit-testing' 481 | }; 482 | const mongo = new Mongo(options); 483 | 484 | await mongo.start(); 485 | }); 486 | 487 | it('sets isReady to true when the connection succeeds', async () => { 488 | 489 | const options = { 490 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 491 | partition: 'unit-testing' 492 | }; 493 | const mongo = new Mongo(options); 494 | 495 | await mongo.start(); 496 | expect(mongo.isReady()).to.be.true(); 497 | }); 498 | 499 | it('resolves all pending promises waiting for a start', async () => { 500 | 501 | const options = { 502 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 503 | partition: 'unit-testing' 504 | }; 505 | const mongo = new Mongo(options); 506 | 507 | mongo.start(); 508 | await mongo.start(); 509 | expect(mongo.isReady()).to.be.true(); 510 | }); 511 | }); 512 | 513 | describe('validateSegmentName()', () => { 514 | 515 | it('returns an error when the name is empty', () => { 516 | 517 | const options = { 518 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 519 | partition: 'unit-testing' 520 | }; 521 | 522 | const mongo = new Mongo(options); 523 | 524 | expect(() => mongo.validateSegmentName('')).to.throw('Empty string'); 525 | }); 526 | 527 | it('returns an error when the name has a null character', () => { 528 | 529 | const options = { 530 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 531 | partition: 'unit-testing' 532 | }; 533 | 534 | const mongo = new Mongo(options); 535 | 536 | expect(() => mongo.validateSegmentName('\0test')).to.throw(); 537 | }); 538 | 539 | it('returns an error when the name starts with system.', () => { 540 | 541 | const options = { 542 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 543 | partition: 'unit-testing' 544 | }; 545 | 546 | const mongo = new Mongo(options); 547 | 548 | expect(() => mongo.validateSegmentName('system.')).to.throw('Begins with "system."'); 549 | }); 550 | 551 | it('returns an error when the name has a $ character', () => { 552 | 553 | const options = { 554 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 555 | partition: 'unit-testing' 556 | }; 557 | 558 | const mongo = new Mongo(options); 559 | 560 | expect(() => mongo.validateSegmentName('te$t')).to.throw('Contains "$"'); 561 | }); 562 | 563 | it('returns an error when the name is too long', () => { 564 | 565 | const options = { 566 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 567 | partition: 'unit-testing' 568 | }; 569 | 570 | const mongo = new Mongo(options); 571 | 572 | expect(() => mongo.validateSegmentName('0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789')).to.throw(); 573 | }); 574 | 575 | it('returns null when the name is valid', () => { 576 | 577 | const options = { 578 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 579 | partition: 'unit-testing' 580 | }; 581 | 582 | const mongo = new Mongo(options); 583 | 584 | expect(mongo.validateSegmentName('valid')).to.equal(null); 585 | }); 586 | }); 587 | 588 | describe('getCollection()', () => { 589 | 590 | it('returns a rejected promise when the connection is closed', async () => { 591 | 592 | const options = { 593 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 594 | partition: 'unit-testing' 595 | }; 596 | 597 | const mongo = new Mongo(options); 598 | 599 | await expect(mongo.getCollection('test')).to.reject('Connection not ready'); 600 | }); 601 | 602 | it('returns a collection', async () => { 603 | 604 | const options = { 605 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 606 | partition: 'unit-testing' 607 | }; 608 | const mongo = new Mongo(options); 609 | 610 | await mongo.start(); 611 | const result = mongo.getCollection('test'); 612 | expect(result).to.exist(); 613 | }); 614 | 615 | it('returns a rejected promise when there is an error getting the collection', async () => { 616 | 617 | const options = { 618 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 619 | partition: 'unit-testing' 620 | }; 621 | 622 | const mongo = new Mongo(options); 623 | await mongo.start(); 624 | 625 | await expect(mongo.getCollection('')).to.reject(); 626 | }); 627 | 628 | it('returns a rejected promise when ensureIndex fails', async () => { 629 | 630 | const options = { 631 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 632 | partition: 'unit-testing' 633 | }; 634 | const mongo = new Mongo(options); 635 | 636 | await mongo.start(); 637 | 638 | mongo.db.collection = (item) => { 639 | 640 | return Promise.resolve({ 641 | ensureIndex: (fieldOrSpec, options2) => { 642 | 643 | return Promise.reject(new Error('test')); 644 | } 645 | }); 646 | }; 647 | 648 | await expect(mongo.getCollection('testcollection')).to.reject(); 649 | }); 650 | }); 651 | 652 | describe('get()', () => { 653 | 654 | it('returns a rejected promise when the connection is closed', async () => { 655 | 656 | const options = { 657 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 658 | partition: 'unit-testing' 659 | }; 660 | 661 | const mongo = new Mongo(options); 662 | 663 | await expect(mongo.get('test')).to.reject('Connection not started'); 664 | }); 665 | 666 | it('returns a null item when it doesn\'t exist', async () => { 667 | 668 | const options = { 669 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 670 | partition: 'unit-testing' 671 | }; 672 | 673 | const mongo = new Mongo(options); 674 | 675 | await mongo.start(); 676 | const result = await mongo.get({ segment: 'test0', id: 'test0' }); 677 | 678 | expect(result).to.equal(null); 679 | }); 680 | 681 | it('is able to retrieve an object thats stored when connection is started', async () => { 682 | 683 | const options = { 684 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 685 | partition: 'unit-testing' 686 | }; 687 | 688 | const key = { 689 | id: 'test', 690 | segment: 'test' 691 | }; 692 | 693 | const mongo = new Mongo(options); 694 | 695 | await mongo.start(); 696 | await mongo.set(key, 'myvalue', 200); 697 | const result = await mongo.get(key); 698 | 699 | expect(result.item).to.equal('myvalue'); 700 | }); 701 | 702 | it('returns a rejected promise when there is an error returned from getting an item', async () => { 703 | 704 | const options = { 705 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 706 | partition: 'unit-testing' 707 | }; 708 | 709 | const key = { 710 | id: 'testerr', 711 | segment: 'testerr' 712 | }; 713 | 714 | const mongo = new Mongo(options); 715 | mongo.isConnectionStarted = true; 716 | mongo.isConnected = true; 717 | 718 | mongo.collections.testerr = { 719 | findOne: (item) => { 720 | 721 | return Promise.reject(new Error('test')); 722 | } 723 | }; 724 | 725 | await expect(mongo.get(key)).to.reject('test'); 726 | }); 727 | 728 | it('returns a rejected promise when there is an issue with the record structure', async () => { 729 | 730 | const options = { 731 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 732 | partition: 'unit-testing' 733 | }; 734 | 735 | const key = { 736 | id: 'testerr', 737 | segment: 'testerr' 738 | }; 739 | 740 | const mongo = new Mongo(options); 741 | mongo.isConnectionStarted = true; 742 | mongo.isConnected = true; 743 | 744 | mongo.collections.testerr = { 745 | findOne: (item) => { 746 | 747 | return Promise.resolve({ stored: null }); 748 | } 749 | }; 750 | 751 | await expect(mongo.get(key)).to.reject('Incorrect record structure'); 752 | }); 753 | 754 | it('returns a rejected promise when not yet connected to MongoDB', async () => { 755 | 756 | const options = { 757 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 758 | partition: 'unit-testing' 759 | }; 760 | 761 | const key = { 762 | id: 'testerr', 763 | segment: 'testerr' 764 | }; 765 | 766 | const mongo = new Mongo(options); 767 | mongo.isConnectionStarted = true; 768 | mongo.isConnected = false; 769 | 770 | mongo.collections.testerr = { 771 | findOne: (item) => { 772 | 773 | return Promise.resolve({ value: false }); 774 | } 775 | }; 776 | 777 | await expect(mongo.get(key)).to.reject('Connection not ready'); 778 | }); 779 | }); 780 | 781 | describe('set()', () => { 782 | 783 | it('returns a rejected promise when the connection is closed', async () => { 784 | 785 | const options = { 786 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 787 | partition: 'unit-testing' 788 | }; 789 | 790 | const mongo = new Mongo(options); 791 | 792 | await expect(mongo.set({ id: 'test1', segment: 'test1' }, 'test1', 3600)).to.reject('Connection not started'); 793 | }); 794 | 795 | it('doesn\'t return an error when the set succeeds', async () => { 796 | 797 | const options = { 798 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 799 | partition: 'unit-testing' 800 | }; 801 | 802 | const mongo = new Mongo(options); 803 | await mongo.start(); 804 | const result = await mongo.set({ id: 'test1', segment: 'test1' }, 'test1', 3600); 805 | 806 | expect(result).to.not.exist(); 807 | }); 808 | 809 | it('returns a rejected promise when there is an error returned from setting an item', async () => { 810 | 811 | const options = { 812 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 813 | partition: 'unit-testing' 814 | }; 815 | 816 | const key = { 817 | id: 'testerr', 818 | segment: 'testerr' 819 | }; 820 | 821 | const mongo = new Mongo(options); 822 | mongo.isConnectionStarted = true; 823 | mongo.isConnected = true; 824 | 825 | mongo.getCollection = (item) => { 826 | 827 | return Promise.reject(new Error('test')); 828 | }; 829 | 830 | await expect(mongo.set(key, true, 0)).to.reject('test'); 831 | }); 832 | 833 | it('returns a rejected promise promise when there is an error returned from calling update', async () => { 834 | 835 | const options = { 836 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 837 | partition: 'unit-testing' 838 | }; 839 | 840 | const key = { 841 | id: 'testerr', 842 | segment: 'testerr' 843 | }; 844 | 845 | const mongo = new Mongo(options); 846 | mongo.isConnectionStarted = true; 847 | mongo.isConnected = true; 848 | 849 | mongo.getCollection = (item) => { 850 | 851 | return Promise.resolve({ 852 | updateOne: (criteria, record, options2) => { 853 | 854 | return Promise.reject(new Error('test')); 855 | } 856 | }); 857 | }; 858 | 859 | await expect(mongo.set(key, true, 0)).to.reject('test'); 860 | }); 861 | }); 862 | 863 | describe('drop()', () => { 864 | 865 | it('returns a rejected promise when the connection is closed', async () => { 866 | 867 | const options = { 868 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 869 | partition: 'unit-testing' 870 | }; 871 | 872 | const mongo = new Mongo(options); 873 | 874 | await expect(mongo.drop({ id: 'test2', segment: 'test2' })).to.reject('Connection not started'); 875 | }); 876 | 877 | it('doesn\'t return an error when the drop succeeds', async () => { 878 | 879 | const options = { 880 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 881 | partition: 'unit-testing' 882 | }; 883 | 884 | const mongo = new Mongo(options); 885 | 886 | await mongo.start(); 887 | const result = await mongo.drop({ id: 'test2', segment: 'test2' }); 888 | 889 | expect(result).to.not.exist(); 890 | }); 891 | 892 | it('returns a rejected promise when there is an error returned from dropping an item', async () => { 893 | 894 | const options = { 895 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 896 | partition: 'unit-testing' 897 | }; 898 | const key = { 899 | id: 'testerr', 900 | segment: 'testerr' 901 | }; 902 | const mongo = new Mongo(options); 903 | mongo.isConnectionStarted = true; 904 | mongo.isConnected = true; 905 | 906 | mongo.getCollection = (item) => { 907 | 908 | return Promise.reject(new Error('test')); 909 | }; 910 | 911 | await expect(mongo.drop(key)).to.reject('test'); 912 | }); 913 | 914 | it('returns a rejected promise when there is an error returned from calling remove', async () => { 915 | 916 | const options = { 917 | uri: 'mongodb://127.0.0.1:27017/?maxPoolSize=5', 918 | partition: 'unit-testing' 919 | }; 920 | const key = { 921 | id: 'testerr', 922 | segment: 'testerr' 923 | }; 924 | const mongo = new Mongo(options); 925 | mongo.isConnectionStarted = true; 926 | 927 | mongo.getCollection = (item) => { 928 | 929 | return Promise.resolve({ 930 | deleteOne: (criteria, safe) => { 931 | 932 | return Promise.reject(new Error('test')); 933 | } 934 | }); 935 | }; 936 | 937 | await expect(mongo.drop(key)).to.reject('test'); 938 | }); 939 | }); 940 | }); 941 | --------------------------------------------------------------------------------