From cf7bc3348594bd81a116643751463d9b2b3bc827 Mon Sep 17 00:00:00 2001 From: brick Date: Mon, 11 Dec 2017 13:50:41 +0800 Subject: [PATCH] feat: support replica set uri --- README.md | 58 ++++++++++-- README.zh_CN.md | 61 +++++++++--- app.js | 14 +-- lib/mongo.js | 79 ++++++++++++---- .../apps/mongo-test/app/controller/home.js | 92 ------------------- test/fixtures/apps/mongo-test/app/router.js | 9 -- .../apps/mongo-test/config/config.unittest.js | 8 +- test/multi-instance.test.js | 66 +++++++++++++ 8 files changed, 234 insertions(+), 153 deletions(-) delete mode 100644 test/fixtures/apps/mongo-test/app/controller/home.js delete mode 100644 test/fixtures/apps/mongo-test/app/router.js create mode 100644 test/multi-instance.test.js diff --git a/README.md b/README.md index 273a3ba..c1b8218 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -[![NPM version][npm-image]][npm-url] -[![build status][travis-image]][travis-url] +[![NPM version][npm-image]][npm-url] [![build status][travis-image]][travis-url] [![Test coverage][codecov-image]][codecov-url] [![David deps][david-image]][david-url] [![Known Vulnerabilities][snyk-image]][snyk-url] @@ -20,9 +19,12 @@ [**中文版**](https://github.com/brickyang/egg-mongo/blob/master/README.zh_CN.md) -This plugin base on [node-mongodb-native](https://github.com/mongodb/node-mongodb-native), provides the official MongoDB native driver and APIs. +This plugin base on +[node-mongodb-native](https://github.com/mongodb/node-mongodb-native), provides +the official MongoDB native driver and APIs. -It wraps some frequently-used API to make it easy to use but keep all properties as it is. For example, to find a document you need this with official API +It wraps some frequently-used API to make it easy to use but keep all properties +as it is. For example, to find a document you need this with official API ```js db @@ -59,13 +61,45 @@ exports.mongo = { ## Configuration +### Single Instance + ```js // {app_root}/config/config.default.js exports.mongo = { client: { - host: 'localhost', - port: 27017, + host: 'host', + port: 'port', name: 'test', + user: 'user', + password: 'password', + }, +}; +``` + +### Replica Set (v2.1.0 or higher) + +```js +// mongodb://host1:port1,host2:port2/name?replicaSet=test +exports.mongo = { + client: { + host: 'host1, host2', + port: 'port1, port2', + name: 'name', + option: { + replicaSet: 'test', + }, + }, +}; + +// mongodb://host:port1,host:port2/name?replicaSet=test +exports.mongo = { + client: { + host: 'host', // or ['host'] + port: 'port1, port2', // or ['port1', 'port2'] + name: 'name', + option: { + replicaSet: 'test', + }, }, }; ``` @@ -74,7 +108,9 @@ see [config/config.default.js](config/config.default.js) for more detail. ## Example -The APIs provided by plugin usually need two arguments. The first is commonly the collection name, and the second is an object keeps the arguments of official API. For example, to insert one document with official API +The APIs provided by plugin usually need two arguments. The first is commonly +the collection name, and the second is an object keeps the arguments of official +API. For example, to insert one document with official API ```js db.collection('name').insertOne(doc, options); @@ -108,7 +144,9 @@ listCollection(); createCollection(); ``` -You can always use `app.mongo.db` to call all official APIs. You can check the APIs here: [Node.js MongoDB Driver API](http://mongodb.github.io/node-mongodb-native/2.2/api/). +You can always use `app.mongo.db` to call all official APIs. You can check the +APIs here: +[Node.js MongoDB Driver API](http://mongodb.github.io/node-mongodb-native/2.2/api/). ## Promise @@ -140,7 +178,9 @@ async function create(doc) { } ``` -If you use `app.mongo.db` you could use callback(usually the last argument), but this plugin doesn't supports callback because Promise and async/await are better choice. +If you use `app.mongo.db` you could use callback(usually the last argument), but +this plugin doesn't supports callback because Promise and async/await are better +choice. ## License diff --git a/README.zh_CN.md b/README.zh_CN.md index 76710af..3a718c3 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -1,5 +1,4 @@ -[![NPM version][npm-image]][npm-url] -[![build status][travis-image]][travis-url] +[![NPM version][npm-image]][npm-url] [![build status][travis-image]][travis-url] [![Test coverage][codecov-image]][codecov-url] [![David deps][david-image]][david-url] [![Known Vulnerabilities][snyk-image]][snyk-url] @@ -20,9 +19,12 @@ [**English**](https://github.com/brickyang/egg-mongo/blob/master/README.md) -本插件基于 [node-mongodb-native](https://github.com/mongodb/node-mongodb-native),提供了 MongoDB 官方 driver 及 API。 +本插件基于 +[node-mongodb-native](https://github.com/mongodb/node-mongodb-native),提供了 +MongoDB 官方 driver 及 API。 -插件对一些常用 API 进行了简单封装以简化使用,同时保留了所有原版属性。例如,使用原版 API 进行一次查找需要写 +插件对一些常用 API 进行了简单封装以简化使用,同时保留了所有原版属性。例如,使用 +原版 API 进行一次查找需要写 ```js db @@ -61,13 +63,45 @@ exports.mongo = { ## 配置 -```javascript +### 单实例 + +```js // {app_root}/config/config.default.js exports.mongo = { client: { - host: 'localhost', - port: 27017, + host: 'host', + port: 'port', name: 'test', + user: 'user', + password: 'password', + }, +}; +``` + +### 集群 (v2.1.0 以上 ) + +```js +// mongodb://host1:port1,host2:port2/name?replicaSet=test +exports.mongo = { + client: { + host: 'host1, host2', + port: 'port1, port2', + name: 'name', + option: { + replicaSet: 'test', + }, + }, +}; + +// mongodb://host:port1,host:port2/name?replicaSet=test +exports.mongo = { + client: { + host: 'host', // or ['host'] + port: 'port1, port2', // or ['port1', 'port2'] + name: 'name', + option: { + replicaSet: 'test', + }, }, }; ``` @@ -76,7 +110,9 @@ exports.mongo = { ## 使用示例 -本插件提供的 API 只是对原版 API 进行了必要的简化,所有属性名称与原版 API 一致。所有针对文档操作的 API,通常接受 2 个参数,第一个参数是 collection 名称,第二个参数是一个对象,属性名即为原版 API 的所有参数。例如,使用原版 API 进行一次插入 +本插件提供的 API 只是对原版 API 进行了必要的简化,所有属性名称与原版 API 一致。 +所有针对文档操作的 API,通常接受 2 个参数,第一个参数是 collection 名称,第二个 +参数是一个对象,属性名即为原版 API 的所有参数。例如,使用原版 API 进行一次插入 ```js db.collection('name').insertOne(doc, options); @@ -110,11 +146,13 @@ listCollection(); createCollection(); ``` -当然,在任何时候你也都可以使用 `app.mongo.db` 调用所有 API。你可以在这里查看所有 API:[Node.js MongoDB Driver API](http://mongodb.github.io/node-mongodb-native/2.2/api/)。 +当然,在任何时候你也都可以使用 `app.mongo.db` 调用所有 API。你可以在这里查看所有 +API:[Node.js MongoDB Driver API](http://mongodb.github.io/node-mongodb-native/2.2/api/)。 ## 同步与异步 -`node-mongodb-native` 所有 API 都支持 Promise,因此你可以自由地以异步或同步方式使用本插件。 +`node-mongodb-native` 所有 API 都支持 Promise,因此你可以自由地以异步或同步方式 +使用本插件。 ### 异步 @@ -142,7 +180,8 @@ async function create(doc) { } ``` -如果你使用 `app.mongo.db` 调用原版 API,则也可以使用回调函数。插件封装的 API 不支持回调函数,因为 Promise 和 async/await 更加优雅。 +如果你使用 `app.mongo.db` 调用原版 API,则也可以使用回调函数。插件封装的 API 不 +支持回调函数,因为 Promise 和 async/await 更加优雅。 Node.js 7.6 开始已经原生支持 async/await,不再需要 Babel。 diff --git a/app.js b/app.js index f0d226b..08db033 100644 --- a/app.js +++ b/app.js @@ -9,21 +9,11 @@ function createMongo(config, app) { const client = new MongoDB(config); client.on('connect', () => { - app.coreLogger.info( - '[egg-mongo] Connect success on mongo://%s:%s/%s.', - config.host, - config.port, - config.name - ); + app.coreLogger.info(`[egg-mongo] Connect success on ${client.url}.`); }); /* istanbul ignore next */ client.on('error', error => { - app.coreLogger.warn( - '[egg-mongo] Connect fail on mongo://%s:%s/%s.', - config.host, - config.port, - config.name - ); + app.coreLogger.warn(`[egg-mongo] Connect fail on ${client.url}.`); app.coreLogger.error(error); }); diff --git a/lib/mongo.js b/lib/mongo.js index f0dc1ae..b30ea98 100644 --- a/lib/mongo.js +++ b/lib/mongo.js @@ -1,20 +1,53 @@ 'use strict'; const EventEmitter = require('events'); const MongoClient = require('mongodb').MongoClient; +const makeURI = Symbol('makeURI'); class MongoDB extends EventEmitter { constructor(options) { super(); + this.url = this[makeURI](options); + this.db; + } + + [makeURI](options) { let url = 'mongodb://'; /* istanbul ignore if */ if (options.user) { if (options.password) url += `${options.user}:${options.password}@`; else url += `${options.user}@`; } - url += `${options.host}:${options.port}/${options.name}`; - this.url = url; - this.db; + const host = options.host.toString().split(','); + const port = options.port.toString().split(','); + const { option } = options; + + if (host.length > 1 && host.length !== port.length) { + const errMsg = + 'The host and port do not match. Please check your config.'; + return this.emit('error', errMsg); + } + + const hostLen = host.length; + const portLen = port.length; + for (let i = 0; i < portLen; i++) { + let h = ''; + if (hostLen === 1) h = host[0]; + else h = host[i]; + + url += `${h}:${port[i]},`; + } + + url = url.slice(0, -1); + url += `/${options.name}`; + + if (option) { + let uriOp = '?'; + Object.keys(option).forEach(key => (uriOp += `${key}=${option[key]}`)); + url += uriOp; + } + + return url; } connect() { @@ -48,7 +81,9 @@ class MongoDB extends EventEmitter { insertOne(name, args = {}) { const doc = args.doc || {}; const options = args.options || {}; - return this.db.collection(name).insertOne(doc, options) + return this.db + .collection(name) + .insertOne(doc, options) .then(result => { return { insertedCount: result.insertedCount, @@ -80,7 +115,8 @@ class MongoDB extends EventEmitter { const filter = args.filter || {}; const update = args.update || {}; const options = args.options || {}; - options.sort = (args.options && args.options.sort) ? args.options.sort : { _id: -1 }; + options.sort = + args.options && args.options.sort ? args.options.sort : { _id: -1 }; return this.db.collection(name).findOneAndUpdate(filter, update, options); } @@ -106,8 +142,9 @@ class MongoDB extends EventEmitter { findOneAndReplace(name, args = {}) { const filter = args.filter || {}; const replacement = args.replacement || {}; - const options = (args.options) ? args.options : { sort: { _id: 1 } }; - return this.db.collection(name) + const options = args.options ? args.options : { sort: { _id: 1 } }; + return this.db + .collection(name) .findOneAndReplace(filter, replacement, options); } @@ -126,9 +163,8 @@ class MongoDB extends EventEmitter { */ findOneAndDelete(name, args = {}) { const filter = args.filter; - const options = (args.options) ? args.options : { sort: { _id: -1 } }; - return this.db.collection(name) - .findOneAndDelete(filter, options); + const options = args.options ? args.options : { sort: { _id: -1 } }; + return this.db.collection(name).findOneAndDelete(filter, options); } /** @@ -151,7 +187,9 @@ class MongoDB extends EventEmitter { insertMany(name, args) { const docs = args.docs; const options = args.options; - return this.db.collection(name).insertMany(docs, options) + return this.db + .collection(name) + .insertMany(docs, options) .then(result => { return { insertedCount: result.insertedCount, @@ -183,7 +221,9 @@ class MongoDB extends EventEmitter { const filter = args.filter || {}; const update = args.update || {}; const options = args.options || {}; - return this.db.collection(name).updateMany(filter, update, options) + return this.db + .collection(name) + .updateMany(filter, update, options) .then(result => { return { matchedCount: result.matchedCount, @@ -207,7 +247,9 @@ class MongoDB extends EventEmitter { deleteMany(name, args = {}) { const filter = args.filter || {}; const options = args.options || {}; - return this.db.collection(name).deleteMany(filter, options) + return this.db + .collection(name) + .deleteMany(filter, options) .then(result => result.deletedCount); } @@ -229,7 +271,8 @@ class MongoDB extends EventEmitter { const limit = args.limit || 0; const project = args.project || {}; const sort = args.sort || { _id: -1 }; - return this.db.collection(name) + return this.db + .collection(name) .find(query) .skip(skip) .limit(limit) @@ -302,11 +345,8 @@ class MongoDB extends EventEmitter { * @param {number} [args.options.v] - Specify the format version of the indexes. * @param {number} [args.options.expireAfterSeconds] - Allows you to expire data on indexes applied to a data (MongoDB 2.2 or higher). * @param {number} [args.options.name] - Override the autogenerated index name (useful if the resulting name is larger than 128 bytes). -<<<<<<< HEAD + * @return {Promise} - Promise -======= - * @return {Promise} - Return ->>>>>>> 40af913305ac6bcaa2879cacec80533a432fd4d8 */ createIndex(name, args = {}) { const fieldOrSpec = args.fieldOrSpec; @@ -324,7 +364,8 @@ class MongoDB extends EventEmitter { listCollections(args = {}) { const filter = args.filter || {}; const options = args.options || {}; - return this.db.listCollections(filter, options) + return this.db + .listCollections(filter, options) .toArray() .then(result => result.map(col => col.name)); } diff --git a/test/fixtures/apps/mongo-test/app/controller/home.js b/test/fixtures/apps/mongo-test/app/controller/home.js deleted file mode 100644 index 202a5b1..0000000 --- a/test/fixtures/apps/mongo-test/app/controller/home.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict'; -const { ObjectID } = require('mongodb'); - -module.exports = app => { - class Controller extends app.Controller { - async index() { - const { ctx } = this; - - ctx.body = await app.mongo.find('test', { query: {} }); - ctx.status = 200; - } - - async show() { - const { ctx } = this; - const { id } = ctx.params; - - const [ result ] = await app.mongo.find('test', { query: { _id: new ObjectID(id) } }); - ctx.body = result; - ctx.status = 200; - } - - async create() { - const { ctx } = this; - - ctx.body = await app.mongo.insertOne('test', { doc: ctx.request.body }); - ctx.status = 201; - } - - async update() { - const { ctx } = this; - const { id } = ctx.params; - - const args = { - filter: { _id: new ObjectID(id) }, - update: { $set: ctx.request.body }, - replacement: ctx.request.body, - options: { returnOriginal: false }, - }; - - if (ctx.query.hasOwnProperty('replace')) { - ctx.body = await app.mongo.findOneAndReplace('test', args); - } else { - ctx.body = await app.mongo.findOneAndUpdate('test', args); - } - ctx.status = 200; - } - - async updateMany() { - const { ctx } = this; - - const args = { - update: { $set: { type: 'update' } }, - options: { returnOriginal: false }, - }; - - ctx.body = await app.mongo.updateMany('test', args); - } - - async destroy() { - const { ctx } = this; - const { id } = ctx.params; - - ctx.body = await app.mongo.findOneAndDelete('test', { filter: { _id: new ObjectID(id) } }); - ctx.status = 200; - } - - async count() { - const { ctx } = this; - - ctx.body = await app.mongo.count('test'); - ctx.status = 200; - } - - async collections() { - const { ctx } = this; - - await app.mongo.createCollection({ name: 'new' }); - ctx.body = await app.mongo.listCollections(); - ctx.status = 200; - } - - async distinct() { - const { ctx } = this; - - ctx.body = { - doc: await app.mongo.distinct('test', { key: 'doc' }), - type: await app.mongo.distinct('test', { key: 'type' }), - }; - } - } - return Controller; -}; diff --git a/test/fixtures/apps/mongo-test/app/router.js b/test/fixtures/apps/mongo-test/app/router.js deleted file mode 100644 index d9bc037..0000000 --- a/test/fixtures/apps/mongo-test/app/router.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = app => { - app.resources('home', '/', 'home'); - app.put('/', 'home.updateMany'); - app.get('/total', 'home.count'); - app.get('/collections', 'home.collections'); - app.get('/distinct', 'home.distinct'); -}; diff --git a/test/fixtures/apps/mongo-test/config/config.unittest.js b/test/fixtures/apps/mongo-test/config/config.unittest.js index c997e00..08c1c6b 100644 --- a/test/fixtures/apps/mongo-test/config/config.unittest.js +++ b/test/fixtures/apps/mongo-test/config/config.unittest.js @@ -1,3 +1,9 @@ 'use strict'; -exports.keys = '123456'; +exports.mongo = { + client: { + host: 'localhost', + port: 27017, + name: 'test', + }, +}; diff --git a/test/multi-instance.test.js b/test/multi-instance.test.js new file mode 100644 index 0000000..7bb2a72 --- /dev/null +++ b/test/multi-instance.test.js @@ -0,0 +1,66 @@ +'use strict'; + +const assert = require('assert'); +const MongoDB = require('../lib/mongo'); + +describe('test/multi-instance.test.js', () => { + it('should make replica URI', () => { + const config = { + host: [ 'host1', 'host2' ], + port: [ 'port1', 'port2' ], + name: 'test', + option: { + replicaSet: 'test', + }, + }; + const expect = 'mongodb://host1:port1,host2:port2/test?replicaSet=test'; + const client = new MongoDB(config); + assert.equal(client.url, expect); + }); + + it('should make replica URI with string options', () => { + const config = { + host: 'host1,host2', + port: 'port1,port2', + name: 'test', + option: { + replicaSet: 'test', + }, + }; + const expect = 'mongodb://host1:port1,host2:port2/test?replicaSet=test'; + const client = new MongoDB(config); + assert.equal(client.url, expect); + }); + + it('should make replica URI with one host', () => { + const config = { + host: [ 'host' ], + port: [ 'port1', 'port2' ], + name: 'test', + option: { + replicaSet: 'test', + }, + }; + const expect = 'mongodb://host:port1,host:port2/test?replicaSet=test'; + const client = new MongoDB(config); + assert.equal(client.url, expect); + }); + + it('should throw', async () => { + const config = { + host: [ 'host1', 'host2' ], + port: [ 'port' ], + name: 'test', + option: { + replicaSet: 'test', + }, + }; + const expect = 'The host and port do not match. Please check your config.'; + try { + new MongoDB(config); + } catch (error) { + assert(error); + assert.equal(error.context, expect); + } + }); +});