diff --git a/.gitignore b/.gitignore index 2068c4d4..85f52fc1 100755 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ npm-debug.log coverage .nyc_output + +_book diff --git a/History.md b/History.md index 133c5b5b..fdba2405 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,15 @@ +3.0.0 / 2016-07-06 +================== + - remove Mongoskin dependency + - new documentation using gitbook + - add `opts` arg to `Collection.count` and `collection.distinct` + - deprecate `Collection.removeById`, `Collection.findById`, `Collection.updateById` in favor of using `remove`, `findOne` and `update` directly + - deprecate `collection.id` and `manager.id` in favor of `monk.id` + - `monk('localhost')` can be used as a promise which resolves when the connection opens and rejects when it throws an error (fix #24, fix #10) + - deprecate `Collection.findAndModify` in favor of `Collection.findOneAndDelete` and `Collection.findOneAndUpdate` (fix #74) + - add `Manager.create` (fix #50) + - add option `rawCursor` to `Collection.find` to return the raw cursor (fix #103) + 2.1.0 / 2016-06-24 ================== - add aggregate method (#56) diff --git a/Makefile b/Makefile index 72e2dcde..9256334a 100755 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ BIN_DIR ?= node_modules/.bin +P="\\033[34m[+]\\033[0m" SRC_DIR ?= src -TEST_TARGET ?= tests/ +TEST_TARGET ?= test/ lint: echo " $(P) Linting" @@ -15,4 +16,32 @@ test-watch: echo " $(P) Testing forever" NODE_ENV=test $(BIN_DIR)/ava --watch -.PHONY: lint test test-watch +docs-clean: + echo " $(P) Cleaning gitbook" + rm -rf _book + +docs-prepare: docs-clean + echo " $(P) Preparing gitbook" + $(BIN_DIR)/gitbook install + +docs-build: docs-prepare + echo " $(P) Building gitbook" + $(BIN_DIR)/gitbook build -g Automattic/monk + +docs-watch: docs-prepare + echo " $(P) Watching gitbook" + $(BIN_DIR)/gitbook serve + +docs-publish: docs-build + echo " $(P) Publishing gitbook" + cd _book && \ + git init && \ + git commit --allow-empty -m 'update book' && \ + git checkout -b gh-pages && \ + touch .nojekyll && \ + git add . && \ + git commit -am 'update book' && \ + git push https://github.com/Automattic/monk gh-pages --force + +.PHONY: lint test test-watch docs-clean docs-prepare docs-build docs-watch docs-publish +.SILENT: lint test test-watch docs-clean docs-prepare docs-build docs-watch docs-publish diff --git a/README.md b/README.md index c7cfb24b..a1259c04 100644 --- a/README.md +++ b/README.md @@ -35,195 +35,12 @@ db.close() - Improvements to the MongoDB APIs (eg: `findAndModify` supports the `update` signature style) - Auto-casting of `_id` in queries -- Builds on top of [mongoskin](http://github.com/kissjs/node-mongoskin) - Allows to set global options or collection-level options for queries. (eg: `safe` is `true` by default for all queries) ## How to use -### Connecting - -#### Single server - -```js -const db = require('monk')('localhost/mydb', options) -``` - -#### Replica set - -```js -const db = require('monk')('localhost/mydb,192.168.1.1') -``` - -### Disconnecting - -```js -db.close() -``` - -### Collections - -#### Getting one - -```js -const users = db.get('users') -// users.insert(), users.update() … (see below) -``` - -#### Dropping - -```js -users.drop(fn) -``` - -### Signatures - -- All commands accept the simple `data[, …][, callback]`. For example - - `find({}, fn)` - - `findOne({}, fn)` - - `update({}, {}, fn)` - - `findAndModify({}, {}, fn)` - - `findById('id', fn)` - - `remove({}, fn)` -- You can pass options in the middle: `data[, …], options[, fn]` -- You can pass fields to select as an array: `data[, …], ['field', …][, fn]` -- You can pass fields as a string delimited by spaces: - `data[, …], 'field1 field2'[, fn]` -- To exclude a field, prefix the field name with '-': - `data[, …], '-field1'[, fn]` -- You can pass sort option the same way as fields - -### Promises - -All methods that perform an async action return a promise. - -```js -users.insert({}).then((doc) => { - // success -}).catch((err) => { - // error -}) -``` - -### Indexes - -```js -users.index('name.first') -users.index('email', { unique: true }) // unique -users.index('name.first name.last') // compound -users.index({ 'email': 1, 'password': -1 }) // compound with sort -users.index('email', { sparse: true }) // with options -users.indexes() // get indexes -users.dropIndex(name) // drop an index -users.dropIndexes() // drop all indexes -``` - -### Inserting - -```js -users.insert({ a: 'b' }) -``` - -### Casting - -To cast to `ObjectId`: - -```js -users.id() // returns new generated ObjectID -users.id('hexstring') // returns ObjectId -users.id(obj) // returns ObjectId -``` - -### Updating - -```js -users.update({}, {}) -users.updateById('id', {}) -``` - -### Finding - -#### Many - -```js -users.find({}).then((docs) => {}) -``` - -#### By ID - -```js -users.findById('hex representation').then((doc) => {}) -users.findById(oid).then((doc) => {}) -``` - -#### Single doc - -`findOne` also provides the `findById` functionality. - -```js -users.findOne({ name: 'test' }).then((doc) => {}) -``` - -#### And modify - -```js -users.findAndModify({ query: {}, update: {} }) -users.findAndModify({ _id: '' }, { $set: {} }, { new: true }) -``` - -#### Streaming - -Note: `stream: true` is optional if you register an `each` handler in the -same tick. In the following example I just include it for extra clarity. - -```js -users.find({}, { stream: true }) - .each((doc, destroy) => {}) - .then(() => {}) - .catch((err) => {}) -``` - -##### Destroying a cursor - -You can call `destroy()` in the `each` handler to close the cursor. Upon the cursor -closing the `then` handler will be called. - -### Removing - -```js -users.remove({ a: 'b' }) -``` - -### Aggregate - -```js -users.aggregate(stages, {}) -``` - -### Global options - -```js -const db = require('monk')('localhost/mydb') -db.options.multi = true // global multi-doc update -db.get('users').options.multi = false // collection-level -``` - -Monk sets `safe` to `true` by default. - -### Query debugging - -If you wish to see what queries `monk` passes to the driver, simply leverage -[debug](http://github.com/visionmedia/debug): - -```bash -DEBUG="monk:queries" -``` - -To see all debugging output: - -```bash -DEBUG="monk:*" -``` +[Documentation](https://Automattic.github.io/monk) ## Contributors diff --git a/book.json b/book.json new file mode 100644 index 00000000..6f3ecf1a --- /dev/null +++ b/book.json @@ -0,0 +1,17 @@ +{ + "gitbook": "2.5.2", + "title": "Monk", + "structure": { + "summary": "docs/README.md" + }, + "plugins": ["edit-link", "prism", "-highlight", "github", "anker-enable"], + "pluginsConfig": { + "edit-link": { + "base": "https://github.com/Automattic/monk/tree/master", + "label": "Edit This Page" + }, + "github": { + "url": "https://github.com/Automattic/monk/" + } + } +} diff --git a/docs/Debugging.md b/docs/Debugging.md new file mode 100644 index 00000000..408daa0c --- /dev/null +++ b/docs/Debugging.md @@ -0,0 +1,14 @@ +# Query debugging + +If you wish to see what queries `monk` passes to the driver, simply leverage +[debug](http://github.com/visionmedia/debug): + +```bash +DEBUG="monk:queries" +``` + +To see all debugging output: + +```bash +DEBUG="monk:*" +``` diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 00000000..8d1f02aa --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,174 @@ +# Getting started + +The quick start guide will show you how to setup a simple application using node.js and MongoDB. Its scope is only how to set up the driver and perform the simple crud operations. + +Installing monk +--------------------------- +Use **NPM** to install `monk`. + +``` +npm install --save monk +``` + +Booting up a MongoDB Server +--------------------------- +Let's boot up a MongoDB server instance. Download the right MongoDB version from [MongoDB](http://www.mongodb.org), open a new shell or command line and ensure the **mongod** command is in the shell or command line path. Now let's create a database directory (in our case under **/data**). + +``` +mongod --dbpath=/data --port 27017 +``` + +You should see the **mongod** process start up and print some status information. + +Connecting to MongoDB +--------------------- +Let's create a new **app.js** file that we will use to show the basic CRUD operations using monk. + +First let's add code to connect to the server and the database **myproject**. + +```js +const monk = require('monk') + +// Connection URL +const url = 'localhost:27017/myproject'; + +const db = monk(url); + +db.then(() => { + console.log('Connected correctly to server') +}) +``` + +Given that you booted up the **mongod** process earlier the application should connect successfully and print **Connected correctly to server** to the console. + +If you are not sure what the `then` is or not up to speed with `Promises`, you might want to check out some tutorials first. + +Let's Add some code to show the different CRUD operations available. + +Inserting a Document +-------------------- +Let's insert some documents in the `documents` collection. + +```js +const url = 'localhost:27017/myproject'; // Connection URL +const db = require('monk')(url); + +const collection = db.get('document') + +collection.insert([{a: 1}, {a: 2}, {a: 3}]) + .then((docs) => { + // docs contains the documents inserted with added **_id** fields + // Inserted 3 documents into the document collection + }).catch((err) => { + // An error happened while inserting + }).then(() => db.close()) +``` + +You can notice that we are not waiting for the connection to be opened before doing the operation. That's because behind the scene, monk will queue all the operations until the connection is opened and then send them. + +We can now run the update **app.js** file. + +``` +node app.js +``` + +You should see the following output after running the **app.js** file. + +``` +Inserted 3 documents into the document collection +``` + +Updating a document +------------------- +Let's look at how to do a simple document update by adding a new field **b** to the document that has the field **a** set to **2**. + +```js +const url = 'localhost:27017/myproject'; // Connection URL +const db = require('monk')(url); + +const collection = db.get('document') + +collection.insert([{a: 1}, {a: 2}, {a: 3}]) + .then((docs) => { + // Inserted 3 documents into the document collection + }) + .then(() => { + + return collection.update({ a: 2 }, { $set: { b: 1 } }) + + }) + .then((result) => { + // Updated the document with the field a equal to 2 + }) + .then(() => db.close()) +``` + +The method will update the first document where the field **a** is equal to **2** by adding a new field **b** to the document set to **1**. + +Delete a document +----------------- +Next let's delete the document where the field **a** equals to **3**. + +```js +const url = 'localhost:27017/myproject'; // Connection URL +const db = require('monk')(url); + +const collection = db.get('document') + +collection.insert([{a: 1}, {a: 2}, {a: 3}]) + .then((docs) => { + // Inserted 3 documents into the document collection + }) + .then(() => collection.update({ a: 2 }, { $set: { b: 1 } })) + .then((result) => { + // Updated the document with the field a equal to 2 + }) + .then(() => { + + return collection.remove({ a: 3}) + + }).then((result) => { + // Deleted the document with the field a equal to 3 + }) + .then(() => db.close()) +``` + +This will delete the first document where the field **a** equals to **3**. + +Find All Documents +------------------ +We will finish up the CRUD methods by performing a simple query that returns all the documents matching the query. + +```js +const url = 'localhost:27017/myproject'; // Connection URL +const db = require('monk')(url); + +const collection = db.get('document') + +collection.insert([{a: 1}, {a: 2}, {a: 3}]) + .then((docs) => { + // Inserted 3 documents into the document collection + }) + .then(() => collection.update({ a: 2 }, { $set: { b: 1 } })) + .then((result) => { + // Updated the document with the field a equal to 2 + }) + .then(() => collection.remove({ a: 3})) + .then((result) => { + // Deleted the document with the field a equal to 3 + }) + .then(() => { + + return collection.find() + + }) + .then((docs) => { + // docs === [{ a: 1 }, { a: 2, b: 1 }] + }) + .then(() => db.close()) +``` + +This query will return all the documents in the **documents** collection. Since we deleted a document the total +documents returned is **2**. + +This concludes the Getting Started of connecting and performing some Basic operations using monk. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..64843bf8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,28 @@ +## Table of Contents + +* [Read Me](/README.md) +* [Getting Started](/docs/GETTING_STARTED.md) +* API Reference + * [Manager](/docs/manager/README.md) + * [close](/docs/manager/close.md) + * [create](/docs/manager/create.md) + * [get](/docs/manager/get.md) + * [Collection](/docs/collection/README.md) + * [aggregate](/docs/collection/aggregate.md) + * [count](/docs/collection/count.md) + * [distinct](/docs/collection/distinct.md) + * [drop](/docs/collection/drop.md) + * [dropIndex](/docs/collection/dropIndex.md) + * [dropIndexes](/docs/collection/dropIndexes.md) + * [ensureIndex](/docs/collection/ensureIndex.md) + * [find](/docs/collection/find.md) + * [findOne](/docs/collection/findOne.md) + * [findOneAndDelete](/docs/collection/findOneAndDelete.md) + * [findOneAndUpdate](/docs/collection/findOneAndUpdate.md) + * [indexes](/docs/collection/indexes.md) + * [insert](/docs/collection/insert.md) + * [remove](/docs/collection/remove.md) + * [update](/docs/collection/update.md) + * [id](/docs/id.md) +* [Debugging](/docs/Debugging.md) +* [Change Log](/History.md) diff --git a/docs/collection/README.md b/docs/collection/README.md new file mode 100644 index 00000000..31a1e29d --- /dev/null +++ b/docs/collection/README.md @@ -0,0 +1,35 @@ +# Collection + +Object representing a mongo collection. Create it using [`manager.get`](/docs/manager/get.md). + +A Collection instance has the following methods: + * [aggregate](/docs/collection/aggregate.md) + * [count](/docs/collection/count.md) + * [distinct](/docs/collection/distinct.md) + * [drop](/docs/collection/drop.md) + * [dropIndex](/docs/collection/dropIndex.md) + * [dropIndexes](/docs/collection/dropIndexes.md) + * [ensureIndex](/docs/collection/ensureIndex.md) + * [find](/docs/collection/find.md) + * [findOne](/docs/collection/findOne.md) + * [findOneAndDelete](/docs/collection/findOneAndDelete.md) + * [findOneAndUpdate](/docs/collection/findOneAndUpdate.md) + * [indexes](/docs/collection/indexes.md) + * [insert](/docs/collection/insert.md) + * [remove](/docs/collection/remove.md) + * [update](/docs/collection/update.md) + +#### Example + +```js +const users = db.get('users', options) +``` + +#### Options + +You can set options to pass to every queries of the collection. +```js +users.options = { + safe: true +} +``` diff --git a/docs/collection/aggregate.md b/docs/collection/aggregate.md new file mode 100644 index 00000000..78ceb78b --- /dev/null +++ b/docs/collection/aggregate.md @@ -0,0 +1,33 @@ +# `collection.aggregate` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#aggregate) + +Calculates aggregate values for the data in a collection. + +#### Arguments + +1. `pipeline` *(Array)*: A sequence of data aggregation operations or stages. + +2. [`options`] *(object)* + +3. [`callback`] *(function)* + +#### Returns + +A promise + +#### Example + +```js +users.aggregate([ + { $project : { + author : 1, + tags : 1 + }}, + { $unwind : "$tags" }, + { $group : { + _id : {tags : "$tags"}, + authors : { $addToSet : "$author" } + }} +]).then((res) => {}) +``` diff --git a/docs/collection/count.md b/docs/collection/count.md new file mode 100644 index 00000000..4d1b7fda --- /dev/null +++ b/docs/collection/count.md @@ -0,0 +1,24 @@ +# `collection.count` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#count) + +Returns the count of documents that would match a `find()` query. The `collection.count()` method does not perform the `find()` operation but instead counts and returns the number of results that match a query. + +#### Arguments + +1. `query` *(String|ObjectId|Object)*: The query for the count. + +2. [`options`] *(object)* + +3. [`callback`] *(function)* + +#### Returns + +A promise + +#### Example + +```js +users.count({name: 'foo'}) +users.count('id') // a bit useless but consistent with the rest of the API +``` diff --git a/docs/collection/distinct.md b/docs/collection/distinct.md new file mode 100644 index 00000000..68cf28d6 --- /dev/null +++ b/docs/collection/distinct.md @@ -0,0 +1,25 @@ +# `collection.distinct` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#distinct) + +Finds the distinct values for a specified field across a single collection and returns the results in an array. + +#### Arguments + +1. `field` *(String)*: The field for which to return distinct values. + +2. [`query`] *(String|ObjectId|Object)*: A query that specifies the documents from which to retrieve the distinct values. + +3. [`options`] *(object)* + +4. [`callback`] *(function)* + +#### Returns + +A promise + +#### Example + +```js +users.distinct('name') +``` diff --git a/docs/collection/drop.md b/docs/collection/drop.md new file mode 100644 index 00000000..c4955f48 --- /dev/null +++ b/docs/collection/drop.md @@ -0,0 +1,19 @@ +# `collection.drop` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#drop) + +Drop the collection from the database, removing it permanently. New accesses will create a new collection. + +#### Arguments + +1. [`callback`] *(function)* + +#### Returns + +A promise + +#### Example + +```js +users.drop() +``` diff --git a/docs/collection/dropIndex.md b/docs/collection/dropIndex.md new file mode 100644 index 00000000..529a013d --- /dev/null +++ b/docs/collection/dropIndex.md @@ -0,0 +1,26 @@ +# `collection.dropIndex` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#dropIndex) + +Drops indexes from this collection. + +#### Arguments + +1. `fields` *(String|Object|Array)*: Defines the index (or indexes) to drop. + +2. [`options`] *(object)* + +3. [`callback`] *(function)* + +#### Returns + +A promise + +#### Example + +```js +users.dropIndex('name.first') +users.dropIndex('name last') +users.dropIndex(['nombre', 'apellido']) +users.dropIndex({ up: 1, down: -1 }) +``` diff --git a/docs/collection/dropIndexes.md b/docs/collection/dropIndexes.md new file mode 100644 index 00000000..d80491d7 --- /dev/null +++ b/docs/collection/dropIndexes.md @@ -0,0 +1,19 @@ +# `collection.dropIndex` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#dropIndexes) + +Drops all indexes from this collection. + +#### Arguments + +1. [`callback`] *(function)* + +#### Returns + +A promise + +#### Example + +```js +users.dropIndexex() +``` diff --git a/docs/collection/ensureIndex.md b/docs/collection/ensureIndex.md new file mode 100644 index 00000000..8c16f633 --- /dev/null +++ b/docs/collection/ensureIndex.md @@ -0,0 +1,27 @@ +# `collection.ensureIndex` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#ensureIndex) + +Ensures that indexes exist, if it does not it creates it + +#### Arguments + +1. `fieldOrSpec` *(String|Array|Object)*: Defines the index. + +2. [`options`] *(object)* + +3. [`callback`] *(function)* + +#### Returns + +A promise + +#### Example + +```js +users.index('name.first') +users.index('name last') +users.index(['nombre', 'apellido']) +users.index({ up: 1, down: -1 }) +users.index({ woot: 1 }, { unique: true }) +``` diff --git a/docs/collection/find.md b/docs/collection/find.md new file mode 100644 index 00000000..762b71d8 --- /dev/null +++ b/docs/collection/find.md @@ -0,0 +1,54 @@ +# `collection.find` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#find) + +Selects documents in a collection and return them. + +#### Arguments + +1. `query` *(String|ObjectId|Object)* + +2. [`options`] *(Object|String|Array)*: If the `options` is a string, it will be parsed as the fields to select. +In addition to the mongo options, you can pass the option `rawCursor` in order to get the raw [mongo cursor](http://mongodb.github.io/node-mongodb-native/2.0/api/Cursor.html) when the promise resolve. + +3. [`callback`] *(function)* + +#### Returns + +A promise with a `each` method to stream the query. +The `each` method expects a function which will receive two arguments: + 1. `doc` *(Object)*: current document of the stream + 2. `cursor` *(Object)*: + * `close` *(function)*: close the stream. The promise will be resolved. + * `pause` *(function)*: pause the stream. + * `resume` *(function)*: resume the stream. + +#### Example + +```js +users.find({}).then((docs) => {}) +``` +```js +users.find({}, 'name').then((docs) => { + // only the name field will be selected +}) +users.find({}, { fields: { name: 1 } }) // equivalent + +users.find({}, '-name').then((docs) => { + // all the fields except the name field will be selected +}) +users.find({}, { fields: { name: -1 } }) // equivalent +``` +```js +users.find({}, { rawCursor: true }).then((cursor) => { + // raw mongo cursor +}) +``` +```js +users.find({}).each((user, {close, pause, resume}) => { + // the users are streaming here + // call `close()` to stop the stream +}).then(() => { + // stream is over +}) +``` diff --git a/docs/collection/findOne.md b/docs/collection/findOne.md new file mode 100644 index 00000000..0fdd88ee --- /dev/null +++ b/docs/collection/findOne.md @@ -0,0 +1,34 @@ +# `collection.findOne` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findOne) + +Returns one document that satisfies the specified query criteria. If multiple documents satisfy the query, this method returns the first document according to the natural order which reflects the order of documents on the disk. In capped collections, natural order is the same as insertion order. If no document satisfies the query, the method returns null. + +#### Arguments + +1. `query` *(String|ObjectId|Object)* + +2. [`options`] *(Object|String|Array)*: If the `options` is a string, it will be parsed as the fields to select. + +3. [`callback`] *(function)* + +#### Returns + +A promise. + +#### Example + +```js +users.findOne({name: 'foo'}).then((doc) => {}) +``` +```js +users.findOne({name: 'foo'}, 'name').then((doc) => { + // only the name field will be selected +}) +users.findOne({name: 'foo'}, { fields: { name: 1 } }) // equivalent + +users.findOne({name: 'foo'}, '-name').then((doc) => { + // all the fields except the name field will be selected +}) +users.findOne({name: 'foo'}, { fields: { name: -1 } }) // equivalent +``` diff --git a/docs/collection/findOneAndDelete.md b/docs/collection/findOneAndDelete.md new file mode 100644 index 00000000..7cb368a5 --- /dev/null +++ b/docs/collection/findOneAndDelete.md @@ -0,0 +1,23 @@ +# `collection.findOneAndDelete` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findOneAndDelete) + +Find a document and delete it in one atomic operation, requires a write lock for the duration of the operation. + +#### Arguments + +1. `query` *(String|ObjectId|Object)* + +2. [`options`] *(Object|String|Array)*: If the `options` is a string, it will be parsed as the fields to select. + +3. [`callback`] *(function)* + +#### Returns + +A promise. + +#### Example + +```js +users.findOneAndDelete({name: 'foo'}).then((doc) => {}) +``` diff --git a/docs/collection/findOneAndUpdate.md b/docs/collection/findOneAndUpdate.md new file mode 100644 index 00000000..108a8467 --- /dev/null +++ b/docs/collection/findOneAndUpdate.md @@ -0,0 +1,25 @@ +# `collection.findOneAndUpdate` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findOneAndUpdate) + +Find a document and update it in one atomic operation, requires a write lock for the duration of the operation. + +#### Arguments + +1. `query` *(String|ObjectId|Object)* + +2. `update` *(Object)*: Update operations to be performed on the document + +3. [`options`] *(Object|String|Array)*: If the `options` is a string, it will be parsed as the fields to select. + +4. [`callback`] *(function)* + +#### Returns + +A promise. + +#### Example + +```js +users.findOneAndUpdate({name: 'foo'}, {name: 'bar'}).then((updatedDoc) => {}) +``` diff --git a/docs/collection/indexes.md b/docs/collection/indexes.md new file mode 100644 index 00000000..bbf006a9 --- /dev/null +++ b/docs/collection/indexes.md @@ -0,0 +1,19 @@ +# `collection.indexes` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#indexes) + +Returns an array that holds a list of documents that identify and describe the existing indexes on the collection. + +#### Arguments + +1. [`callback`] *(function)* + +#### Returns + +A promise. + +#### Example + +```js +users.indexes().then((indexes) => {}) +``` diff --git a/docs/collection/insert.md b/docs/collection/insert.md new file mode 100644 index 00000000..01ef0c22 --- /dev/null +++ b/docs/collection/insert.md @@ -0,0 +1,24 @@ +# `collection.insert` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#insert) + +Inserts a single document or a an array of documents into MongoDB. + +#### Arguments + +1. `docs` *(Object|Array)* + +2. [`options`] *(Object)* + +3. [`callback`] *(function)* + +#### Returns + +A promise. + +#### Example + +```js +users.insert({ woot: 'foo' }) +users.insert([{ woot: 'bar' }, { woot: 'baz' }]) +``` diff --git a/docs/collection/remove.md b/docs/collection/remove.md new file mode 100644 index 00000000..72bf066a --- /dev/null +++ b/docs/collection/remove.md @@ -0,0 +1,23 @@ +# `collection.remove` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#remove) + +Remove documents. + +#### Arguments + +1. `query` *(Object|ObjectId|String)* + +2. [`options`] *(Object)* + +3. [`callback`] *(function)* + +#### Returns + +A promise. + +#### Example + +```js +users.remove({ woot: 'foo' }) +``` diff --git a/docs/collection/update.md b/docs/collection/update.md new file mode 100644 index 00000000..f8c55dd7 --- /dev/null +++ b/docs/collection/update.md @@ -0,0 +1,25 @@ +# `collection.update` + +[Mongo documentation ](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#update) + +Modifies an existing document or documents in a collection. The method can modify specific fields of an existing document or documents or replace an existing document entirely, depending on the update parameter. By default, the update() method updates a single document. Set the `multi` option to update all documents that match the query criteria. + +#### Arguments + +1. `query` *(String|ObjectId|Object)* + +2. `update` *(Object)*: Update operations to be performed on the document + +3. [`options`] *(Object)* + +4. [`callback`] *(function)* + +#### Returns + +A promise. + +#### Example + +```js +users.update({name: 'foo'}, {name: 'bar'}) +``` diff --git a/docs/id.md b/docs/id.md new file mode 100644 index 00000000..ee62564a --- /dev/null +++ b/docs/id.md @@ -0,0 +1,19 @@ +# `monk.id` + +Casts a string to (or create) an [ObjectId](https://docs.mongodb.com/manual/reference/method/ObjectId/). + +#### Arguments + +1. [`string`] *(string)*: optional hex id to cast to ObjectId. If not provided, a random ObjectId will be created. + +#### Returns + +An [ObjectId](https://docs.mongodb.com/manual/reference/method/ObjectId/). + +#### Example + +```js +const monk = require('monk') +const id = monk.id('4ee0fd75d6bd52107c000118') +const newId = monk.id() +``` diff --git a/docs/manager/README.md b/docs/manager/README.md new file mode 100644 index 00000000..c9a9bdda --- /dev/null +++ b/docs/manager/README.md @@ -0,0 +1,45 @@ +# Manager + +Monk constructor. + +#### Arguments + +1. `uri` *(string or Array)*: A [mongo connection string URI](https://docs.mongodb.com/manual/reference/connection-string/). Replica sets can be an array or comma separated. + +2. [`options`] *(Object)*: You may optionally specify [options](http://mongodb.github.io/node-mongodb-native/2.1/reference/connecting/connection-settings/). + +3. [`callback`] *(Function)*: You may optionally specify a callback which will be called once the connection to the mongo database is opened or throws an error. + +#### Returns + +A Manager instance with the following methods: + * [close](/docs/manager/close.md) + * [create](/docs/manager/create.md) + * [get](/docs/manager/get.md) + +#### Example + +```js +const db = require('monk')('localhost/mydb', options) +``` + +```js +const db = require('monk')('localhost/mydb,192.168.1.1') // replica set +``` + +```js +require('monk')('localhost/mydb,192.168.1.1').then((db) => { + // db is the connected instance of the Manager +}).catch((err) => { + // error connecting to the database +}) +``` + +#### Options + +You can set options to pass to every queries. By default, monk set +```js +db.options = { + safe: true +} +``` diff --git a/docs/manager/close.md b/docs/manager/close.md new file mode 100644 index 00000000..313efed1 --- /dev/null +++ b/docs/manager/close.md @@ -0,0 +1,19 @@ +# `manager.close` + +Closes the connection. + +#### Arguments + +1. [`force`] *(Boolean)*: Force close, emitting no events + +2. [`callback`] *(Function)*: You may optionally specify a callback which will be called once the connection to the mongo database is closed. + +#### Returns + +A Promise + +#### Example + +```js +db.close() +``` diff --git a/docs/manager/create.md b/docs/manager/create.md new file mode 100644 index 00000000..a88ac310 --- /dev/null +++ b/docs/manager/create.md @@ -0,0 +1,21 @@ +# `manager.create` + +Create a collection. + +#### Arguments + +1. `name` *(string)*: name of the mongo collection + +2. [`creationOptions`] *(object)*: options to create the collection + +3. [`options`] *(object)*: collection level options + +#### Returns + +A [Collection](/docs/collection/README.md) instance. + +#### Example + +```js +const users = db.create('users', { capped: true, size: n }) +``` diff --git a/docs/manager/get.md b/docs/manager/get.md new file mode 100644 index 00000000..ba28765f --- /dev/null +++ b/docs/manager/get.md @@ -0,0 +1,19 @@ +# `manager.get` + +Gets a collection. + +#### Arguments + +1. `name` *(string)*: name of the mongo collection + +2. [`options`] *(object)*: collection level options + +#### Returns + +A [Collection](/docs/collection/README.md) instance. + +#### Example + +```js +const users = db.get('users', options) +``` diff --git a/lib/collection.js b/lib/collection.js index 43a7520e..240679b0 100644 --- a/lib/collection.js +++ b/lib/collection.js @@ -1,36 +1,45 @@ -/** +/* * Module dependencies. */ var util = require('./util') var debug = require('debug')('monk:queries') -var inherits = require('util').inherits -var EventEmitter = require('events').EventEmitter -/** +function thenFn (fn) { + return function (res) { + if (fn && typeof fn === 'function') { + fn(null, res) + } + return res + } +} + +function catchFn (fn) { + return function (err) { + if (fn && typeof fn === 'function') { + return fn(err) + } + throw err + } +} + +/* * Module exports */ module.exports = Collection /** - * Collection. + * Mongo Collection. * - * @api public */ -function Collection (manager, name) { +function Collection (manager, name, options) { this.manager = manager - this.driver = manager.driver - this.helper = manager.helper this.name = name - this.col = this.driver.collection(name) - this.col.id = this.helper.id - this.options = {} - this.col.emitter = this.col.emitter || this.col._emitter - this.col.emitter.setMaxListeners(Infinity) + this.options = options || {} - this.oid = this.id = this.id.bind(this) + this.oid = this.id this.opts = this.opts.bind(this) this.index = this.ensureIndex = this.ensureIndex.bind(this) this.dropIndex = this.dropIndex.bind(this) @@ -41,6 +50,8 @@ function Collection (manager, name) { this.remove = this.remove.bind(this) this.removeById = this.removeById.bind(this) this.findAndModify = this.findAndModify.bind(this) + this.findOneAndUpdate = this.findOneAndUpdate.bind(this) + this.findOneAndDelete = this.findOneAndDelete.bind(this) this.insert = this.insert.bind(this) this.findById = this.findById.bind(this) this.find = this.find.bind(this) @@ -49,30 +60,24 @@ function Collection (manager, name) { this.findOne = this.findOne.bind(this) this.aggregate = this.aggregate.bind(this) this.drop = this.drop.bind(this) - this.cast = this.cast.bind(this) + util.cast = util.cast.bind(this) + this.executeWhenOpened = this.executeWhenOpened.bind(this) } /** - * Inherits from EventEmitter. + * Execute when connection opened. + * @private */ -inherits(Collection, EventEmitter) - -/** - * Casts to objectid - * - * @param {Mixed} hex id or ObjectId - * @return {ObjectId} - * @api public - */ - -Collection.prototype.id = function (str) { - if (str == null) return this.col.id() - return typeof str === 'string' ? this.col.id(str) : str +Collection.prototype.executeWhenOpened = function (fn) { + return this.manager.executeWhenOpened().then(function (db) { + return db.collection(this.name) + }.bind(this)) } /** * Opts utility. + * @private */ Collection.prototype.opts = function (opts) { @@ -94,194 +99,304 @@ Collection.prototype.opts = function (opts) { } /** - * Set up indexes. + * Calculates aggregate values for the data in a collection. * - * @param {Object|String|Array} fields - * @param {Object|Function} optional, options or callback - * @param {Function} optional, callback + * @param {Array} pipeline - A sequence of data aggregation operations or stages. + * @param {Object|Function} [opts] + * @param {Function} [fn] * @return {Promise} - * @api public */ -Collection.prototype.ensureIndex = function (fields, opts, fn) { +Collection.prototype.aggregate = function (stages, opts, fn) { if (typeof opts === 'function') { fn = opts opts = {} } - fields = util.fields(fields) + // opts opts = this.opts(opts) // query - debug('%s ensureIndex %j (%j)', this.name, fields, opts) - return new Promise(function (resolve, reject) { - this.col.ensureIndex(fields, opts, util.callback(resolve, reject, fn)) - }.bind(this)) + debug('%s aggregate %j', this.name, stages) + return this.executeWhenOpened().then(function (col) { + return col.aggregate(stages, opts) + }).then(function (cursor) { + return cursor.toArray() + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * Drop indexes. + * Returns the count of documents that would match a find() query. The db.collection.count() method does not perform the find() operation but instead counts and returns the number of results that match a query. * - * @param {Object|String|Array} fields - * @param {Object|Function} optional, options or callback - * @param {Function} optional, callback + * @param {Object} query - The query selection criteria. + * @param {Object} [opts] - Extra options for modifying the count. + * @param {Function} [fn] - completion callback. * @return {Promise} - * @api public */ -Collection.prototype.dropIndex = function (fields, opts, fn) { +Collection.prototype.count = function (query, opts, fn) { + query = util.query(query) + if (typeof opts === 'function') { fn = opts opts = {} } - fields = util.fields(fields) + // opts opts = this.opts(opts) + // cast + if (opts.castIds !== false) { + query = util.cast(query) + } + // query - debug('%s dropIndex %j (%j)', this.name, fields, opts) - return new Promise(function (resolve, reject) { - this.col.dropIndex(fields, opts, util.callback(resolve, reject, fn)) - }.bind(this)) + debug('%s count %j', this.name, query) + return this.executeWhenOpened().then(function (col) { + return col.count(query) + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * Drop all indexes. + * Finds the distinct values for a specified field across a single collection and returns the results in an array. * - * @param {Object|Function} optional, options or callback - * @param {Function} optional, callback + * @param {String} field - The field for which to return distinct values. + * @param {Object} [query] - A query that specifies the documents from which to retrieve the distinct values. + * @param {Object} [opts] - options + * @param {Function} [fn] completion callback * @return {Promise} - * @api public */ -Collection.prototype.dropIndexes = function (fn) { +Collection.prototype.distinct = function (field, query, opts, fn) { + if (typeof opts === 'function') { + fn = opts + opts = {} + } + + if (typeof query === 'function') { + fn = query + query = {} + } + + query = util.query(query) + + // opts + opts = this.opts(opts) + + // cast + if (opts.castIds !== false) { + query = util.cast(query) + } + // query - debug('%s dropIndexes', this.name) - return new Promise(function (resolve, reject) { - this.col.dropIndexes(util.callback(resolve, reject, fn)) - }.bind(this)) + debug('%s distinct %s (%j)', this.name, field, query) + return this.executeWhenOpened().then(function (col) { + return col.distinct(field, query) + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * Gets all indexes. + * Removes a collection from the database. The method also removes any indexes associated with the dropped collection. * - * @param {Function} callback + * @param {Function} [fn] callback * @return {Promise} - * @api public */ -Collection.prototype.indexes = function (fn) { - debug('%s indexInformation', this.name) - return new Promise(function (resolve, reject) { - this.col.indexInformation(util.callback(resolve, reject, fn)) - }.bind(this)) +Collection.prototype.drop = function (fn) { + debug('%s drop', this.name) + return this.executeWhenOpened().then(function (col) { + return col.drop() + }).catch(function (err) { + if (err && err.message === 'ns not found') { + return 'ns not found' + } else { + throw err + } + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * update + * Drops or removes the specified index or indexes from a collection. * - * @param {Object} search query - * @param {Object} update obj - * @param {Object|String|Array} optional, options or fields - * @param {Function} callback + * https://docs.mongodb.com/manual/reference/method/db.collection.dropIndex/ + * + * @param {Object|String|Array} fields + * @param {Object} [opts] + * @param {Function} [fn] callback * @return {Promise} - * @api public */ -Collection.prototype.update = function (search, update, opts, fn) { - if (typeof search === 'string' || typeof search.toHexString === 'function') { - return this.update({ _id: search }, update, opts, fn) - } - +Collection.prototype.dropIndex = function (fields, opts, fn) { if (typeof opts === 'function') { fn = opts opts = {} } + fields = util.fields(fields) opts = this.opts(opts) - // cast - if (opts.castIds !== false) { - search = this.cast(search) - update = this.cast(update) - } - // query - debug('%s update %j with %j', this.name, search, update) - return new Promise(function (resolve, reject) { - this.col.update(search, update, opts, util.callback(resolve, reject, fn, function (err, doc, next) { - next(err, doc && doc.result || doc) - })) - }.bind(this)) + debug('%s dropIndex %j (%j)', this.name, fields, opts) + return this.executeWhenOpened().then(function (col) { + return col.dropIndex(fields, opts) + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * update by id helper + * Drops all indexes other than the required index on the _id field. * - * @param {String|Object} object id - * @param {Object} update obj - * @param {Object|String|Array} optional, options or fields - * @param {Function} callback + * https://docs.mongodb.com/manual/reference/method/db.collection.dropIndexes/ + * + * @param {Function} [fn] callback + * + * @example + * + * users.dropIndexes() * @return {Promise} - * @api public */ -Collection.prototype.updateById = function (id, obj, opts, fn) { - return this.update({ _id: id }, obj, opts, fn) +Collection.prototype.dropIndexes = function (fn) { + // query + debug('%s dropIndexes', this.name) + return this.executeWhenOpened().then(function (col) { + return col.dropIndexes() + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * remove + * Creates indexes on collections. + * + * https://docs.mongodb.com/manual/reference/method/db.collection.ensureIndex/ + * + * @param {Object|String|Array} fields + * @param {Object} [opts] options + * @param {Function} [fn] callback + * + * @example * - * @param {Object} search query - * @param {Object|Function} optional, options or callback - * @param {Function} optional, callback + * users.index('name.first') + * users.index('name last') + * users.index(['nombre', 'apellido']) + * users.index({ up: 1, down: -1 }) + * users.index({ woot: 1 }, { unique: true }) * @return {Promise} */ -Collection.prototype.remove = function (search, opts, fn) { +Collection.prototype.ensureIndex = function (fields, opts, fn) { if (typeof opts === 'function') { fn = opts opts = {} } + fields = util.fields(fields) opts = this.opts(opts) - // cast - if (opts.castIds !== false) { - search = this.cast(search) - } - // query - debug('%s remove %j with %j', this.name, search, opts) - return new Promise(function (resolve, reject) { - this.col.remove(search, opts, util.callback(resolve, reject, fn)) - }.bind(this)) + debug('%s ensureIndex %j (%j)', this.name, fields, opts) + return this.executeWhenOpened().then(function (col) { + return col.ensureIndex(fields, opts) + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * remove by ID + * Selects documents in a collection and return them. * - * @param {String} hex id - * @param {Object|String|Array} optional, options or fields - * @param {Function} completion callback + * @param {String|Object|ObjectId} query + * @param {Object|String|Array} [opts] options or fields + * @param {Function} [fn] completion callback * @return {Promise} - * @api public */ -Collection.prototype.removeById = function (id, opts, fn) { - return this.remove({ _id: id }, opts, fn) +Collection.prototype.find = function (query, opts, fn) { + query = util.query(query) + + if (typeof opts === 'function') { + fn = opts + opts = {} + } + + // opts + opts = this.opts(opts) + + // cast + if (opts.castIds !== false) { + query = util.cast(query) + } + + // query + debug('%s find %j', this.name, query) + + if (opts.rawCursor) { + delete opts.rawCursor + return this.executeWhenOpened().then(function (col) { + return col.find(query, opts) + }).then(thenFn(fn)).catch(catchFn(fn)) + } + + var didClose = false + var promise = this.executeWhenOpened().then(function (col) { + return col.find(query, opts) + }).then(function (cursor) { + if (!opts.stream && !promise.eachListener) { + return cursor.toArray().then(thenFn(fn)).catch(catchFn(fn)) + } + + function close () { + didClose = true + cursor = cursor.cursor || cursor + cursor.close() + } + + return new Promise(function (resolve, reject) { + cursor.each(function (err, doc) { + if (didClose && !err) { + // emit success + err = doc = null + } + + if (err) { + if (fn) { + fn(err) + } + reject(err) + } else if (doc) { + promise.eachListener(doc, { + close: close, + pause: cursor.pause, + resume: cursor.resume + }) + } else { + if (fn) { + fn() + } + resolve() + } + }) + }) + }) + + promise.each = function (eachListener) { + promise.eachListener = eachListener + return promise + } + + return promise } /** - * findAndModify + * @deprecated + * Modifies and returns a single document. By default, the returned document does not include the modifications made on the update. To return the document with the modifications made on the update, use the `new` option. * * @param {Object} search query, or { query, update } object - * @param {Object} optional, update object - * @param {Object|String|Array} optional, options or fields - * @param {Function} callback + * @param {Object} [update] object + * @param {Object|String|Array} [opts] options or fields + * @param {Function} [fn] callback + * + * @example + * + * users.findAndModify({ name: 'Mathieu' }, { $set: { foo: 'bar' } }, opts) + * users.findAndModify({ query: { name: 'Mathieu' }, update: { $set: { foo: 'bar' } }}, opts) * @return {Promise} - * @api public */ Collection.prototype.findAndModify = function (query, update, opts, fn) { @@ -297,9 +412,7 @@ Collection.prototype.findAndModify = function (query, update, opts, fn) { opts = update } - if (typeof query.query === 'string' || typeof query.query.toHexString === 'function') { - query.query = { _id: query.query } - } + query.query = util.query(query.query) if (typeof opts === 'function') { fn = opts @@ -308,6 +421,12 @@ Collection.prototype.findAndModify = function (query, update, opts, fn) { opts = this.opts(opts) + if (opts.remove) { + console.warn('DEPRECATED (collection.findAndModify): use collection.findOneAndDelete instead (see https://Automattic.github.io/monk/docs/collection/findOneAndDelete.html)') + } else { + console.warn('DEPRECATED (collection.findAndModify): use collection.findOneAndUpdate instead (see https://Automattic.github.io/monk/docs/collection/findOneAndUpdate.html)') + } + // `new` defaults to `true` for upserts if (opts.new == null && opts.upsert) { opts.new = true @@ -315,213 +434,238 @@ Collection.prototype.findAndModify = function (query, update, opts, fn) { // cast if (opts.castIds !== false) { - query.query = this.cast(query.query) - query.update = this.cast(query.update) + query.query = util.cast(query.query) + query.update = util.cast(query.update) } // query debug('%s findAndModify %j with %j', this.name, query.query, query.update) - return new Promise(function (resolve, reject) { - this.col.findAndModify( + return this.executeWhenOpened().then(function (col) { + return col.findAndModify( query.query, [], query.update, - this.opts(opts), - util.callback(resolve, reject, fn, function (err, doc, next) { - next(err, doc && doc.value) - })) - }.bind(this)) + opts + ) + }).then(function (doc) { + return doc && doc.value || doc + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * insert + * Returns one document that satisfies the specified query criteria. If multiple documents satisfy the query, this method returns the first document according to the natural order which reflects the order of documents on the disk. In capped collections, natural order is the same as insertion order. If no document satisfies the query, the method returns null. + * + * https://docs.mongodb.com/manual/reference/method/db.collection.findOne/ + * + * @param {String|ObjectId|Object} query + * @param {Object} [opts] - options + * @param {Function} [fn] - completion callback + * + * @example * - * @param {Object} data - * @param {Object|String|Array} optional, options or fields - * @param {Function} callback + * users.findOne({name: 'foo'}).then((doc) => {}) * @return {Promise} - * @api public */ -Collection.prototype.insert = function (data, opts, fn) { +Collection.prototype.findOne = function (query, opts, fn) { + query = util.query(query) + if (typeof opts === 'function') { fn = opts opts = {} } + // opts opts = this.opts(opts) - var arrayInsert = Array.isArray(data) - // cast if (opts.castIds !== false) { - data = this.cast(data) + query = util.cast(query) } // query - debug('%s insert %j', this.name, data) - return new Promise(function (resolve, reject) { - this.col.insert(data, opts, util.callback(resolve, reject, fn, function (err, docs, next) { - var res = (docs || {}).ops - if (res && !arrayInsert) { - res = docs.ops[0] - } - next(err, res) - })) - }.bind(this)) + debug('%s findOne %j', this.name, query) + return this.executeWhenOpened().then(function (col) { + return col.find(query, opts).limit(1).toArray() + }).then(function (docs) { + return docs && docs[0] || null + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * findOne by ID + * @deprecated + * findOne by ID helper + * + * @see findOne * * @param {String} hex id - * @param {Object|String|Array} optional, options or fields - * @param {Function} completion callback + * @param {Object|String|Array} [opts] options or fields + * @param {Function} [fn] completion callback * @return {Promise} - * @api public */ Collection.prototype.findById = function (id, opts, fn) { + console.warn('DEPRECATED (collection.findById): use collection.findOne instead (see https://Automattic.github.io/monk/docs/collection/findOne.html)') return this.findOne({ _id: id }, opts, fn) } /** - * find + * Find a document and delete it in one atomic operation, requires a write lock for the duration of the operation. * - * @param {Object} query - * @param {Object|String|Array} optional, options or fields - * @param {Function} completion callback + * http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findOneAndDelete + * + * @param {String|Object|ObjectId} query + * @param {Object|String|Array} [opts] options or fields + * @param {Function} [fn] callback * @return {Promise} - * @api public */ -Collection.prototype.find = function (query, opts, fn) { +Collection.prototype.findOneAndDelete = function (query, opts, fn) { + query = util.query(query) + if (typeof opts === 'function') { fn = opts opts = {} } - // opts opts = this.opts(opts) // cast if (opts.castIds !== false) { - query = this.cast(query) + query = util.cast(query) } // query - debug('%s find %j', this.name, query) - - var didClose = false - var promise = new Promise(function (resolve, reject) { - Promise.resolve(this.col.find(query, opts)).then(function (cursor) { - if (opts.stream || promise.eachListener) { - stream() - } else { - cursor.toArray(util.callback(resolve, reject, fn)) - } - - function destroy () { - didClose = true - cursor = cursor.cursor || cursor - cursor.close() - } - - function stream () { - cursor.each(function (err, doc) { - if (didClose && !err) { - // emit success - err = doc = null - } - - if (err) { - if (fn) { - fn(err) - } - reject(err) - } else if (doc) { - promise.eachListener(doc, destroy) - } else { - if (fn) { - fn() - } - resolve() - } - }) - } - }) - }.bind(this)) - - promise.each = function (eachListener) { - promise.eachListener = eachListener - return promise - } - - return promise + debug('%s findOneAndDelete %j with %j', this.name, query) + return this.executeWhenOpened().then(function (col) { + return col.findOneAndDelete(query, opts) + }).then(function (doc) { + return doc && doc.value || doc + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * distinct + * Find a document and update it in one atomic operation, requires a write lock for the duration of the operation. + * + * http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findOneAndUpdate * - * @param {String} distinct field to select - * @param {Object} optional, query - * @param {Function} completion callback + * @param {String|Object|ObjectId} query + * @param {Object} update + * @param {Object|String|Array} [opts] options or fields + * @param {Function} [fn] callback + * + * @example + * + * users.findOneAndUpdate({ name: 'Mathieu' }, opts) + * users.findOneAndUpdate({ query: { name: 'Mathieu' }, opts) * @return {Promise} - * @api public */ -Collection.prototype.distinct = function (field, query, fn) { - if (typeof query === 'function') { - fn = query - query = {} +Collection.prototype.findOneAndUpdate = function (query, update, opts, fn) { + query = util.query(query) + + if (typeof opts === 'function') { + fn = opts + opts = {} + } + + opts = this.opts(opts) + + if (typeof opts.returnOriginal === 'undefined') { + opts.returnOriginal = false } // cast - query = this.cast(query) + if (opts.castIds !== false) { + query = util.cast(query) + } // query - debug('%s distinct %s (%j)', this.name, field, query) - return new Promise(function (resolve, reject) { - this.col.distinct(field, query, util.callback(resolve, reject, fn)) - }.bind(this)) + debug('%s findOneAndUpdate %j with %j', this.name, query, update) + return this.executeWhenOpened().then(function (col) { + return col.findOneAndUpdate(query, update, opts) + }).then(function (doc) { + return doc && doc.value || doc + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * count + * Returns an array that holds a list of documents that identify and describe the existing indexes on the collection. * - * @param {Object} query - * @param {Function} completion callback + * https://docs.mongodb.com/manual/reference/method/db.collection.getIndexes/ + * + * @param {Function} [fn] callback * @return {Promise} - * @api public */ -Collection.prototype.count = function (query, fn) { +Collection.prototype.indexes = function (fn) { + debug('%s indexInformation', this.name) + return this.executeWhenOpened().then(function (col) { + return col.indexInformation() + }).then(thenFn(fn)).catch(catchFn(fn)) +} + +/** + * Inserts a document or documents into a collection. + * + * https://docs.mongodb.com/manual/reference/method/db.collection.insert/ + * + * @param {Object|Array} data + * @param {Object} [opts] options + * @param {Function} [fn] callback + * + * @example + * + * users.insert({ woot: 'foo' }) + * users.insert([{ woot: 'bar' }, { woot: 'baz' }]) + * @return {Promise} + */ + +Collection.prototype.insert = function (data, opts, fn) { + if (typeof opts === 'function') { + fn = opts + opts = {} + } + + opts = this.opts(opts) + + var arrayInsert = Array.isArray(data) + // cast - query = this.cast(query) + if (opts.castIds !== false) { + data = util.cast(data) + } // query - debug('%s count %j', this.name, query) - return new Promise(function (resolve, reject) { - this.col.count(query, util.callback(resolve, reject, fn)) - }.bind(this)) + debug('%s insert %j', this.name, data) + return this.executeWhenOpened().then(function (col) { + return col.insert(data, opts) + }).then(function (docs) { + var res = (docs || {}).ops + if (res && !arrayInsert) { + res = docs.ops[0] + } + return res + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * findOne + * Removes documents from a collection. * - * @param {String|ObjectId|Object} query - * @param {Object} options - * @param {Function} completion callback + * https://docs.mongodb.com/manual/reference/method/db.collection.remove/ + * + * @param {Object|ObjectId|String} search query + * @param {Object} [opts] options + * @param {Function} [fn] callback + * + * @example + * + * users.remove({ name: 'Mathieu' }) * @return {Promise} - * @api public */ -Collection.prototype.findOne = function (search, opts, fn) { - search = search || {} - - if (typeof search === 'string' || typeof search.toHexString === 'function') { - return this.findById(search, opts, fn) - } +Collection.prototype.remove = function (query, opts, fn) { + query = util.query(query) if (typeof opts === 'function') { fn = opts @@ -532,112 +676,103 @@ Collection.prototype.findOne = function (search, opts, fn) { // cast if (opts.castIds !== false) { - search = this.cast(search) + query = util.cast(query) } // query - debug('%s findOne %j', this.name, search) - return new Promise(function (resolve, reject) { - this.col.findOne(search, opts, util.callback(resolve, reject, fn)) - }.bind(this)) + debug('%s remove %j with %j', this.name, query, opts) + return this.executeWhenOpened().then(function (col) { + return col.remove(query, opts) + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * Drops the collection. + * @deprecated + * remove by ID helper + * @see remove * - * @param {Function} optional, callback + * @param {String} hex id + * @param {Object} [opts] options + * @param {Function} [fn] callback + * + * @example + * + * users.removeById(id) * @return {Promise} - * @api public */ -Collection.prototype.drop = function (fn) { - debug('%s drop', this.name) - return new Promise(function (resolve, reject) { - this.col.drop(function (err, res) { - if (err && err.message === 'ns not found') { - err = undefined - res = 'ns not found' - } - util.callback(resolve, reject, fn)(err, res) - }) - }.bind(this)) +Collection.prototype.removeById = function (id, opts, fn) { + console.warn('DEPRECATED (collection.removeById): use collection.remove instead (see https://Automattic.github.io/monk/docs/collection/remove.html)') + return this.remove({ _id: id }, opts, fn) } /** - * aggregate + * Modifies an existing document or documents in a collection. The method can modify specific fields of an existing document or documents or replace an existing document entirely, depending on the update parameter. By default, the update() method updates a single document. Set the `multi` option to update all documents that match the query criteria. * - * @param {Array} aggregation stages - * @param {Object|Function} optional, options or callback - * @param {Function} optional, callback + * https://docs.mongodb.com/manual/reference/method/db.collection.update/ + * + * @param {Object} query + * @param {Object} update obj + * @param {Object|String|Array} [opts], options or fields + * @param {Function} [fn] callback + * + * @example + * + * users.update({ name: 'Mathieu' }, { $set: { foo: 'bar' } }) * @return {Promise} */ -Collection.prototype.aggregate = function (stages, opts, fn) { +Collection.prototype.update = function (query, update, opts, fn) { + query = util.query(query) + if (typeof opts === 'function') { fn = opts opts = {} } opts = this.opts(opts) + + // cast + if (opts.castIds !== false) { + query = util.cast(query) + update = util.cast(update) + } + // query - debug('%s aggregate %j', this.name, stages) - return new Promise(function (resolve, reject) { - this.col.aggregate(stages, opts, util.callback(resolve, reject, fn)) - }.bind(this)) + debug('%s update %j with %j', this.name, query, update) + return this.executeWhenOpened().then(function (col) { + return col.update(query, update, opts) + }).then(function (doc) { + return doc && doc.result || doc + }).then(thenFn(fn)).catch(catchFn(fn)) } /** - * Applies ObjectId casting to _id fields. + * @deprecated + * update by id helper + * @see update + * + * @param {String|Object} id - object id + * @param {Object} update - update obj + * @param {Object|String|Array} [opts] options or fields + * @param {Function} [fn] callback + * + * @example * - * @param {Object} optional, query - * @return {Object} query - * @api private + * users.updateById(id, { $set: { foo: 'bar' } }) + * @return {Promise} */ -Collection.prototype.cast = function (obj) { - obj = obj || {} - - if (obj._id) { - if (obj._id.$in) { - obj._id.$in = obj._id.$in.map(function (q) { - return this.id(q) - }, this) - } else if (obj._id.$nin) { - obj._id.$nin = obj._id.$nin.map(function (q) { - return this.id(q) - }, this) - } else if (obj._id.$ne) { - obj._id.$ne = this.id(obj._id.$ne) - } else { - obj._id = this.id(obj._id) - } - } - - if (obj.$set && obj.$set._id) { - obj.$set._id = this.id(obj.$set._id) - } - - if (obj.$not && obj.$not._id) { - obj.$not._id = this.id(obj.$not._id) - } - - if (obj.$and && Array.isArray(obj.$and)) { - obj.$and = obj.$and.map(function (q) { - return this.cast(q) - }, this) - } - - if (obj.$or && Array.isArray(obj.$or)) { - obj.$or = obj.$or.map(function (q) { - return this.cast(q) - }, this) - } +Collection.prototype.updateById = function (id, update, opts, fn) { + console.warn('DEPRECATED (collection.updateById): use collection.update instead (see https://Automattic.github.io/monk/docs/collection/update.html)') + return this.update({ _id: id }, update, opts, fn) +} - if (obj.$nor && Array.isArray(obj.$nor)) { - obj.$nor = obj.$nor.map(function (q) { - return this.cast(q) - }, this) - } +/** + * @deprecated + */ - return obj +Collection.prototype.id = function (str) { + console.warn('DEPRECATED (collection.id): use monk.id instead (see https://Automattic.github.io/monk/docs/id.html)') + return require('./helpers').id(str) } diff --git a/lib/helpers.js b/lib/helpers.js new file mode 100644 index 00000000..bfc66c12 --- /dev/null +++ b/lib/helpers.js @@ -0,0 +1,14 @@ +var ObjectId = require('mongodb').ObjectID + +/** + * Casts to objectid + * + * @param {Mixed} str - hex id or ObjectId + * @return {ObjectId} + * @api public + */ + +exports.id = function (str) { + if (str == null) return ObjectId() + return typeof str === 'string' ? ObjectId.createFromHexString(str) : str +} diff --git a/lib/manager.js b/lib/manager.js index cd86da9a..40a1fa69 100644 --- a/lib/manager.js +++ b/lib/manager.js @@ -1,27 +1,35 @@ -/** +/* * Module dependencies. */ -var mongoskin = require('mongoskin') +var mongo = require('mongodb') var debug = require('debug')('monk:manager') var Collection = require('./collection') -var ObjectId = mongoskin.ObjectID +var ObjectId = mongo.ObjectID +var MongoClient = mongo.MongoClient var EventEmitter = require('events').EventEmitter var inherits = require('util').inherits -/** +var STATE = { + CLOSED: 'closed', + OPENING: 'opening', + OPEN: 'open' +} + +/* * Module exports. */ module.exports = Manager /** - * Manager constructor. + * Monk constructor. * - * @param {Array|String} connection uri. replica sets can be an array or + * @param {Array|String} uri replica sets can be an array or * comma-separated - * @param {Object|Function} options or connect callback - * @param {Function} connect callback + * @param {Object|Function} opts or connect callback + * @param {Function} fn connect callback + * @return {Promise} resolve when the connection is opened */ function Manager (uri, opts, fn) { @@ -60,74 +68,199 @@ function Manager (uri, opts, fn) { } } - this.driver = mongoskin.db(uri, opts) - this.helper = mongoskin.helper - this.helper.id = ObjectId - this.driver.open(this.onOpen.bind(this)) + this._state = STATE.OPENING + + this._queue = [] + this.on('open', function (db) { + this._queue.forEach(function (cb) { + cb(db) + }) + }.bind(this)) + + this._connectionURI = uri + this._connectionOptions = opts + + this.open(uri, opts, fn && function (err) { + fn(err, this) + }.bind(this)) + + this.helper = { + id: ObjectId + } + this.collections = {} this.options = { safe: true } + this.open = this.open.bind(this) this.close = this.close.bind(this) - this.col = this.get = this.get.bind(this) - this.oid = this.id = this.id.bind(this) + this.executeWhenOpened = this.executeWhenOpened.bind(this) + this.collection = this.col = this.get = this.get.bind(this) + this.oid = this.id +} - if (fn) { - this.once('open', fn) +/* + * Inherits from EventEmitter. + */ + +inherits(Manager, EventEmitter) + +/** + * Open the connection + * @private + */ +Manager.prototype.open = function (uri, opts, fn) { + MongoClient.connect(uri, opts, function (err, db) { + if (err || !db) { + this._state = STATE.CLOSED + this.emit('error-opening', err) + } else { + this._state = STATE.OPEN + this._db = db + this.emit('open', db) + } + if (fn) { + fn(err, this) + } + }.bind(this)) +} + +/** + * Execute when connection opened. + * @private + */ + +Manager.prototype.executeWhenOpened = function () { + switch (this._state) { + case STATE.OPEN: + return Promise.resolve(this._db) + case STATE.OPENING: + return new Promise(function (resolve) { + this._queue.push(resolve) + }.bind(this)) + case STATE.CLOSED: + default: + return new Promise(function (resolve) { + this._queue.push(resolve) + this.open(this._connectionURI, this._connectionOptions) + }.bind(this)) } } /** - * Inherits from EventEmitter + * Then + * + * @param {Function} [fn] - callback */ -inherits(Manager, EventEmitter) +Manager.prototype.then = function (fn) { + return new Promise(function (resolve, reject) { + this.once('open', resolve) + this.once('error-opening', reject) + }.bind(this)).then(fn.bind(null, this)) +} /** - * Open callback. + * Catch * - * @api private + * @param {Function} [fn] - callback */ -Manager.prototype.onOpen = function () { - this.emit('open') +Manager.prototype.catch = function (fn) { + return new Promise(function (resolve) { + this.once('error-opening', resolve) + }.bind(this)).then(fn.bind(null)) } /** * Closes the connection. * - * @return {Manager} for chaining - * @api private + * @param {Boolean} [force] - Force close, emitting no events + * @param {Function} [fn] - callback + * @return {Promise} */ -Manager.prototype.close = function (fn) { - this.driver.close(fn) - return this +Manager.prototype.close = function (force, fn) { + if (typeof force === 'function') { + fn = force + force = false + } + + var self = this + function close (resolve, db) { + db.close(force, function () { + self._state = STATE.CLOSED + self.emit('close') + if (fn) { + fn() + } + resolve() + }) + } + + switch (this._state) { + case STATE.CLOSED: + if (fn) { + fn() + } + return Promise.resolve() + case STATE.OPENING: + return new Promise(function (resolve) { + self._queue.push(function (db) { + close(resolve, db) + }) + }) + case STATE.OPEN: + default: + return new Promise(function (resolve) { + close(resolve, self._db) + }) + } } /** * Gets a collection. * + * @param {String} name - name of the mongo collection + * @param {Object} [options] - options to pass to the collection * @return {Collection} collection to query against - * @api private */ -Manager.prototype.get = function (name) { +Manager.prototype.get = function (name, options) { if (!this.collections[name]) { - this.collections[name] = new Collection(this, name) + this.collections[name] = new Collection(this, name, options) } return this.collections[name] } /** - * Casts to objectid + * Create a collection. * - * @param {Mixed} hex id or ObjectId - * @return {ObjectId} - * @api public + * @param {String} name - name of the mongo collection + * @param {Object} [creationOptions] - options used when creating the collection + * @param {Object} [options] - options to pass to the collection + * @return {Collection} collection to query against + */ + +Manager.prototype.create = function (name, creationOptions, options) { + this.executeWhenOpened().then(function (db) { + db.createCollection(name, creationOptions) + }).catch(function (err) { + this.emit('error', err) + }) + + if (!this.collections[name]) { + this.collections[name] = new Collection(this, name, options) + } + + return this.collections[name] +} + +/** + * @deprecated */ Manager.prototype.id = function (str) { - if (str == null) return ObjectId() - return typeof str === 'string' ? ObjectId.createFromHexString(str) : str + console.warn('DEPRECATED (manager.id): use monk.id instead (see https://Automattic.github.io/monk/docs/id.html)') + return require('./helpers').id(str) } diff --git a/lib/monk.js b/lib/monk.js index 4b29935a..b0b17168 100644 --- a/lib/monk.js +++ b/lib/monk.js @@ -1,11 +1,11 @@ -/** +/* * Module exports. */ module.exports = exports = require('./manager') -/** +/* * Expose Collection. * * @api public @@ -13,10 +13,20 @@ module.exports = exports = require('./manager') exports.Collection = require('./collection') -/** +/* * Expose util. * * @api public */ exports.util = require('./util') + +/* + * Expose helpers at the top level + */ + +var helpers = require('./helpers') + +for (var key in helpers) { + exports[key] = helpers[key] +} diff --git a/lib/util.js b/lib/util.js index fab01a15..526c0409 100755 --- a/lib/util.js +++ b/lib/util.js @@ -1,30 +1,77 @@ +var id = require('./helpers').id + /** - * Callback for a mongo query + * Applies ObjectId casting to _id fields. * - * @param {Function} resolve the promise - * @param {Function} reject the promise - * @param {Function} fn - optional callback passed to the query - * @param {Function} middleware to parse the error and result and the query - * before passing them to the callback - * @api public + * @param {Object} optional, query + * @return {Object} query + * @private */ -exports.callback = function (resolve, reject, fn, middleware) { - if (!middleware) { - middleware = function (err, res, next) { - return next(err, res) + +exports.cast = function cast (obj) { + obj = obj || {} + + if (obj._id) { + if (obj._id.$in) { + obj._id.$in = obj._id.$in.map(function (q) { + return id(q) + }) + } else if (obj._id.$nin) { + obj._id.$nin = obj._id.$nin.map(function (q) { + return id(q) + }) + } else if (obj._id.$ne) { + obj._id.$ne = id(obj._id.$ne) + } else { + obj._id = id(obj._id) } } - return function (err, res) { - return middleware(err, res, function (parsedErr, parsedRes) { - if (fn && typeof fn === 'function') { - fn(parsedErr, parsedRes) - } - if (parsedErr) { - return reject(parsedErr) - } - return resolve(parsedRes) - }) + + if (obj.$set && obj.$set._id) { + obj.$set._id = id(obj.$set._id) + } + + if (obj.$not && obj.$not._id) { + obj.$not._id = id(obj.$not._id) + } + + if (obj.$and && Array.isArray(obj.$and)) { + obj.$and = obj.$and.map(function (q) { + return cast(q) + }, this) } + + if (obj.$or && Array.isArray(obj.$or)) { + obj.$or = obj.$or.map(function (q) { + return cast(q) + }, this) + } + + if (obj.$nor && Array.isArray(obj.$nor)) { + obj.$nor = obj.$nor.map(function (q) { + return cast(q) + }, this) + } + + return obj +} + +/** + * Check if the query is an id and if so, transform it to a proper query + * + * @param {String|ObjectId|Object} query + * @return {Object} query + * @private + */ + +exports.query = function (query) { + query = query || {} + + if (typeof query === 'string' || typeof query.toHexString === 'function') { + return {_id: query} + } + + return query } /** @@ -33,7 +80,7 @@ exports.callback = function (resolve, reject, fn, middleware) { * @param {String|Object|Array} fields * @param {number} number when - * @return {Object} fields in object format - * @api public + * @private */ exports.fields = function (obj, numberWhenMinus) { @@ -60,7 +107,7 @@ exports.fields = function (obj, numberWhenMinus) { * * @param {String|Array|Object} fields or options * @return {Object} options - * @api public + * @private */ exports.options = function (opts) { diff --git a/package.json b/package.json index 382d1a29..00e13158 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monk", - "version": "2.1.0", + "version": "3.0.0", "main": "lib/monk.js", "tags": [ "mongodb", @@ -16,8 +16,7 @@ }, "dependencies": { "debug": "*", - "mongodb": "^2.1.18", - "mongoskin": "^2.1.0" + "mongodb": "^2.1.18" }, "devDependencies": { "ava": "^0.15.2", @@ -27,6 +26,11 @@ "eslint-plugin-ava": "2.5.0", "eslint-plugin-promise": "^1.3.2", "eslint-plugin-standard": "^1.3.2", + "gitbook-cli": "2.3.0", + "gitbook-plugin-anker-enable": "0.0.4", + "gitbook-plugin-edit-link": "2.0.2", + "gitbook-plugin-github": "2.0.0", + "gitbook-plugin-prism": "1.0.0", "nyc": "^6.4.4" }, "license": "MIT", diff --git a/test/casting.js b/test/casting.js index 4c35e80f..fec17bd3 100644 --- a/test/casting.js +++ b/test/casting.js @@ -1,88 +1,80 @@ import test from 'ava' -const Monk = require('../lib/monk') +const monk = require('../lib/monk') -const db = new Monk('127.0.0.1/monk-test-collection') -const users = db.get('users') - -test('string -> oid', (t) => { - const oid = users.id('4ee0fd75d6bd52107c000118') +test('string -> id', (t) => { + const oid = monk.id('4ee0fd75d6bd52107c000118') t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) -test('oid -> oid', (t) => { - const oid = users.id(users.id('4ee0fd75d6bd52107c000118')) +test('id -> id', (t) => { + const oid = monk.id(monk.id('4ee0fd75d6bd52107c000118')) t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) -test('new oid', (t) => { - const oid = users.id() - t.is(typeof oid.toHexString(), 'string') -}) - -test('#oid', (t) => { - const oid = users.oid() +test('new id', (t) => { + const oid = monk.id() t.is(typeof oid.toHexString(), 'string') }) -test('should cast oids inside $and', (t) => { - const cast = users.cast({ +test('should cast ids inside $and', (t) => { + const cast = monk.util.cast({ $and: [{_id: '4ee0fd75d6bd52107c000118'}] }) - const oid = users.id(cast.$and[0]._id) + const oid = monk.id(cast.$and[0]._id) t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) -test('should cast oids inside $nor', (t) => { - const cast = users.cast({ +test('should cast ids inside $nor', (t) => { + const cast = monk.util.cast({ $nor: [{_id: '4ee0fd75d6bd52107c000118'}] }) - const oid = users.id(cast.$nor[0]._id) + const oid = monk.id(cast.$nor[0]._id) t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) -test('should cast oids inside $not queries', (t) => { - const cast = users.cast({$not: {_id: '4ee0fd75d6bd52107c000118'}}) +test('should cast ids inside $not queries', (t) => { + const cast = monk.util.cast({$not: {_id: '4ee0fd75d6bd52107c000118'}}) - const oid = users.id(cast.$not._id) + const oid = monk.id(cast.$not._id) t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) -test('should cast oids inside $ne queries', (t) => { - const cast = users.cast({_id: {$ne: '4ee0fd75d6bd52107c000118'}}) +test('should cast ids inside $ne queries', (t) => { + const cast = monk.util.cast({_id: {$ne: '4ee0fd75d6bd52107c000118'}}) - const oid = users.id(cast._id.$ne) + const oid = monk.id(cast._id.$ne) t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) -test('should cast oids inside $in queries', (t) => { - const cast = users.cast({_id: {$in: ['4ee0fd75d6bd52107c000118']}}) +test('should cast ids inside $in queries', (t) => { + const cast = monk.util.cast({_id: {$in: ['4ee0fd75d6bd52107c000118']}}) - const oid = users.id(cast._id.$in[0]) + const oid = monk.id(cast._id.$in[0]) t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) -test('should cast oids inside $nin queries', (t) => { - const cast = users.cast({_id: {$nin: ['4ee0fd75d6bd52107c000118']}}) +test('should cast ids inside $nin queries', (t) => { + const cast = monk.util.cast({_id: {$nin: ['4ee0fd75d6bd52107c000118']}}) - const oid = users.id(cast._id.$nin[0]) + const oid = monk.id(cast._id.$nin[0]) t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) -test('should cast oids inside $set queries', (t) => { - const cast = users.cast({$set: {_id: '4ee0fd75d6bd52107c000118'}}) +test('should cast ids inside $set queries', (t) => { + const cast = monk.util.cast({$set: {_id: '4ee0fd75d6bd52107c000118'}}) - const oid = users.id(cast.$set._id) + const oid = monk.id(cast.$set._id) t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) -test('should cast oids inside $or', (t) => { - const cast = users.cast({ +test('should cast ids inside $or', (t) => { + const cast = monk.util.cast({ $or: [{_id: '4ee0fd75d6bd52107c000118'}] }) - const oid = users.id(cast.$or[0]._id) + const oid = monk.id(cast.$or[0]._id) t.is(oid.toHexString(), '4ee0fd75d6bd52107c000118') }) diff --git a/test/collection.js b/test/collection.js index 1722a695..b64b7fa4 100644 --- a/test/collection.js +++ b/test/collection.js @@ -142,6 +142,13 @@ test.cb('findById > callback', (t) => { }) }) +test('findOne > should return null if no document', (t) => { + return users.findOne({nonExistingField: true}) + .then((doc) => { + t.is(doc, null) + }) +}) + test('findOne > findOne(undefined) should not work', (t) => { return users.insert({ a: 'b', c: 'd', e: 'f' }).then((doc) => { return users.findOne() @@ -150,7 +157,7 @@ test('findOne > findOne(undefined) should not work', (t) => { }) }) -test('find > should only provide selected fields', (t) => { +test('findOne > should only provide selected fields', (t) => { return users.insert({ a: 'b', c: 'd', e: 'f' }).then((doc) => { return users.findOne(doc._id, 'a e') }).then((doc) => { @@ -181,6 +188,19 @@ test('find > should sort', (t) => { }) }) +test('find > should return the raw cursor', (t) => { + const query = { stream: 3 } + return users.insert([{ stream: 3 }, { stream: 3 }, { stream: 3 }, { stream: 3 }]).then(() => { + return users.find(query, {rawCursor: true}) + .then((cursor) => { + t.truthy(cursor.close) + t.truthy(cursor.pause) + t.truthy(cursor.resume) + cursor.close() + }) + }) +}) + test('find > should work with streaming', (t) => { const query = { stream: 1 } let found = 0 @@ -218,10 +238,10 @@ test('find > should allow stream cursor destroy', (t) => { return users.count(query).then((total) => { if (total <= 1) throw new Error('Bad test') return users.find(query) - .each((doc, destroy) => { + .each((doc, {close}) => { t.not(doc.cursor, null) found++ - if (found === 2) destroy() + if (found === 2) close() }) .then(() => { return new Promise((resolve) => { @@ -268,6 +288,18 @@ test('distinct', (t) => { }) }) +test('distinct with options', (t) => { + return users.insert([{ distinct2: 'a' }, { distinct2: 'a' }, { distinct2: 'b' }]).then(() => { + return users.distinct('distinct2', {}) + }).then((docs) => { + t.deepEqual(docs, ['a', 'b']) + }) +}) + +test.cb('distinct > with options callback', (t) => { + users.distinct('distinct', {}, t.end) +}) + test.cb('distinct > callback', (t) => { users.distinct('distinct', t.end) }) @@ -275,7 +307,7 @@ test.cb('distinct > callback', (t) => { test('update > should update', (t) => { return users.insert({ d: 'e' }).then((doc) => { return users.update({ _id: doc._id }, { $set: { d: 'f' } }).then(() => { - return users.findById(doc._id) + return users.findOne(doc._id) }) }).then((doc) => { t.is(doc.d, 'f') @@ -289,7 +321,7 @@ test.cb('update > callback', (t) => { test('updateById > should update by id', (t) => { return users.insert({ d: 'e' }).then((doc) => { return users.updateById(doc._id, { $set: { d: 'f' } }).then(() => { - return users.findById(doc._id) + return users.findOne(doc._id) }) }).then((doc) => { t.is(doc.d, 'f') @@ -297,13 +329,13 @@ test('updateById > should update by id', (t) => { }) test.cb('updateById > callback', (t) => { - users.updateById('xxxxxxxxxxxx', { $set: { d: 'f' } }, t.end) + users.updateById('aaaaaaaaaaaaaaaaaaaaaaaa', { $set: { d: 'f' } }, t.end) }) test('update > should update with an objectid', (t) => { return users.insert({ d: 'e' }).then((doc) => { return users.update(doc._id, { $set: { d: 'f' } }).then(() => { - return users.findById(doc._id) + return users.findOne(doc._id) }) }).then((doc) => { t.is(doc.d, 'f') @@ -313,7 +345,7 @@ test('update > should update with an objectid', (t) => { test('update > should update with an objectid (string)', (t) => { return users.insert({ d: 'e' }).then((doc) => { return users.update(doc._id.toString(), { $set: { d: 'f' } }).then(() => { - return users.findById(doc._id) + return users.findOne(doc._id) }) }).then((doc) => { t.is(doc.d, 'f') @@ -345,7 +377,7 @@ test('removeById > should remove a document by id', (t) => { }) test.cb('removeById > callback', (t) => { - users.removeById('xxxxxxxxxxxx', t.end) + users.removeById('aaaaaaaaaaaaaaaaaaaaaaaa', t.end) }) test('findAndModify > should alter an existing document', (t) => { @@ -361,6 +393,18 @@ test('findAndModify > should alter an existing document', (t) => { }) }) +test('findAndModify > should remove an existing document', (t) => { + const rand = 'now2-' + Date.now() + return users.insert({ find: rand }).then(() => { + return users.findAndModify({ find: rand }, {}, { remove: true }) + }).then((doc) => { + t.is(doc.find, rand) + return users.findOne(doc._id).then((found) => { + t.is(found, null) + }) + }) +}) + test('findAndModify > should accept an id as query param', (t) => { return users.insert({ locate: 'me' }).then((user) => { return users.findAndModify(user._id, { $set: { locate: 'you' } }).then(() => { @@ -402,12 +446,61 @@ test.cb('findAndModify > callback', (t) => { users.findAndModify({ query: {find: rand}, update: { find: rand } }, t.end) }) +test('findOneAndDelete > should remove a document and return it', (t) => { + return users.insert({ name: 'Bob' }).then((doc) => { + return users.findOneAndDelete({ name: 'Bob' }) + }).then((doc) => { + t.is(doc.name, 'Bob') + return users.find({ name: 'Bob' }) + }).then((doc) => { + t.deepEqual(doc, []) + }) +}) + +test.cb('findOneAndDelete > callback', (t) => { + users.insert({ name: 'Bob2' }).then((doc) => { + users.findOneAndDelete({ name: 'Bob2' }, (err, doc) => { + t.is(err, null) + t.is(doc.name, 'Bob2') + users.find({ name: 'Bob2' }).then((doc) => { + t.deepEqual(doc, []) + t.end() + }) + }) + }) +}) + +test('findOneAndUpdate > should update a document and return it', (t) => { + return users.insert({ name: 'Jack' }).then((doc) => { + return users.findOneAndUpdate({ name: 'Jack' }, { name: 'Jack4' }) + }).then((doc) => { + t.is(doc.name, 'Jack4') + }) +}) + +test.cb('findOneAndUpdate > callback', (t) => { + users.insert({ name: 'Jack2' }).then((doc) => { + users.findOneAndUpdate({ name: 'Jack2' }, { name: 'Jack3' }, (err, doc) => { + t.is(err, null) + t.is(doc.name, 'Jack3') + t.end() + }) + }) +}) + test('aggregate > should fail properly', (t) => { return users.aggregate().catch(() => { t.pass() }) }) +test.cb('aggregate > should fail properly with callback', (t) => { + users.aggregate(undefined, function (err) { + t.truthy(err) + t.end() + }) +}) + test('aggregate > should work in normal case', (t) => { return users.aggregate([{$group: {_id: null, maxWoot: { $max: '$woot' }}}]).then((res) => { t.true(Array.isArray(res)) @@ -418,7 +511,7 @@ test('aggregate > should work in normal case', (t) => { test('aggregate > should work with option', (t) => { return users.aggregate([{$group: {_id: null, maxWoot: { $max: '$woot' }}}], { explain: true }).then((res) => { t.true(Array.isArray(res)) - t.is(res.length, 2) + t.is(res.length, 1) }) }) @@ -456,3 +549,8 @@ test('drop > should not throw when dropping an empty db', (t) => { test.cb('drop > callback', (t) => { db.get('dropDB2-' + Date.now()).drop(t.end) }) + +test('Collection#id', (t) => { + const oid = users.id() + t.is(typeof oid.toHexString(), 'string') +}) diff --git a/test/monk.js b/test/monk.js index dd00b227..4fc2611b 100644 --- a/test/monk.js +++ b/test/monk.js @@ -21,13 +21,35 @@ test('Should throw if no uri provided', (t) => { }) test.cb('to a regular server', (t) => { - t.plan(1) - monk('127.0.0.1/monk-test', () => { - t.pass() + t.plan(2) + monk('127.0.0.1/monk-test', (err, db) => { + t.falsy(err) + t.true(db instanceof monk) t.end() }) }) +test('connect with promise', (t) => { + return monk('127.0.0.1/monk-test').then((db) => { + t.true(db instanceof monk) + }) +}) + +test.cb('should fail', (t) => { + t.plan(2) + monk('non-existent-db/monk-test', (err, db) => { + t.truthy(err) + t.true(db instanceof monk) + t.end() + }) +}) + +test('should fail with promise', (t) => { + return monk('non-existent-db/monk-test').catch((err) => { + t.truthy(err) + }) +}) + test.cb('to a replica set (array)', (t) => { t.plan(1) monk(['127.0.0.1/monk-test', 'localhost/monk-test'], () => { @@ -54,15 +76,53 @@ test.cb('followed by disconnection', (t) => { }) }) +test('executeWhenOpened > should reopen the connection if closed', (t) => { + const db = monk('127.0.0.1/monk') + return db + .then(() => t.is(db._state, 'open')) + .then(() => db.close(true)) + .then(() => t.is(db._state, 'closed')) + .then(() => db.executeWhenOpened()) + .then(() => t.is(db._state, 'open')) + .then(() => db.close()) +}) + +test('close > closing a closed connection should work', (t) => { + const db = monk('127.0.0.1/monk') + return db + .then(() => t.is(db._state, 'open')) + .then(() => db.close()) + .then(() => t.is(db._state, 'closed')) + .then(() => db.close()) +}) + +test.cb('close > closing a closed connection should work with callback', (t) => { + const db = monk('127.0.0.1/monk') + db.then(() => t.is(db._state, 'open')) + .then(() => db.close(() => { + t.is(db._state, 'closed') + db.close(() => t.end()) + })) +}) + +test('close > closing an opening connection should close it once opened', (t) => { + const db = monk('127.0.0.1/monk') + return db.close() +}) + const Collection = monk.Collection const db = monk('127.0.0.1/monk-test') +test('Manager#create', (t) => { + t.true(db.create('users') instanceof Collection) +}) + test('Manager#get', (t) => { - t.true(db.get('users')instanceof Collection) + t.true(db.get('users') instanceof Collection) }) test('Manager#col', (t) => { - t.true(db.col('users')instanceof Collection) + t.true(db.col('users') instanceof Collection) }) test('Manager#id', (t) => {