diff --git a/.eslintignore b/.eslintignore index 235bdcd4f7d..b03442e5d60 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,6 @@ docs/ bin/ test/triage/ +test/model.discriminator.test.js +tools/ +test/es6/ diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index b48ed430078..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,30 +0,0 @@ -extends: 'eslint:recommended' - -env: - node: true - mocha: true - -rules: - indent: - - 2 - - 2 - - SwitchCase: 1 - VariableDeclarator: 2 - no-trailing-spaces: 2 - comma-style: 2 - no-spaced-func: 2 - no-multi-spaces: 1 - space-after-keywords: 2 - space-before-blocks: 2 - space-before-function-paren: - - 2 - - never - space-before-keywords: 2 - space-infix-ops: 2 - space-return-throw-case: 2 - space-unary-ops: 1 - no-console: 0 - consistent-this: - - 1 - - _this - semi: 2 diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000000..013d2ec3dd1 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,31 @@ +extends: 'eslint:recommended' + +env: + node: true + mocha: true + +rules: + comma-style: error + consistent-this: + - error + - _this + indent: + - error + - 2 + - SwitchCase: 1 + VariableDeclarator: 2 + keyword-spacing: error + no-console: off + no-multi-spaces: error + no-spaced-func: error + no-trailing-spaces: error + quotes: + - error + - single + semi: error + space-before-blocks: error + space-before-function-paren: + - error + - never + space-infix-ops: error + space-unary-ops: error diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..31302f24782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,12 @@ + + +**Do you want to request a *feature* or report a *bug*?** + +**What is the current behavior?** + +**If the current behavior is a bug, please provide the steps to reproduce.** + + +**What is the expected behavior?** + +**Please mention your node.js, mongoose and MongoDB version.** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..65654629d59 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ + + +**Summary** + + + +**Test plan** + + diff --git a/.gitignore b/.gitignore index 6386a6bf953..28577b8fb07 100644 --- a/.gitignore +++ b/.gitignore @@ -10,13 +10,16 @@ benchmarks/v8.log .DS_Store docs/*.json docs/source/_docs -docs/*.html tags test/triage/*.js /.idea /inspections bin/mongoose.min.js coverage +npm-debug.log +data/ + +tools/31* # Visual Studio # ========= @@ -24,3 +27,14 @@ coverage *.ntvs* *.njsproj *.sln + +# Visual STudio Code +.vscode + +*.key + +docs/*.html + +test/files/main.js + +.config.js \ No newline at end of file diff --git a/.npmignore b/.npmignore index 377d63fa438..c69c61dc1b7 100644 --- a/.npmignore +++ b/.npmignore @@ -14,3 +14,10 @@ index.jade bin/ karma.*.js format_deps.js +tools/31* +*.key +data/ + +test/files + +.config.js \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index a8086538480..02eda0a1aeb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,26 @@ language: node_js sudo: false node_js: - - "4.0" + - "9" + - "8" + - "7" + - "6" + - "5" + - "4" - "0.12" - "0.10" - "iojs" -services: - - mongodb +before_script: + - wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.4.23.tgz + - tar -zxvf mongodb-linux-x86_64-3.4.23.tgz + - mkdir -p ./data/db/27017 + - mkdir -p ./data/db/27000 + - printf "\n--timeout 8000" >> ./test/mocha.opts + - ./mongodb-linux-x86_64-3.4.23/bin/mongod --fork --dbpath ./data/db/27017 --syslog --port 27017 + - export PATH=`pwd`/mongodb-linux-x86_64-3.4.23/bin/:$PATH + - sleep 3 +script: + - npm test + - npm run lint +notifications: + email: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 514657e448c..09988088b7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,58 +1,62 @@ ## Contributing to Mongoose -If you have a question about Mongoose (not a bug report) please post it to either [StackOverflow](http://stackoverflow.com/questions/tagged/mongoose), our [Google Group](http://groups.google.com/group/mongoose-orm), or on the #mongoosejs irc channel on freenode. +If you have a question about Mongoose (not a bug report) please post it to either [StackOverflow](http://stackoverflow.com/questions/tagged/mongoose), or on [Gitter](https://gitter.im/Automattic/mongoose?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ### Reporting bugs -- Before opening a new issue, look for existing [issues](https://github.com/learnboost/mongoose/issues) to avoid duplication. If the issue does not yet exist, [create one](https://github.com/learnboost/mongoose/issues/new). - - Please describe the issue you are experiencing, along with any associated stack trace. - - Please post code that reproduces the issue, the version of mongoose, node version, and mongodb version. - - _The source of this project is written in javascript, not coffeescript, therefore your bug reports should be written in javascript_. - - In general, adding a "+1" comment to an existing issue does little to help get it resolved. A better way is to submit a well documented pull request with clean code and passing tests. +- Before opening a new issue, look for existing [issues](https://github.com/Automattic/mongoose/issues) to avoid duplication. If the issue does not yet exist, [create one](https://github.com/Automattic/mongoose/issues/new). + - Please post any relevant code samples, preferably a standalone script that + reproduces your issue. Do **not** describe your issue in prose, show your + code. + - If the bug involves an error, please post the stack trace. + - Please post the version of mongoose and mongodb that you're using. + - Please write bug reports in JavaScript (ES5 or ES2015), not coffeescript, typescript, etc. ### Requesting new features - Before opening a new issue, look for existing [issues](https://github.com/learnboost/mongoose/issues) to avoid duplication. If the issue does not yet exist, [create one](https://github.com/learnboost/mongoose/issues/new). - Please describe a use case for it - it would be ideal to include test cases as well -- In general, adding a "+1" comment to an existing issue does little to help get it resolved. A better way is to submit a well documented pull request with clean code and passing tests. ### Fixing bugs / Adding features - Before starting to write code, look for existing [issues](https://github.com/learnboost/mongoose/issues). That way you avoid working on something that might not be of interest or that has been addressed already in a different branch. You can create a new issue [here](https://github.com/learnboost/mongoose/issues/new). - - _The source of this project is written in javascript, not coffeescript, therefore your bug reports should be written in javascript_. -- Fork the [repo](https://github.com/learnboost/mongoose) _or_ for small documentation changes, navigate to the source on github and click the [Edit](https://github.com/blog/844-forking-with-the-edit-button) button. + - _The source of this project is written in javascript, not coffeescript, therefore your bug reports should be written in javascript_. +- Fork the [repo](https://github.com/Automattic/mongoose) _or_ for small documentation changes, navigate to the source on github and click the [Edit](https://github.com/blog/844-forking-with-the-edit-button) button. - Follow the general coding style of the rest of the project: - 2 space tabs - no trailing whitespace - - comma first - - inline documentation for new methods, class members, etc - - 1 space between conditionals/functions, and their parenthesis and curly braces + - inline documentation for new methods, class members, etc. + - 1 space between conditionals, no space before function parenthesis - `if (..) {` - `for (..) {` - `while (..) {` - - `function (err) {` -- Write tests and make sure they pass (tests are in the [test](https://github.com/LearnBoost/mongoose/tree/master/test) directory). + - `function(err) {` +- Write tests and make sure they pass (tests are in the [test](https://github.com/Automattic/mongoose/tree/master/test) directory). ### Running the tests - Open a terminal and navigate to the root of the project - execute `npm install` to install the necessary dependencies -- execute `make test` to run the tests (we're using [mocha](http://visionmedia.github.com/mocha/)) - - or to execute a single test `T="-g 'some regexp that matches the test description'" make test` - - any mocha flags can be specified with `T="..."` +- start a mongodb instance on port 27017 if one isn't running already. `mongod --dbpath --port 27017` +- execute `npm test` to run the tests (we're using [mocha](http://mochajs.org/)) + - or to execute a single test `npm test -- -g 'some regexp that matches the test description'` + - any mocha flags can be specified with `-- ` + - For example, you can use `npm test -- -R spec` to use the spec reporter, rather than the dot reporter (by default, the test output looks like a bunch of dots) ### Documentation -To contribute to the [API documentation](http://mongoosejs.com/docs/api.html) just make your changes to the inline documentation of the appropriate [source code](https://github.com/LearnBoost/mongoose/tree/master/lib) in the master branch and submit a [pull request](https://help.github.com/articles/using-pull-requests/). You might also use the github [Edit](https://github.com/blog/844-forking-with-the-edit-button) button. +To contribute to the [API documentation](http://mongoosejs.com/docs/api.html) just make your changes to the inline documentation of the appropriate [source code](https://github.com/Automattic/mongoose/tree/master/lib) in the master branch and submit a [pull request](https://help.github.com/articles/using-pull-requests/). You might also use the github [Edit](https://github.com/blog/844-forking-with-the-edit-button) button. -To contribute to the [guide](http://mongoosejs.com/docs/guide.html) or [quick start](http://mongoosejs.com/docs/index.html) docs, make your changes to the appropriate `.jade` files in the [docs](https://github.com/LearnBoost/mongoose/tree/master/docs) directory of the master branch and submit a pull request. Again, the [Edit](https://github.com/blog/844-forking-with-the-edit-button) button might work for you here. +To contribute to the [guide](http://mongoosejs.com/docs/guide.html) or [quick start](http://mongoosejs.com/docs/index.html) docs, make your changes to the appropriate `.jade` files in the [docs](https://github.com/Automattic/mongoose/tree/master/docs) directory of the master branch and submit a pull request. Again, the [Edit](https://github.com/blog/844-forking-with-the-edit-button) button might work for you here. -If you'd like to preview your documentation changes, first commit your changes to your local master branch, then execute `make docs` from the project root, which switches to the gh-pages branch, merges from the master branch and builds all the static pages for you. Now execute `node static.js` from the project root which will launch a local webserver where you can browse the documentation site locally. If all looks good, submit a [pull request](https://help.github.com/articles/using-pull-requests/) to the master branch with your changes. +If you'd like to preview your documentation changes, first commit your changes to your local master branch, then execute: -### Plugins website +* `make docclean` +* `make gendocs` +* `node static.js` -The [plugins](http://plugins.mongoosejs.com/) site is also an [open source project](https://github.com/aheckmann/mongooseplugins) that you can get involved with. Feel free to fork and improve it as well! +Visit `http://localhost:8088` and you should see the docs with your local changes. Make sure you `git reset --hard` before commiting, because changes to `docs/*` should **not** be in PRs. -### Sharing your projects +### Plugins website -All are welcome to share their creations which use mongoose on our [tumbler](http://mongoosejs.tumblr.com/). Just fill out the [simple submission form](http://mongoosejs.tumblr.com/submit). +The [plugins](http://plugins.mongoosejs.io/) site is also an [open source project](https://github.com/vkarpov15/mongooseplugins) that you can get involved with. Feel free to fork and improve it as well! diff --git a/History.md b/History.md index c683bc258d4..294b1e0ce5c 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,1345 @@ +4.13.20 / 2020-01-07 +==================== + * fix(schema): make aliases handle mongoose-lean-virtuals #6069 + +4.13.19 / 2019-07-02 +==================== + * fix(aggregate): make `setOptions()` work as advertised #7950 #6011 [cdimitroulas](https://github.com/cdimitroulas) + +4.13.18 / 2019-01-21 +==================== + * fix(model): handle setting populated path set via `Document#populate()` #7302 + * fix(cast): backport fix from #7290 to 4.x + +4.13.17 / 2018-08-30 +==================== + * fix(document): disallow setting `constructor` and `prototype` if strict mode false + +4.13.16 / 2018-08-30 +==================== + * fix(document): disallow setting __proto__ if strict mode false + * feat(error): backport adding modified paths to VersionError #6928 [freewil](https://github.com/freewil) + +4.13.15 / 2018-08-14 +==================== + * fix(mongoose): add global `usePushEach` option for easier Mongoose 4.x + MongoDB 3.6 #6858 + * chore: fix flakey tests for 4.x #6853 [Fonger](https://github.com/Fonger) + * feat(error): add version number to VersionError #6852 [freewil](https://github.com/freewil) + +4.13.14 / 2018-05-25 +==================== + * fix(model): handle retainKeyOrder option in findOneAndUpdate() #6484 + +4.13.13 / 2018-05-17 +==================== + * fix(update): stop clobbering $in when casting update #6441 #6339 + * fix: upgrade async -> 2.6.0 re: security warning + +4.13.12 / 2018-03-13 +==================== + * fix(document): make virtual get() return undefined instead of null if no getters #6223 + * docs: fix url in useMongoClient error message #6219 #6217 [lineus](https://github.com/lineus) + * fix(discriminator): don't copy `discriminators` property from base schema #6122 #6064 + +4.13.11 / 2018-02-07 +==================== + * docs: fix links in 4.x docs #6081 + * chore: add release script that uses --tag for npm publish for 4.x releases #6063 + +4.13.10 / 2018-01-28 +==================== + * docs(model+query): add lean() option to Model helpers #5996 [aguyinmontreal](https://github.com/aguyinmontreal) + * fix: use lazy loading so we can build mongoose with webpack #5993 #5842 + * docs(connections): clarify multi-mongos with useMongoClient for 4.x docs #5984 + * fix(populate): handle populating embedded discriminator paths #5970 + * docs(query+aggregate): add more detail re: maxTimeMS #4066 + +4.13.9 / 2018-01-07 +=================== + * chore: update marked (dev dependency) re: security vulnerability #5951 [ChristianMurphy](https://github.com/ChristianMurphy) + * fix: upgrade mongodb -> 2.2.34 for ipv6 and autoReconnect fixes #5794 #5760 + * docs: use useMongooseAggCursor for aggregate docs #2955 + +4.13.8 / 2017-12-27 +=================== + * docs(guide): use more up-to-date syntax for autoIndex example #5933 + * docs: fix grammar #5927 [abagh0703](https://github.com/abagh0703) + * fix: propagate lean options to child schemas #5914 + * fix(populate): use correct model with discriminators + nested populate #5858 + +4.13.7 / 2017-12-11 +=================== + * docs(schematypes): fix typo #5889 [gokaygurcan](https://github.com/gokaygurcan) + * fix(cursor): handle `reject(null)` with eachAsync callback #5875 #5874 [ZacharyRSmith](https://github.com/ZacharyRSmith) + * fix: disallow setting `mongoose.connection` to invalid values #5871 [jinasonlin](https://github.com/jinasonlin) + * docs(middleware): suggest using `return next()` to stop middleware execution #5866 + * docs(connection): improve connection string query param docs #5864 + * fix(document): run validate hooks on array subdocs even if not directly modified #5861 + * fix(discriminator): don't treat $meta as defining projection when querying #5859 + * fix(types): handle Decimal128 when using bson-ext on server side #5850 + * fix(document): ensure projection with only $slice isn't treated as inclusive for discriminators #4991 + * fix(model): throw error when passing non-object to create() #2037 + +4.13.6 / 2017-12-02 +=================== + * fix(schema): support strictBool option in schema #5856 [ekulabuhov](https://github.com/ekulabuhov) + * fix(update): make upsert option consistently handle truthy values, not just booleans, for updateOne() #5839 + * refactor: remove unnecessary constructor check #2057 + * docs(query): correct function signature for .mod() helper #1806 + * fix(query): report ObjectParameterError when passing non-object as filter to find() and findOne() #1698 + +4.13.5 / 2017-11-24 +=================== + * fix(model): handle update cast errors correctly with bulkWrite #5845 [Michael77](https://github.com/Michael77) + * docs: add link to bufferCommands option #5844 [ralphite](https://github.com/ralphite) + * fix(model): allow virtual ref function to return arrays #5834 [brunohcastro](https://github.com/brunohcastro) + * fix(query): don't throw uncaught error if query filter too big #5812 + * fix(document): if setting unselected nested path, don't overwrite nested path #5800 + * fix(document): support calling `populate()` on nested document props #5703 + * fix: add `strictBool` option for schema type boolean #5344 #5211 #4245 + * docs(faq): add faq re: typeKey #1886 + * docs(query): add more detailed docs re: options #1702 + +4.13.4 / 2017-11-17 +=================== + * fix(aggregate): add chainable .option() helper for setting arbitrary options #5829 + * fix(aggregate): add `.pipeline()` helper to get the current pipeline #5825 + * docs: grammar fixes for `unique` FAQ #5823 [mfluehr](https://github.com/mfluehr) + * chore: add node 9 to travis #5822 [superheri](https://github.com/superheri) + * fix(model): fix infinite recursion with recursive embedded discriminators #5821 [Faibk](https://github.com/Faibk) + +4.13.3 / 2017-11-15 +=================== + * chore: add node 8 to travis #5818 [superheri](https://github.com/superheri) + * fix(document): don't apply transforms to nested docs when updating already saved doc #5807 + +4.13.2 / 2017-11-11 +=================== + * feat(buffer): add support for subtype prop #5530 + +4.13.1 / 2017-11-08 +=================== + * fix: accept multiple paths or array of paths to depopulate #5798 #5797 [adamreisnz](https://github.com/adamreisnz) + * fix(document): pass default array as actual array rather than taking first element #5780 + * fix(model): increment version when $set-ing it in a save() that requires a version bump #5779 + * fix(query): don't explicitly project in discriminator key if user projected in parent path #5775 #5754 + * fix(model): cast query option to geoNear() #5765 + * fix(query): don't treat projection with just $slice as inclusive #5737 + * fix(discriminator): defer applying embedded discriminator hooks until top-level model is compiled #5706 + * docs(discriminator): add warning to always attach hooks before calling discriminator() #5706 + +4.13.0 / 2017-11-02 +=================== + * feat(aggregate): add $addFields helper #5740 [AyushG3112](https://github.com/AyushG3112) + * feat(connection): add connection-level bufferCommands #5720 + * feat(connection): add createCollection() helper #5712 + * feat(populate): support setting localField and foreignField to functions #5704 #5602 + * feat(query): add multipleCastError option for aggregating cast errors when casting update #5609 + * feat(populate): allow passing a function to virtual ref #5602 + * feat(schema): add excludeIndexes option to optionally prevent collecting indexes from nested schemas #5575 + * feat(model): report validation errors from `insertMany()` if using `ordered: false` and `rawResult: true` #5337 + * feat(aggregate): add pre/post aggregate middleware #5251 + * feat(schema): allow using `set` as a schema path #1939 + +4.12.6 / 2017-11-01 +=================== + * fix(schema): make clone() copy query helpers correctly #5752 + * fix: undeprecate `ensureIndex()` and use it by default #3280 + +4.12.5 / 2017-10-29 +=================== + * fix(query): correctly handle `$in` and required for $pull and update validators #5744 + * feat(aggegate): add $addFields pipeline operator #5740 [AyushG3112](https://github.com/AyushG3112) + * fix(document): catch sync errors in document pre hooks and report as error #5738 + * fix(populate): handle slice projections correctly when automatically selecting populated fields #5737 + * fix(discriminator): fix hooks for embedded discriminators #5706 [wlingke](https://github.com/wlingke) + * fix(model): throw sane error when customer calls `mongoose.Model()` over `mongoose.model()` #2005 + +4.12.4 / 2017-10-21 +=================== + * test(plugins): add coverage for idGetter with id as a schema property #5713 [wlingke](https://github.com/wlingke) + * fix(model): avoid copying recursive $$context object when creating discriminator after querying #5721 + * fix(connection): ensure connection promise helpers are removed before emitting 'connected' #5714 + * docs(schema): add notes about runSettersOnQuery to schema setters #5705 + * fix(collection): ensure queued operations run on the next tick #5562 + +4.12.3 / 2017-10-16 +=================== + * fix(connection): emit 'reconnect' event as well as 'reconnected' for consistency with driver #5719 + * fix: correctly bubble up left/joined events for replica set #5718 + * fix(connection): allow passing in `autoIndex` as top-level option rather than requiring `config.autoIndex` #5711 + * docs(connection): improve docs regarding reconnectTries, autoReconnect, and bufferMaxEntries #5711 + * fix(query): handle null with addToSet/push/pull/pullAll update validators #5710 + * fix(model): handle setDefaultsOnInsert option for bulkWrite updateOne and updateMany #5708 + * fix(query): avoid infinite recursion edge case when cloning a buffer #5702 + +4.12.2 / 2017-10-14 +=================== + * docs(faq): add FAQ about using arrow functions for getters/setters, virtuals, and methods #5700 + * docs(schema): document the childSchemas property and add to public API #5695 + * fix(query): don't project in populated field if parent field is already projected in #5669 + * fix: bump mongodb -> 2.2.33 for issue with autoReconnect #4513 + +4.12.1 / 2017-10-08 +=================== + * fix(document): create new doc when setting single nested, no more set() on copy of priorVal #5693 + * fix(model): recursively call applyMethods on child schemas for global plugins #5690 + * docs: fix bad promise lib example on home page #5686 + * fix(query): handle false when checking for inclusive/exclusive projection #5685 + * fix(discriminator): allow reusing child schema #5684 + * fix: make addToSet() on empty array with subdoc trigger manual population #5504 + +4.12.0 / 2017-10-02 +=================== + * docs(validation): add docs coverage for ValidatorError.reason #5681 + * feat(discriminator): always add discriminatorKey to base schema to allow updating #5613 + * fix(document): make nested docs no longer inherit parent doc's schema props #5586 #5546 #5470 + * feat(query): run update validators on $pull and $pullAll #5555 + * feat(query): add .error() helper to query to error out in pre hooks #5520 + * feat(connection): add dropCollection() helper #5393 + * feat(schema): add schema-level collation option #5295 + * feat(types): add `discriminator()` function for single nested subdocs #5244 + * feat(document): add $isDeleted() getter/setter for better support for soft deletes #4428 + * feat(connection): bubble up reconnectFailed event when driver gives up reconnecting #4027 + * fix(query): report error if passing array or other non-object as filter to update query #3677 + * fix(collection): use createIndex() instead of deprecated ensureIndex() #3280 + +4.11.14 / 2017-09-30 +==================== + * chore: add nsp check to the CI build #5679 [hairyhenderson](https://github.com/hairyhenderson) + * fix: bump mquery because of security issue with debug package #5677 #5675 [jonathanprl](https://github.com/jonathanprl) + * fix(populate): automatically select() populated()-ed fields #5669 + * fix(connection): make force close work as expected #5664 + * fix(document): treat $elemMatch as inclusive projection #5661 + * docs(model/query): clarify which functions fire which middleware #5654 + * fix(model): make `init()` public and return a promise that resolves when indexes are done building #5563 + +4.11.13 / 2017-09-24 +==================== + * fix(query): correctly run replaceOne with update validators #5665 [sime1](https://github.com/sime1) + * fix(schema): replace mistype in setupTimestamp method #5656 [zipp3r](https://github.com/zipp3r) + * fix(query): avoid throwing cast error for strict: throw with nested id in query #5640 + * fix(model): ensure class gets combined schema when using class syntax with discriminators #5635 + * fix(document): handle setting doc array to array of top-level docs #5632 + * fix(model): handle casting findOneAndUpdate() with overwrite and upsert #5631 + * fix(update): correctly handle $ in updates #5628 + * fix(types): handle manual population consistently for unshift() and splice() #5504 + +4.11.12 / 2017-09-18 +==================== + * docs(model): asterisk should not render as markdown bullet #5644 [timkinnane](https://github.com/timkinnane) + * docs: use useMongoClient in connection example #5627 [GabrielNicolasAvellaneda](https://github.com/GabrielNicolasAvellaneda) + * fix(connection): call callback when initial connection failed #5626 + * fix(query): apply select correctly if a given nested schema is used for 2 different paths #5603 + * fix(document): add graceful fallback for setting a doc array value and `pull()`-ing a doc #3511 + +4.11.11 / 2017-09-10 +==================== + * fix(connection): properly set readyState in response to driver 'close' and 'reconnect' events #5604 + * fix(document): ensure single embedded doc setters only get called once, with correct value #5601 + * fix(timestamps): allow enabling updatedAt without createdAt #5598 + * test: improve unique validator test by making create run before ensureIndex #5595 #5562 + * fix(query): ensure find callback only gets called once when post init hook throws error #5592 + +4.11.10 / 2017-09-03 +==================== + * docs: add KeenIO tracking #5612 + * fix(schema): ensure validators declared with `.validate()` get copied with clone() #5607 + * fix: remove unnecessary jest warning #5480 + * fix(discriminator): prevent implicit discriminator schema id from clobbering base schema custom id #5591 + * fix(schema): hide schema objectid warning for non-hex strings of length 24 #5587 + * docs(populate): use story schema defined key author instead of creator #5578 [dmric](https://github.com/dmric) + * docs(document): describe usage of `.set()` #5576 + * fix(document): ensure correct scope in single nested validators #5569 + * fix(populate): don't mark path as populated until populate() is done #5564 + * fix(document): make push()-ing a doc onto an empty array act as manual population #5504 + * fix(connection): emit timeout event on socket timeout #4513 + +4.11.9 / 2017-08-27 +=================== + * fix(error): avoid using arguments.callee because that breaks strict mode #5572 + * docs(schematypes): fix spacing #5567 + * fix(query): enforce binary subtype always propagates to mongodb #5551 + * fix(query): only skip castForQuery for mongoose arrays #5536 + * fix(browser): rely on browser entrypoint to decide whether to use BrowserDocument or NodeDocument #5480 + +4.11.8 / 2017-08-23 +=================== + * feat: add warning about using schema ObjectId as type ObjectId #5571 [efkan](https://github.com/efkan) + * fix(schema): allow setting `id` property after schema was created #5570 #5548 + * docs(populate): remove confusing _ from populate docs #5560 + * fix(connection): expose parsed uri fields (host, port, dbname) when using openUri() #5556 + * docs: added type boolean to options documentation #5547 [ndabAP](https://github.com/ndabAP) + * test: add test coverage for stopping/starting server #5524 + * fix(aggregate): pull read preference from schema by default #5522 + +4.11.7 / 2017-08-14 +=================== + * fix: correct properties when calling toJSON() on populated virtual #5544 #5442 [davidwu226](https://github.com/davidwu226) + * docs: fix spelling #5535 [et](https://github.com/et) + * fix(error): always set name before stack #5533 + * fix: add warning about running jest in jsdom environment #5532 #5513 #4943 + * fix(document): ensure overwriting a doc array cleans out individual docs #5523 + * fix(schema): handle creating arrays of single nested using type key #5521 + * fix: upgrade mongodb -> 2.2.31 to support user/pass options #5419 + +4.11.6 / 2017-08-07 +=================== + * fix: limiting number of async operations per time in insertMany #5529 [andresattler](https://github.com/andresattler) + * fix: upgrade mongodb -> 2.2.30 #5517 + * fix(browserDocument): prevent stack overflow caused by double-wrapping embedded doc save() in jest #5513 + * fix(document): clear single nested doc when setting to empty object #5506 + * fix(connection): emit reconnected and disconnected events correctly with useMongoClient #5498 + * fix(populate): ensure nested virtual populate gets set even if top-level property is null #5431 + +4.11.5 / 2017-07-30 +=================== + * docs: fix link to $lookup #5516 [TalhaAwan](https://github.com/TalhaAwan) + * fix: better parallelization for eachAsync #5502 [lchenay](https://github.com/lchenay) + * docs(document): copy docs for save from model to doc #5493 + * fix(document): handle dotted virtuals in toJSON output #5473 + * fix(populate): restore user-provided limit after mutating so cursor() works with populate limit #5468 + * fix(query): don't throw StrictModeError if geo query with upsert #5467 + * fix(populate): propagate readPreference from query to populate queries by default #5460 + * docs: warn not to use arrow functions for statics and methods #5458 + * fix(query): iterate over all condition keys for setDefaultsOnInsert #5455 + * docs(connection): clarify server/replset/mongos option deprecation with useMongoClient #5442 + +4.11.4 / 2017-07-23 +=================== + * fix: handle next() errors in `eachAsync()` #5486 [lchenay](https://github.com/lchenay) + * fix(schema): propagate runSettersOnQuery option to implicitly created schemas #5479 [https://github.com/ValYouW] + * fix(query): run castConditions() correctly in update ops #5477 + * fix(query): ensure castConditions called for findOne and findOneAnd* #5477 + * docs: clarify relationship between $lookup and populate #5475 [TalhaAwan](https://github.com/TalhaAwan) + * test: add coverage for arrays of arrays [zbjornson](https://github.com/zbjornson) + * fix(middleware): ensure that error handlers for save get doc as 2nd param #5466 + * fix: handle strict: false correctly #5454 #5453 [wookieb](https://github.com/wookieb) + * fix(query): apply schema excluded paths if only projection is a $slice #5450 + * fix(query): correct discriminator handling for schema `select: false` fields in schema #5448 + * fix(cursor): call next() in series when parallel option used #5446 + * chore: load bundled driver first to avoid packaging problem #5443 [prototypeme](https://github.com/prototypeme) + * fix(query): defer condition casting until final exec #5434 + * fix(aggregate): don't rely on mongodb aggregate to put a cursor in the callback #5394 + * docs(aggregate): add useMongooseAggCursor docs #5394 + * docs(middleware): clarify context for document, query, and model middleware #5381 + +4.11.3 / 2017-07-14 +=================== + * fix(connection): remove .then() before resolving to prevent infinite recursion #5471 + +4.11.2 / 2017-07-13 +=================== + * docs: fix comment typo in connect example #5435 [ConnorMcF](https://github.com/ConnorMcF) + * fix(update): correctly cast document array in update validators with exec() #5430 + * fix(connection): handle autoIndex with useMongoClient #5423 + * fix(schema): handle `type: [Array]` in schemas #5416 + * fix(timestamps): if overwrite is set and there's a $set, use $set instead of top-level update #5413 + * fix(document): don't double-validate deeply nested doc array elements #5411 + * fix(schematype): clone default objects so default not shared across object instances unless `shared` specified #5407 + * fix(document): reset down the nested subdocs when resetting parent doc #5406 + * fix: don't pass error arg twice to error handlers #5405 + * fix(connection): make openUri() return connection decorated with then() and catch() #5404 + * fix: enforce $set on an array must be an array #5403 + * fix(document): don't crash if calling `validateSync()` after overwriting doc array index #5389 + * fix(discriminator): ensure discriminator key doesn't count as user-selected field for projection #4629 + +4.11.1 / 2017-07-02 +=================== +* docs: populate virtuals fix justOne description #5427 [fredericosilva](https://github.com/fredericosilva) + * fix(connection): make sure to call onOpen in openUri() #5404 + * docs(query): justOne is actually single, and it default to false #5402 [zbjornson](https://github.com/zbjornson) + * docs: fix small typo in lib/schema.js #5398 #5396 [pjo336](https://github.com/pjo336) + * fix: emit remove on single nested subdocs when removing parent #5388 + * fix(update): handle update with defaults and overwrite but no update validators #5384 + * fix(populate): handle undefined refPath values in middle of array #5377 + * fix(document): ensure consistent setter context for single nested #5363 + * fix(query): support runSettersOnQuery as query option #5350 + +4.11.0 / 2017-06-25 +=================== + * feat(query): execute setters with query as context for `runSettersOnQuery` #5339 + * feat(model): add translateAliases function #5338 [rocketspacer](https://github.com/rocketspacer) + * feat(connection): add `useMongoClient` and `openUri` functions, deprecate current connect logic #5304 + * refactor(schema): make id virtual not access doc internals #5279 + * refactor: handle non-boolean lean #5279 + * feat(cursor): add addCursorFlag() support to query and agg cursors #4814 + * feat(cursor): add parallel option to eachAsync #4244 + * feat(schema): allow setting custom error constructor for custom validators #4009 + +4.10.8 / 2017-06-21 +=================== + * docs: fix small formatting typo on schematypes #5374 [gianpaj](https://github.com/gianpaj) + * fix(model): allow null as an _id #5370 + * fix(populate): don't throw async uncaught exception if model not found in populate #5364 + * fix: correctly cast decimals in update #5361 + * fix(error): don't use custom getter for ValidationError message #5359 + * fix(query): handle runSettersOnQuery in built-in _id setter #5351 + * fix(document): ensure consistent context for nested doc custom validators #5347 + +4.10.7 / 2017-06-18 +=================== + * docs(validation): show overriding custom validator error with 2nd cb arg #5358 + * fix: `parseOption` mutates user passed option map #5357 [igwejk](https://github.com/igwejk) + * docs: fix guide.jade typo #5356 [CalebAnderson2014](https://github.com/CalebAnderson2014) + * fix(populate): don't set populate virtual to ids when match fails #5336 + * fix(query): callback with cast error if remove and delete* args have a cast error #5323 + +4.10.6 / 2017-06-12 +=================== + * fix(cursor): handle custom model option for populate #5334 + * fix(populate): handle empty virtual populate with Model.populate #5331 + * fix(model): make ensureIndexes() run with autoIndex: false unless called internally #5328 #5324 #5317 + * fix: wait for all connections to close before resolving disconnect() promise #5316 + * fix(document): handle setting populated path with custom typeKey in schema #5313 + * fix(error): add toJSON helper to ValidationError so `message` shows up with JSON.stringify #5309 + * feat: add `getPromiseConstructor()` to prevent need for `mongoose.Promise.ES6` #5305 + * fix(document): handle conditional required with undefined props #5296 + * fix(model): clone options before inserting in save() #5294 + * docs(populate): clarify that multiple populate() calls on same path overwrite #5274 + +4.10.5 / 2017-06-06 +=================== + * chore: improve contrib guide for building docs #5312 + * fix(populate): handle init-ing nested virtuals properly #5311 + * fix(update): report update validator error if required path under single nested doc not set + * fix(schema): remove default validate pre hook that was causing issues with jest #4943 + +4.10.4 / 2017-05-29 +=================== + * chore: dont store test data in same directory #5303 + * chore: add data dirs to npmignore #5301 [Starfox64](https://github.com/Starfox64) + * docs(query): add docs about runSettersOnQuery #5300 + +4.10.3 / 2017-05-27 +=================== + * docs: correct inconsistent references to updateOne and replaceOne #5297 [dhritzkiv](https://github.com/dhritzkiv) + * docs: fix dropdowns in docs #5292 [nathanallen](https://github.com/nathanallen) + * docs: add description of alias option #5287 + * fix(document): prevent infinite loop if validating nested array #5282 + * fix(schema): correctly handle ref ObjectIds from different mongoose libs #5259 + * fix(schema): load child class methods after base class methods to allow override #5227 + +4.10.2 / 2017-05-22 +=================== + * fix: bump ms -> 2.0.0 and mquery -> 2.3.1 for minor security vulnerability #5275 + +4.10.1 / 2017-05-21 +=================== + * fix(aggregate): handle sorting by text score correctly #5258 + * fix(populate): handle doc.populate() with virtuals #5240 + * fix(schema): enforce that `_id` is never null #5236 + +4.10.0 / 2017-05-18 +=================== + * fix(schema): update clone method to include indexes #5268 [clozanosanchez](https://github.com/clozanosanchez) + * feat(schema): support aliases #5184 [rocketspacer](https://github.com/rocketspacer) + * feat(aggregate): add mongoose-specific aggregation cursor option #5145 + * refactor(model): make sharding into a plugin instead of core #5105 + * fix(document): make nested doc mongoose internals not enumerable again #5078 + * feat(model): pass params to pre hooks #5064 + * feat(timestamps): support already defined timestamp paths in schema #4868 + * feat(query): add runSettersOnQuery option #4569 + * fix(query): add strictQuery option that throws when not querying on field not in schema #4136 + * fix(update): more complete handling for overwrite option with update validators #3556 + * feat: support `unique: true` in arrays via the mongoose-unique-array plugin #3347 + * fix(model): always emit 'index', even if no indexes #3347 + * fix(schema): set unique indexes on primitive arrays #3347 + * feat(validation): include failed paths in error message and inspect output #3064 #2135 + * fix(model): return saved docs when create() fails #2190 + +4.9.10 / 2017-05-17 +=================== + * fix(connection): ensure callback arg to openSet() is handled properly #5249 + * docs: remove dead plugins repo and add content links #5247 + * fix(model): skip index build if connecting after model init and autoIndex false #5176 + +4.9.9 / 2017-05-13 +================== + * docs: correct value for Query#regex() #5230 + * fix(connection): don't throw if .catch() on open() promise #5229 + * fix(schema): allow update with $currentDate for updatedAt to succeed #5222 + * fix(model): versioning doesn't fail if version key undefined #5221 [basileos](https://github.com/basileos) + * fix(document): don't emit model error if callback specified for consistency with docs #5216 + * fix(document): handle errors in subdoc pre validate #5215 + +4.9.8 / 2017-05-07 +================== + * docs(subdocs): rewrite subdocs guide #5217 + * fix(document): avoid circular JSON if error in doc array under single nested subdoc #5208 + * fix(document): set intermediate empty objects for deeply nested undefined paths before path itself #5206 + * fix(schema): throw error if first param to schema.plugin() is not a function #5201 + * perf(document): major speedup in validating subdocs (50x in some cases) #5191 + +4.9.7 / 2017-04-30 +================== + * docs: fix typo #5204 [phutchins](https://github.com/phutchins) + * fix(schema): ensure correct path for deeply nested schema indexes #5199 + * fix(schema): make remove a reserved name #5197 + * fix(model): handle Decimal type in insertMany correctly #5190 + * fix: upgrade kareem to handle async pre hooks correctly #5188 + * docs: add details about unique not being a validator #5179 + * fix(validation): handle returning a promise with isAsync: true #5171 + +4.9.6 / 2017-04-23 +================== + * fix: update `parentArray` references when directly assigning document arrays #5192 [jhob](https://github.com/jhob) + * docs: improve schematype validator docs #5178 [milesbarr](https://github.com/milesbarr) + * fix(model): modify discriminator() class in place #5175 + * fix(model): handle bulkWrite updateMany casting #5172 [tzellman](https://github.com/tzellman) + * docs(model): fix replaceOne example for bulkWrite #5168 + * fix(document): don't create a new array subdoc when creating schema array #5162 + * fix(model): merge query hooks from discriminators #5147 + * fix(document): add parent() function to subdocument to match array subdoc #5134 + +4.9.5 / 2017-04-16 +================== + * fix(query): correct $pullAll casting of null #5164 [Sebmaster](https://github.com/Sebmaster) + * docs: add advanced schemas docs for loadClass #5157 + * fix(document): handle null/undefined gracefully in applyGetters() #5143 + * fix(model): add resolveToObject option for mapReduce with ES6 promises #4945 + +4.9.4 / 2017-04-09 +================== + * fix(schema): clone query middleware correctly #5153 #5141 [clozanosanchez](https://github.com/clozanosanchez) + * docs(aggregate): fix typo #5142 + * fix(query): cast .$ update to underlying array type #5130 + * fix(populate): don't mutate populate result in place #5128 + * fix(query): handle $setOnInsert consistent with $set #5126 + * docs(query): add strict mode option for findOneAndUpdate #5108 + +4.9.3 / 2017-04-02 +================== + * docs: document.js fixes for functions prepended with `$` #5131 [krmannix](https://github.com/krmannix) + * fix: Avoid exception on constructor check #5129 [monkbroc](https://github.com/monkbroc) + * docs(schematype): explain how to use `isAsync` with validate() #5125 + * docs(schematype): explain custom message with required function #5123 + * fix(populate): only apply refPath duplicate id optimization if not array #5114 + * fix(document): copy non-objects to doc when init() #5111 + * perf(populate): dont clone whole options every time #5103 + * feat(document): add isDirectSelected() to minimize isSelected() changes #5063 + * docs(schematypes): explain some subtleties with arrays #5059 + +4.9.2 / 2017-03-26 +================== + * fix(discriminator): handle class names consistently #5104 + * fix(schema): make clone() work with reusing discriminator schemas #5098 + * fix(querycursor): run pre find hooks with .cursor() #5096 + * fix(connection): throw error if username:password includes @ or : #5091 + * fix(timestamps): handle overwriting createdAt+updatedAt consistently #5088 + * fix(document): ensure subdoc post save runs after parent save #5085 + * docs(model): improve update docs #5076 [bertolo1988](https://github.com/bertolo1988) + +4.9.1 / 2017-03-19 +================== + * fix(query): handle $type for arrays #5080 #5079 [zoellner](https://github.com/zoellner) + * fix(model): handle ordered param for `insertMany` validation errors #5072 [sjorssnoeren](https://github.com/sjorssnoeren) + * fix(populate): avoid duplicate ids in dynref queries #5054 + * fix(timestamps): dont set timestamps in update if user set it #5045 + * fix(update): dont double-call setters on arrays #5041 + * fix: upgrade driver -> 2.2.25 for jest fix #5033 + * fix(model): get promise each time save() is called rather than once #5030 + * fix(connection): make connect return value consistent #5006 + +4.9.0 / 2017-03-13 +================== + * feat(document): return this from `depopulate()` #5027 + * fix(drivers): stop emitting timeouts as errors #5026 + * feat(schema): add a clone() function for schemas #4983 + * feat(query): add rawResult option to replace passRawResult, deprecate passRawResult #4977 #4925 + * feat(schematype): support isAsync validator option and handle returning promises from validators, deprecate implicit async validators #4290 + * feat(query): add `replaceOne()`, `deleteOne()`, `deleteMany()` #3998 + * feat(model): add `bulkWrite()` #3998 + +4.8.7 / 2017-03-12 +================== + * fix(model): if last arg in spread is falsy, treat it as a callback #5061 + * fix(document): use $hook instead of hook to enable 'hook' as a path name #5047 + * fix(populate): dont select foreign field if parent field is selected #5037 + * fix(populate): handle passing no args to query.populate #5036 + * fix(update): use correct method for casting nested arrays #5032 + * fix(discriminator): handle array discriminators when casting $push #5009 + +4.8.6 / 2017-03-05 +================== + * docs(document): remove text that implies that transform is false by default #5023 + * fix(applyHooks): dont wrap a function if it is already wrapped #5019 + * fix(document): ensure nested docs' toObject() clones #5008 + +4.8.5 / 2017-02-25 +================== + * fix: check for empty schemaPath before accessing property $isMongooseDocumentArray #5017 [https://github.com/randyhoulahan](randyhoulahan) + * fix(discriminators): handle create() and push() for embedded discriminators #5001 + * fix(querycursor): ensure close emitted after last data event #4998 + * fix(discriminators): remove fields not selected in child when querying by base model #4991 + +4.8.4 / 2017-02-19 +================== + * docs(discriminators): explain embedded discriminators #4997 + * fix(query): fix TypeError when findOneAndUpdate errors #4990 + * fix(update): handle nested single embedded in update validators correctly #4989 + * fix(browser): make browser doc constructor not crash #4987 + +4.8.3 / 2017-02-15 +================== + * chore: upgrade mongodb driver -> 2.2.24 + * docs(connections): addd some details about callbacks #4986 + * fix: ensure class is created with new keyword #4972 #4947 [benhjames](https://github.com/benhjames) + * fix(discriminator): add applyPluginsToDiscriminators option #4965 + * fix(update): properly cast array subdocs when casting update #4960 + * fix(populate): ensure foreign field is selected for virtual populate #4959 + * docs(query): document some query callback params #4949 + * fix(document): ensure errors in validators get caught #2185 + +4.8.2 / 2017-02-10 +================== + * fix(update): actually run validators on addToSet #4953 + * fix(update): improve buffer error handling #4944 [ValYouW](https://github.com/ValYouW) + * fix(discriminator): handle subclassing with loadClass correctly #4942 + * fix(query): allow passing Map to sort() #4941 + * fix(document): handle setting discriminator doc #4935 + * fix(schema): return correct value from pre init hook #4928 + * fix(query): ensure consistent params in error handlers if pre hook errors #4927 + +4.8.1 / 2017-01-30 +================== + * fix(query): handle $exists for arrays and embedded docs #4937 + * fix(query): handle passing string to hint() #4931 + +4.8.0 / 2017-01-28 +================== + * feat(schema): add saveErrorIfNotFound option and $where property #4924 #4004 + * feat(query): add $in implicitly if passed an array #4912 [QuotableWater7](https://github.com/QuotableWater7) + * feat(aggregate): helper for $facet #4904 [varunjayaraman](https://github.com/varunjayaraman) + * feat(query): add collation method #4839 + * feat(schema): propogate strict option to implicit array subschemas #4831 [dkrosso](https://github.com/dkrosso) + * feat(aggregate): add helper for graphLookup #4819 [varunjayaraman](https://github.com/varunjayaraman) + * feat(types): support Decimal128 #4759 + * feat(aggregate): add eachAsync() to aggregate cursor #4300 + * feat(query): add updateOne and updateMany #3997 + * feat(model): support options for insertMany #3893 + * fix(document): run validation on single nested docs if not directly modified #3884 + * feat(model): use discriminator constructor based on discriminatorKey in create() #3624 + * feat: pass collection as context to debug function #3261 + * feat(query): support push and addToSet for update validators #2933 + * perf(document): refactor registerHooksFromSchema so hooks are defined on doc prototype #2754 + * feat(types): add discriminator() function to doc arrays #2723 #1856 + * fix(populate): return an error if sorting underneath a doc array #2202 + +4.7.9 / 2017-01-27 +================== + * fix(query): handle casting $exists under $not #4933 + * chore: upgrade mongodb -> 2.2.22 re: #4931 + +4.7.8 / 2017-01-23 +================== + * fix(populate): better handling for virtual populate under arrays #4923 + * docs: upgrade contributors count #4918 [AdamZaczek](https://github.com/AdamZaczek) + * fix(query): don't set nested path default if setting parent path #4911 + * docs(promise): add missing bracket #4907 + * fix(connection): ensure error handling is consistently async #4905 + * fix: handle authMechanism in query string #4900 + * fix(document): ensure error handlers run for validate #4885 + +4.7.7 / 2017-01-15 +================== + * fix(utils): don't crash if to[key] is null #4881 + * fix: upgrade mongodb -> 2.2.21 #4867 + * fix: add a toBSON to documents for easier querying #4866 + * fix: suppress bluebird warning #4854 [davidwu226](https://github.com/davidwu226) + * fix(populate): handle nested virtuals in virtual populate #4851 + +4.7.6 / 2017-01-02 +================== + * fix(model): allow passing non-array to insertMany #4846 + * fix(populate): use base model name if no discriminator for backwards compat #4843 + * fix: allow internal validate callback to be optional #4842 [arciisine](https://github.com/arciisine) + * fix(document): don't skip pointCut if save not defined (like in browser doc) #4841 + * chore: improve benchmarks #4838 [billouboq](https://github.com/billouboq) + * perf: remove some unused parameters #4837 [billouboq](https://github.com/billouboq) + * fix(query): don't call error handler if passRawResult is true and no error occurred #4836 + +4.7.5 / 2016-12-26 +================== + * docs(model): fix spelling mistake #4828 [paulinoj](https://github.com/paulinoj) + * fix(aggregate): remove unhandled rejection when using aggregate.then() #4824 + * perf: remove try/catch that kills optimizer #4821 + * fix(model): handles populating with discriminators that may not have a ref #4817 + * fix(document): handle setting array of discriminators #3575 + +4.7.4 / 2016-12-21 +================== + * docs: fix typo #4810 [GEEKIAM](https://github.com/GEEKIAM) + * fix(query): timestamps with $push + $each #4805 + * fix(document): handle buffers correctly in minimize #4800 + * fix: don't disallow overwriting default and cast fns #4795 [pdspicer](https://github.com/pdspicer) + * fix(document): don't convert single nested docs to POJOs #4793 + * fix(connection): handle reconnect to replica set correctly #4972 [gfzabarino](https://github.com/gfzabarino) + +4.7.3 / 2016-12-16 +================== + * fix: upgrade mongodb driver -> 2.2.16 for several bug fixes and 3.4 support #4799 + * fix(model): ensure discriminator key is correct for child schema on discriminator #4790 + * fix(document): handle mark valid in subdocs correctly #4778 + * fix(query): check for objects consistently #4775 + +4.7.2 / 2016-12-07 +================== + * test(populate): fix justOne test #4772 [cblanc](https://github.com/cblanc) + * chore: fix benchmarks #4769 [billouboq](https://github.com/billouboq) + * fix(document): handle setting subdoc to null after setting parent doc #4766 + * fix(query): support passRawResult with lean #4762 #4761 [mhfrantz](https://github.com/mhfrantz) + * fix(query): throw StrictModeError if upsert with nonexisting field in condition #4757 + * test: fix a couple of sort tests #4756 [japod](https://github.com/japod) + * chore: upgrade mongodb driver -> 2.2.12 #4753 [mdlavin](https://github.com/mdlavin) + * fix(query): handle update with upsert and overwrite correctly #4749 + +4.7.1 / 2016-11-30 +================== + * fix(schema): throw error if you use prototype as a schema path #4746 + * fix(schema): throw helpful error if you define a virtual with the same path as a real path #4744 + * fix(connection): make createConnection not throw rejected promises #4742 + * fix(populate): allow specifiying options in model schema #4741 + * fix(document): handle selected nested elements with defaults #4739 + * fix(query): add model to cast error if possible #4729 + * fix(query): handle timestamps with overwrite #4054 + +4.7.0 / 2016-11-23 +================== + * docs: clean up schematypes #4732 [kidlj](https://github.com/kidlj) + * perf: only get stack when necessary with VersionError #4726 [Sebmaster](https://github.com/Sebmaster) + * fix(query): ensure correct casting when setting array element #4724 + * fix(connection): ensure db name gets set when you pass 4 params #4721 + * fix: prevent TypeError in node v7 #4719 #4706 + * feat(document): support .set() on virtual subpaths #4716 + * feat(populate): support populate virtuals on nested schemas #4715 + * feat(querycursor): support transform option and .map() #4714 #4705 [cblanc](https://github.com/cblanc) + * fix(document): dont set defaults on not-selected nested paths #4707 + * fix(populate): don't throw if empty string passed to populate #4702 + * feat(model): add `loadClass()` function for importing schema from ES6 class #4668 [rockmacaca](https://github.com/rockmacaca) + +4.6.8 / 2016-11-14 +================== + * fix(querycursor): clear stack when iterating onto next doc #4697 + * fix: handle null keys in validation error #4693 #4689 [arciisine](https://github.com/arciisine) + * fix(populate): handle pre init middleware correctly with populate virtuals #4683 + * fix(connection): ensure consistent return value for open and openSet #4659 + * fix(schema): handle falsy defaults for arrays #4620 + +4.6.7 / 2016-11-10 +================== + * fix(document): only invalidate in subdoc if using update validators #4681 + * fix(document): don't create subdocs when excluded in projection #4669 + * fix(document): ensure single embedded schema validator runs with correct context #4663 + * fix(document): make sure to depopulate top level for sharding #4658 + * fix(connection): throw more helpful error when .model() called incorrectly #4652 + * fix(populate): throw more descriptive error when trying to populate a virtual that doesn't have proper options #4602 + * fix(document): ensure subtype gets set properly when saving with a buffer id #4506 + * fix(query): handle setDefaultsOnInsert with defaults on doc arrays #4456 + * fix(drivers): make debug output better by calling toBSON() #4356 + +4.6.6 / 2016-11-03 +================== + * chore: upgrade deps #4674 [TrejGun](https://github.com/TrejGun) + * chore: run tests on node v7 #4673 [TrejGun](https://github.com/TrejGun) + * perf: make setDefaultsOnInsert more efficient if upsert is off #4672 [CamHenlin](https://github.com/CamHenlin) + * fix(populate): ensure document array is returned #4656 + * fix(query): cast doc arrays with positionals correctly for update #4655 + * fix(document): ensure single nested doc validators run with correct context #4654 + * fix: handle reconnect failed error in new version of driver #4653 [loris](https://github.com/loris) + * fix(populate): if setting a populated doc, take its id #4632 + * fix(populate): handle populated virtuals in init #4618 + +4.6.5 / 2016-10-23 +================== + * docs: fix grammar issues #4642 #4640 #4639 [silvermanj7](https://github.com/silvermanj7) + * fix(populate): filter out nonexistant values for dynref #4637 + * fix(query): handle $type as a schematype operator #4632 + * fix(schema): better handling for uppercase: false and lowercase: false #4622 + * fix(query): don't run transforms on updateForExec() #4621 + * fix(query): handle id = 0 in findById #4610 + * fix(query): handle buffers in mergeClone #4609 + * fix(document): handle undefined with conditional validator for validateSync #4607 + * fix: upgrade to mongodb driver 2.2.11 #4581 + * docs(schematypes): clarify schema.path() #4518 + * fix(query): ensure path is defined before checking in timestamps #4514 + * fix(model): set version key in upsert #4505 + * fix(document): never depopulate top-level doc #3057 + * refactor: ensure sync for setting non-capped collections #2690 + +4.6.4 / 2016-10-16 +================== + * fix(query): cast $not correctly #4616 #4592 [prssn](https://github.com/prssn) + * fix: address issue with caching global plugins #4608 #4601 [TrejGun](https://github.com/TrejGun) + * fix(model): make sure to depopulate in insertMany #4590 + * fix(model): buffer autoIndex if bufferCommands disabled #4589 + * fix(populate): copy ids array before modifying #4585 + * feat(schema): add retainKeyOrder prop #4542 + * fix(document): return isModified true for children of direct modified paths #4528 + * fix(connection): add dropDatabase() helper #4490 + * fix(model): add usePushEach option for schemas #4455 + * docs(connections): add some warnings about buffering #4413 + * fix: add ability to set promise implementation in browser #4395 + +4.6.3 / 2016-10-05 +================== + * fix(document): ensure single nested docs get initialized correctly when setting nested paths #4578 + * fix: turn off transforms when writing nested docs to db #4574 + * fix(document): don't set single nested subdocs to null when removing parent doc #4566 + * fix(model): ensure versionKey gets set in insertMany #4561 + * fix(schema): handle typeKey in arrays #4548 + * feat(schema): set $implicitlyCreated on schema if created by interpretAsType #4443 + +4.6.2 / 2016-09-30 +================== + * chore: upgrade to async 2.0.1 internally #4579 [billouboq](https://github.com/billouboq) + * fix(types): ensure nested single doc schema errors reach update validators #4557 #4519 + * fix(connection): handle rs names with leading numbers (muri 1.1.1) #4556 + * fix(model): don't throw if method name conflicts with Object.prototype prop #4551 + * docs: fix broken link #4544 [VFedyk](https://github.com/VFedyk) + * fix: allow overwriting model on mongoose singleton #4541 [Nainterceptor](https://github.com/Nainterceptor) + * fix(document): don't use init: true when building doc defaults #4540 + * fix(connection): use replSet option if replset not specified #4535 + * fix(query): cast $not objects #4495 + +4.6.1 / 2016-09-20 +================== + * fix(query): improve handling of $not with $elemMatch #4531 #3719 [timbowhite](https://github.com/timbowhite) + * fix: upgrade mongodb -> 2.2.10 #4517 + * chore: fix webpack build issue #4512 [saiichihashimoto](https://github.com/saiichihashimoto) + * fix(query): emit error on next tick when exec callback errors #4500 + * test: improve test case #4496 [isayme](https://github.com/isayme) + * fix(schema): use same check for array types and top-level types #4493 + * style: fix indentation in docs #4489 [dhurlburtusa](https://github.com/dhurlburtusa) + * fix(schema): expose original object passed to constructor #4486 + * fix(query): handle findOneAndUpdate with array of arrays #4484 #4470 [fedotov](https://github.com/fedotov) + * feat(document): add $ignore to make a path ignored #4480 + * fix(query): properly handle setting single embedded in update #4475 #4466 #4465 + * fix(updateValidators): handle single nested schema subpaths correctly #4479 + * fix(model): throw handy error when method name conflicts with property name #4475 + * fix(schema): handle .set() with array field #4472 + * fix(query): check nested path when avoiding double-validating Mixed #4441 + * fix(schema): handle calling path.trim() with no args correctly #4042 + +4.6.0 / 2016-09-02 +================== + * docs(document): clarify the findById and findByIdAndUpdate examples #4471 [mdcanham](https://github.com/mdcanham) + * docs(schematypes): add details re: options #4452 + * docs(middleware): add docs for insertMany hooks #4451 + * fix(schema): create new array when copying from existing object to preserve change tracking #4449 + * docs: fix typo in index.jade #4448 + * fix(query): allow array for populate options #4446 + * fix(model): create should not cause unhandle reject promise #4439 + * fix: upgrade to mongodb driver 2.2.9 #4363 #4341 #4311 (see [comments here](https://github.com/mongodb/js-bson/commit/aa0b54597a0af28cce3530d2144af708e4b66bf0#commitcomment-18850498) if you use node 0.10) + +4.5.10 / 2016-08-23 +=================== + * docs: fix typo on documents.jade #4444 [Gabri3l](https://github.com/Gabri3l) + * chore: upgrade mocha to 3.0.2 #4437 [TrejGun](https://github.com/TrejGun) + * fix: subdocuments causing error with parent timestamp on update #4434 [dyang108](https://github.com/dyang108) + * fix(query): don't crash if timestamps on and update doesn't have a path #4425 #4424 #4418 + * fix(query): ensure single nested subdoc is hydrated when running update validators #4420 + * fix(query): cast non-$geometry operators for $geoWithin #4419 + * docs: update contributor count #4415 [AdamZaczek](https://github.com/AdamZaczek) + * docs: add more clarification re: the index event #4410 + * fix(document): only skip modifying subdoc path if parent is direct modified #4405 + * fix(schema): throw cast error if provided date invalid #4404 + * feat(error): use util.inspect() so CastError never prints "[object Object]" #4398 + * fix(model): dont error if the discriminator key is unchanged #4387 + * fix(query): don't throw unhandled rejection with bluebird when using cbs #4379 + +4.5.9 / 2016-08-14 +================== + * docs: add mixed schema doc for Object literal #4400 [Kikobeats](https://github.com/Kikobeats) + * fix(query): cast $geoWithin and convert mongoose objects to POJOs before casting #4392 + * fix(schematype): dont cast defaults without parent doc #4390 + * fix(query): disallow passing empty string to findOne() #4378 + * fix(document): set single nested doc isNew correctly #4369 + * fix(types): checks field name correctly with nested arrays and populate #4365 + * fix(drivers): make debug output copy-pastable into mongodb shell #4352 + * fix(services): run update validators on nested paths #4332 + * fix(model): handle typeKey with discriminators #4339 + * fix(query): apply timestamps to child schemas when explicitly specified in update #4049 + * fix(schema): set prefix as nested path with add() #1730 + +4.5.8 / 2016-08-01 +================== + * fix(model): make changing the discriminator key cause a cast error #4374 + * fix(query): pass projection fields to cursor #4371 #4342 [Corei13](https://github.com/Corei13) + * fix(document): support multiple paths for isModified #4370 [adambuczynski](https://github.com/adambuczynski) + * fix(querycursor): always cast fields before returning cursor #4355 + * fix(query): support projection as alias for fields in findOneAndUpdate #4315 + * fix(schema): treat index false + unique false as no index #4304 + * fix(types): dont mark single nested subpath as modified if whole doc already modified #4224 + +4.5.7 / 2016-07-25 +================== + * fix(document): ensure no unhandled rejections if callback specified for save #4364 + +4.5.6 / 2016-07-23 +================== + * fix(schema): don't overwrite createdAt if it isn't selected #4351 [tusbar](https://github.com/tusbar) + * docs(api): fix link to populate() and add a new one from depopulate() #4345 [Delapouite](https://github.com/Delapouite) + * fix(types): ownerDocument() works properly with single nested docs #4344 [vichle](https://github.com/vichle) + * fix(populate): dont use findOne when justOne option set #4329 + * fix(document): dont trigger .then() deprecated warning when calling doc.remove() #4291 + * docs(connection): add promiseLibrary option #4280 + * fix(plugins): apply global plugins to subschemas #4271 + * fix(model): ensure `ensureIndex()` never calls back in the same tick #4246 + * docs(schema): improve post hook docs on schema #4238 + +4.5.5 / 2016-07-18 +================== + * fix(document): handle setting root to empty obj if minimize false #4337 + * fix: downgrade to mongodb 2.1.18 #4335 #4334 #4328 #4323 + * perf(types): remove defineProperty usage in documentarray #4333 + * fix(query): correctly pass model in .toConstructor() #4318 + * fix(services): avoid double-validating mixed types with update validators #4305 + * docs(middleware): add docs describing error handling middleware #4229 + * fix(types): throw correct error when invalidating doc array #3602 + +4.5.4 / 2016-07-11 +================== + * fix(types): fix removing embedded documents #4309 [RoCat](https://github.com/RoCat) + * docs: various docs improvements #4302 #4294 [simonxca](https://github.com/simonxca) + * fix: upgrade mongodb -> 2.1.21 #4295 #4202 [RoCat](https://github.com/RoCat) + * fix(populate): convert single result to array for virtual populate because of lean #4288 + * fix(populate): handle empty results for populate virtuals properly #4285 #4284 + * fix(query): dont cast $inc to number if type is long #4283 + * fix(types): allow setting single nested doc to null #4281 + * fix(populate): handle deeply nested virtual populate #4278 + * fix(document): allow setting empty obj if strict mode is false #4274 + * fix(aggregate): allow passing obj to .unwind() #4239 + * docs(document): add return statements to transform examples #1963 + +4.5.3 / 2016-06-30 +================== + * fix(query): pass correct options to QueryCursor #4277 #4266 + * fix(querycursor): handle lean option correctly #4276 [gchudnov](https://github.com/gchudnov) + * fix(document): fix error handling when no error occurred #4275 + * fix(error): use strict mode for version error #4272 + * docs(populate): fix crashing compilation for populate.jade #4267 + * fix(populate): support `justOne` option for populate virtuals #4263 + * fix(populate): ensure model param gets used for populate virtuals #4261 #4243 + * fix(querycursor): add ability to properly close the cursor #4258 + * docs(model): correct link to Document #4250 + * docs(populate): correct path for refPath populate #4240 + * fix(document): support validator.isEmail as validator #4064 + +4.5.2 / 2016-06-24 +================== + * fix(connection): add checks for collection presence for `onOpen` and `onClose` #4259 [nodkz](https://github.com/nodkz) + * fix(cast): allow strings for $type operator #4256 + * fix(querycursor): support lean() #4255 [pyramation](https://github.com/pyramation) + * fix(aggregate): allow setting noCursorTimeout option #4241 + * fix(document): handle undefined for Array.pull #4222 [Sebmaster](https://github.com/Sebmaster) + * fix(connection): ensure promise.catch() catches initial connection error #4135 + * fix(document): show additional context for VersionError #2633 + +4.5.1 / 2016-06-18 +================== + * fix(model): ensure wrapped insertMany() returns a promise #4237 + * fix(populate): dont overwrite populateVirtuals when populating multiple paths #4234 + * docs(model): clarify relationship between create() and save() #4233 + * fix(types): handle option param in subdoc remove() #4231 [tdebarochez](https://github.com/tdebarochez) + * fix(document): dedupe modified paths #4226 #4223 [adambuczynski](https://github.com/adambuczynski) + * fix(model): don't modify user-provided options object #4221 + * fix(document): handle setting nested path to empty object #4218 #4182 + * fix(document): clean subpaths when removing single nested #4216 + * fix(document): don't force transform on subdocs with inspect #4213 + * fix(error): allow setting .messages object #4207 + +4.5.0 / 2016-06-13 +================== + * feat(query): added Query.prototype.catch() #4215 #4173 [adambuczynski](https://github.com/adambuczynski) + * feat(query): add Query.prototype.cursor() as a .stream() alternative #4117 #3637 #1907 + * feat(document): add markUnmodified() function #4092 [vincentcr](https://github.com/vincentcr) + * feat(aggregate): convert aggregate object to a thenable #3995 #3946 [megagon](https://github.com/megagon) + * perf(types): remove defineProperties call for array (**Note:** Because of this, a mongoose array will no longer `assert.deepEqual()` a plain old JS array) #3886 + * feat(model): add hooks for insertMany() #3846 + * feat(schema): add support for custom query methods #3740 #2372 + * feat(drivers): emit error on 'serverClosed' because that means that reconnect failed #3615 + * feat(model): emit error event when callback throws exception #3499 + * feat(model): inherit options from discriminator base schema #3414 #1818 + * feat(populate): expose mongoose-populate-virtuals inspired populate API #2562 + * feat(document): trigger remove hooks on subdocs when removing parent #2348 + * feat(schema): add support for express-style error handling middleware #2284 + * fix(model): disallow setting discriminator key #2041 + * feat(schema): add support for nested arrays #1361 + +4.4.20 / 2016-06-05 +=================== + * docs: clarify command buffering when using driver directly #4195 + * fix(promise): correct broken mpromise .catch() #4189 + * fix(document): clean modified subpaths when set path to empty obj #4182 + * fix(query): support minDistance with query casting and `.near()` #4179 + * fix(model): remove unnecessary .save() promise #4177 + * fix(schema): cast all valid ObjectId strings to object ids #3365 + * docs: remove unclear "unsafe" term in query docs #3282 + +4.4.19 / 2016-05-21 +=================== + * fix(model): handle insertMany if timestamps not set #4171 + +4.4.18 / 2016-05-21 +=================== + * docs: add missing period #4170 [gitname](https://github.com/gitname) + * docs: change build badge to svg #4158 [a0viedo](https://github.com/a0viedo) + * fix(model): update timestamps when setting `createdAt` #4155 + * fix(utils): make sure to require in document properly #4152 + * fix(model): throw overwrite error when discriminator name conflicts #4148 + +4.4.17 / 2016-05-13 +=================== + * docs: remove repetition in QueryStream docs #4147 [hugoabonizio](https://github.com/hugoabonizio) + * fix(document): dont double-validate doc array elements #4145 + * fix(document): call required function with correct scope #4142 [JedWatson](https://github.com/JedWatson) + +4.4.16 / 2016-05-09 +=================== + * refactor(document): use function reference #4133 [dciccale](https://github.com/dciccale) + * docs(querystream): clarify `destroy()` and close event #4126 [AnthonyCC](https://github.com/AnthonyCC) + * test: make before hook fail fast if it can't connect #4121 + * docs: add description of CastError constructor params #4120 + * fix(schematype): ensure single embedded defaults have $parent #4115 + * fix(document): mark nested paths for validation #4111 + * fix(schema): make sure element is always a subdoc in doc array validation #3816 + +4.4.15 / 2016-05-06 +=================== + * fix(schema): support overwriting array default #4109 + * fix(populate): assign values when resolving each populate #4104 + * fix(aggregate): dont send async option to server #4101 + * fix(model): ensure isNew set to false after insertMany #4099 + * fix(connection): emit on error if listeners and no callback #4098 + * fix(document): treat required fn that returns false as required: false #4094 + +4.4.14 / 2016-04-27 +=================== + * fix: upgrade mongodb -> 2.1.18 #4102 + * feat(connection): allow setting mongos as a uri query param #4093 #4035 [burtonjc](https://github.com/burtonjc) + * fix(populate): make sure to use correct assignment order for each model #4073 + * fix(schema): add complete set of geospatial operators for single embedded subdocs #4014 + +3.8.40 / 2016-04-24 +=================== + * upgraded; mquery -> 1.10.0 #3989 + +4.4.13 / 2016-04-21 +=================== + * docs: add docs favicons #4082 [robertjustjones](https://github.com/robertjustjones) + * docs(model): correct Model.remove() return value #4075 [Jokero](https://github.com/Jokero) + * fix(query): add $geoWithin query casting for single embedded docs #4044 + * fix(schema): handle setting trim option to falsy #4042 + * fix(query): handle setDefaultsOnInsert with empty update #3835 + +4.4.12 / 2016-04-08 +=================== + * docs(query): document context option for update and findOneAndUpdate #4055 + * docs(query): correct link to $geoWithin docs #4050 + * fix(project): upgrade to mongodb driver 2.1.16 #4048 [schmalliso](https://github.com/schmalliso) + * docs(validation): fix validation docs #4028 + * fix(types): improve .id() check for document arrays #4011 + * fix(query): remove premature return when using $rename #3171 + * docs(connection): clarify relationship between models and connections #2157 + +4.4.11 / 2016-04-03 +=================== + * fix: upgrade to mongodb driver 2.1.14 #4036 #4030 #3945 + * fix(connection): allow connecting with { mongos: true } to handle query params #4032 [burtonjc](https://github.com/burtonjc) + * docs(connection): add autoIndex example #4026 [tilleps](https://github.com/tilleps) + * fix(query): handle passRawResult option when zero results #4023 + * fix(populate): clone options before modifying #4022 + * docs: add missing whitespace #4019 [chenxsan](https://github.com/chenxsan) + * chore: upgrade to ESLint 2.4.0 #4015 [ChristianMurphy](https://github.com/ChristianMurphy) + * fix(types): single nested subdocs get ids by default #4008 + * chore(project): add dependency status badge #4007 [Maheshkumar-Kakade](http://github.com/Maheshkumar-Kakade) + * fix: make sure timestamps don't trigger unnecessary updates #4005 #3991 [tommarien](https://github.com/tommarien) + * fix(document): inspect inherits schema options #4001 + * fix(populate): don't mark populated path as modified if setting to object w/ same id #3992 + * fix(document): support kind argument to invalidate #3965 + +4.4.10 / 2016-03-24 +=================== + * fix(document): copy isNew when copying a document #3982 + * fix(document): don't override defaults with undefined keys #3981 + * fix(populate): merge multiple deep populate options for the same path #3974 + +4.4.9 / 2016-03-22 +================== + * fix: upgrade mongodb -> 2.1.10 re https://jira.mongodb.org/browse/NODE-679 #4010 + * docs: add syntax highlighting for acquit examples #3975 + +4.4.8 / 2016-03-18 +================== + * docs(aggregate): clarify promises #3990 [megagon](https://github.com/megagon) + * fix: upgrade mquery -> 1.10 #3988 [matskiv](https://github.com/matskiv) + * feat(connection): 'all' event for repl sets #3986 [xizhibei](https://github.com/xizhibei) + * docs(types): clarify Array.pull #3985 [seriousManual](https://github.com/seriousManual) + * feat(query): support array syntax for .sort() via mquery 1.9 #3980 + * fix(populate): support > 3 level nested populate #3973 + * fix: MongooseThenable exposes connection correctly #3972 + * docs(connection): add note about reconnectTries and reconnectInterval #3969 + * feat(document): invalidate returns the new validationError #3964 + * fix(query): .eq() as shorthand for .equals #3953 [Fonger](https://github.com/Fonger) + * docs(connection): clarify connection string vs passed options #3941 + * docs(query): select option for findOneAndUpdate #3933 + * fix(error): ValidationError.properties no longer enumerable #3925 + * docs(validation): clarify how required validators work with nested schemas #3915 + * fix: upgrade mongodb driver -> 2.1.8 to make partial index errors more sane #3864 + +4.4.7 / 2016-03-11 +================== + * fix(query): stop infinite recursion caused by merging a mongoose buffer #3961 + * fix(populate): handle deep populate array -> array #3954 + * fix(schema): allow setting timestamps with .set() #3952 #3951 #3907 [Fonger](https://github.com/Fonger) + * fix: MongooseThenable doesn't overwrite constructors #3940 + * fix(schema): don't cast boolean to date #3935 + * fix(drivers): support sslValidate in connection string #3929 + * fix(types): correct markModified() for single nested subdocs #3910 + * fix(drivers): catch and report any errors that occur in driver methods #3906 + * fix(populate): get subpopulate model correctly when array under nested #3904 + * fix(document): allow fields named 'pre' and 'post' #3902 + * docs(query): clarify runValidators and setDefaultsOnInsert options #3892 + * docs(validation): show how to use custom required messages in schema #2616 + +4.4.6 / 2016-03-02 +================== + * fix: upgrade mongodb driver to 2.1.7 #3938 + * docs: fix plugins link #3917 #3909 [fbertone](https://github.com/fbertone) + * fix(query): sort+select with count works #3914 + * fix(query): improve mergeUpdate's ability to handle nested docs #3890 + +4.4.5 / 2016-02-24 +================== + * fix(query): ability to select a length field (upgrade to mquery 1.7.0) #3903 + * fix: include nested CastError as reason for array CastError #3897 [kotarou3](https://github.com/kotarou3) + * fix(schema): check for doc existence before taking fields #3889 + * feat(schema): useNestedStrict option to take nested strict mode for update #3883 + * docs(validation): clarify relationship between required and checkRequired #3822 + * docs(populate): dynamic reference docs #3809 + * docs: expand dropdown when clicking on file name #3807 + * docs: plugins.mongoosejs.io is up #3127 + * fix(schema): ability to add a virtual with same name as removed path #2398 + +4.4.4 / 2016-02-17 +================== + * fix(schema): handle field selection when casting single nested subdocs #3880 + * fix(populate): populating using base model with multiple child models in result #3878 + * fix: ability to properly use return value of `mongoose.connect()` #3874 + * fix(populate): dont hydrate populated subdoc if lean option set #3873 + * fix(connection): dont re-auth if already connected with useDb #3871 + * docs: cover how to set underlying driver's promise lib #3869 + * fix(document): handle conflicting names in validation errors with subdocs #3867 + * fix(populate): set undefined instead of null consistently when populate couldn't find results #3859 + * docs: link to `execPopulate()` in `doc.populate()` docs #3836 + * docs(plugin): link to the `mongoose.plugin()` function #3732 + +4.4.3 / 2016-02-09 +================== + * fix: upgrade to mongodb 2.1.6 to remove kerberos log output #3861 #3860 [cartuchogl](https://github.com/cartuchogl) + * fix: require('mongoose') is no longer a pseudo-promise #3856 + * fix(query): update casting for single nested docs #3820 + * fix(populate): deep populating multiple paths with same options #3808 + * docs(middleware): clarify save/validate hook order #1149 + +4.4.2 / 2016-02-05 +================== + * fix(aggregate): handle calling .cursor() with no options #3855 + * fix: upgrade mongodb driver to 2.1.5 for GridFS memory leak fix #3854 + * docs: fix schematype.html conflict #3853 #3850 #3843 + * fix(model): bluebird unhandled rejection with ensureIndexes() on init #3837 + * docs: autoIndex option for createConnection #3805 + +4.4.1 / 2016-02-03 +================== + * fix: linting broke some cases where we use `== null` as shorthand #3852 + * docs: fix up schematype.html conflict #3848 #3843 [mynameiscoffey](https://github.com/mynameiscoffey) + * fix: backwards breaking change with `.connect()` return value #3847 + * docs: downgrade dox and highlight.js to fix docs build #3845 + * docs: clean up typo #3842 [Flash-](https://github.com/Flash-) + * fix(document): storeShard handles undefined values #3841 + * chore: more linting #3838 [TrejGun](https://github.com/TrejGun) + * fix(schema): handle `text: true` as a way to declare a text index #3824 + +4.4.0 / 2016-02-02 +================== + * docs: fix expireAfterSeconds index option name #3831 [Flash-](https://github.com/Flash-) + * chore: run lint after test #3829 [ChristianMurphy](https://github.com/ChristianMurphy) + * chore: use power-assert instead of assert #3828 [TrejGun](https://github.com/TrejGun) + * chore: stricter lint #3827 [TrejGun](https://github.com/TrejGun) + * feat(types): casting moment to date #3813 [TrejGun](https://github.com/TrejGun) + * chore: comma-last lint for test folder #3810 [ChristianMurphy](https://github.com/ChristianMurphy) + * fix: upgrade async mpath, mpromise, muri, and sliced #3801 [TrejGun](https://github.com/TrejGun) + * fix(query): geo queries now return proper ES2015 promises #3800 [TrejGun](https://github.com/TrejGun) + * perf(types): use `Object.defineProperties()` for array #3799 [TrejGun](https://github.com/TrejGun) + * fix(model): mapReduce, ensureIndexes, remove, and save properly return ES2015 promises #3795 #3628 #3595 [TrejGun](https://github.com/TrejGun) + * docs: fixed dates in History.md #3791 [Jokero](https://github.com/Jokero) + * feat: connect, open, openSet, and disconnect return ES2015 promises #3790 #3622 [TrejGun](https://github.com/TrejGun) + * feat: custom type for int32 via mongoose-int32 npm package #3652 #3102 + * feat: basic custom schema type API #995 + * feat(model): `insertMany()` for more performant bulk inserts #723 + +4.3.7 / 2016-01-23 +================== + * docs: grammar fix in timestamps docs #3786 [zclancy](https://github.com/zclancy) + * fix(document): setting nested populated docs #3783 [slamuu](https://github.com/slamuu) + * fix(document): don't call post save hooks twice for pushed docs #3780 + * fix(model): handle `_id=0` correctly #3776 + * docs(middleware): async post hooks #3770 + * docs: remove confusing sentence #3765 [marcusmellis89](https://github.com/marcusmellis89) + +3.8.39 / 2016-01-15 +=================== + * fixed; casting a number to a buffer #3764 + * fixed; enumerating virtual property with nested objects #3743 [kusold](https://github.com/kusold) + +4.3.6 / 2016-01-15 +================== + * fix(types): casting a number to a buffer #3764 + * fix: add "listener" to reserved keywords #3759 + * chore: upgrade uglify #3757 [ChristianMurphy](https://github.com/ChristianMurphy) + * fix: broken execPopulate() in 4.3.5 #3755 #3753 + * fix: ability to remove() a single embedded doc #3754 + * style: comma-last in test folder #3751 [ChristianMurphy](https://github.com/ChristianMurphy) + * docs: clarify versionKey option #3747 + * fix: improve colorization for arrays #3744 [TrejGun](https://github.com/TrejGun) + * fix: webpack build #3713 + +4.3.5 / 2016-01-09 +================== + * fix(query): throw when 4th parameter to update not a function #3741 [kasselTrankos](https://github.com/kasselTrankos) + * fix(document): separate error type for setting an object to a primitive #3735 + * fix(populate): Model.populate returns ES6 promise #3734 + * fix(drivers): re-register event handlers after manual reconnect #3729 + * docs: broken links #3727 + * fix(validation): update validators run array validation #3724 + * docs: clarify the need to use markModified with in-place date ops #3722 + * fix(document): mark correct path as populated when manually populating array #3721 + * fix(aggregate): support for array pipeline argument to append #3718 [dbkup](https://github.com/dbkup) + * docs: clarify `.connect()` callback #3705 + * fix(schema): properly validate nested single nested docs #3702 + * fix(types): handle setting documentarray of wrong type #3701 + * docs: broken links #3700 + * fix(drivers): debug output properly displays '0' #3689 + +3.8.38 / 2016-01-07 +=================== + * fixed; aggregate.append an array #3730 [dbkup](https://github.com/dbkup) + +4.3.4 / 2015-12-23 +================== + * fix: upgrade mongodb driver to 2.1.2 for repl set error #3712 [sansmischevia](https://github.com/sansmischevia) + * docs: validation docs typo #3709 [ivanmaeder](https://github.com/ivanmaeder) + * style: remove unused variables #3708 [ChristianMurphy](https://github.com/ChristianMurphy) + * fix(schema): duck-typing for schemas #3703 [mgcrea](https://github.com/mgcrea) + * docs: connection sample code issue #3697 + * fix(schema): duck-typing for schemas #3693 [mgcrea](https://github.com/mgcrea) + * docs: clarify id schema option #3638 + +4.3.3 / 2015-12-18 +================== + * fix(connection): properly support 'replSet' as well as 'replset' #3688 [taxilian](https://github.com/taxilian) + * fix(document): single nested doc pre hooks called before nested doc array #3687 [aliatsis](https://github.com/aliatsis) + +4.3.2 / 2015-12-17 +================== + * fix(document): .set() into single nested schemas #3686 + * fix(connection): support 'replSet' as well as 'replset' option #3685 + * fix(document): bluebird unhandled rejection when validating doc arrays #3681 + * fix(document): hooks for doc arrays in single nested schemas #3680 + * fix(document): post hooks for single nested schemas #3679 + * fix: remove unused npm module #3674 [sybarite](https://github.com/sybarite) + * fix(model): don't swallow exceptions in nested doc save callback #3671 + * docs: update keepAlive info #3667 [ChrisZieba](https://github.com/ChrisZieba) + * fix(document): strict 'throw' throws a specific mongoose error #3662 + * fix: flakey test #3332 + * fix(query): more robust check for RegExp #2969 + +4.3.1 / 2015-12-11 +================== + * feat(aggregate): `.sample()` helper #3665 + * fix(query): bitwise query operators with buffers #3663 + * docs(migration): clarify `new` option and findByIdAndUpdate #3661 + +4.3.0 / 2015-12-09 +================== + * feat(query): support for mongodb 3.2 bitwise query operators #3660 + * style: use comma-last style consistently #3657 [ChristianMurphy](https://github.com/ChristianMurphy) + * feat: upgrade mongodb driver to 2.1.0 for full MongoDB 3.2 support #3656 + * feat(aggregate): `.lookup()` helper #3532 + +4.2.10 / 2015-12-08 +=================== + * fixed; upgraded marked #3653 [ChristianMurphy](https://github.com/ChristianMurphy) + * docs; cross-db populate #3648 + * docs; update mocha URL #3646 [ojhaujjwal](https://github.com/ojhaujjwal) + * fixed; call close callback asynchronously #3645 + * docs; virtuals.html issue #3644 [Psarna94](https://github.com/Psarna94) + * fixed; single embedded doc casting on init #3642 + * docs; validation docs improvements #3640 + +4.2.9 / 2015-12-02 +================== + * docs; defaults docs #3625 + * fix; nested numeric keys causing an embedded document crash #3623 + * fix; apply path getters before virtual getters #3618 + * fix; casting for arrays in single nested schemas #3616 + +4.2.8 / 2015-11-25 +================== + * docs; clean up README links #3612 [ReadmeCritic](https://github.com/ReadmeCritic) + * fix; ESLint improvements #3605 [ChristianMurphy](https://github.com/ChristianMurphy) + * fix; assigning single nested subdocs #3601 + * docs; describe custom logging functions in `mongoose.set()` docs #3557 + +4.2.7 / 2015-11-20 +================== + * fixed; readPreference connection string option #3600 + * fixed; pulling from manually populated arrays #3598 #3579 + * docs; FAQ about OverwriteModelError #3597 [stcruy](https://github.com/stcruy) + * fixed; setting single embedded schemas to null #3596 + * fixed; indexes for single embedded schemas #3594 + * docs; clarify projection for `findOne()` #3593 [gunar](https://github.com/gunar) + * fixed; .ownerDocument() method on single embedded schemas #3589 + * fixed; properly throw casterror for query on single embedded schema #3580 + * upgraded; mongodb driver -> 2.0.49 for reconnect issue fix #3481 + +4.2.6 / 2015-11-16 +================== + * fixed; ability to manually populate an array #3575 + * docs; clarify `isAsync` parameter to hooks #3573 + * fixed; use captureStackTrace if possible instead #3571 + * fixed; crash with buffer and update validators #3565 [johnpeb](https://github.com/johnpeb) + * fixed; update casting with operators overwrite: true #3564 + * fixed; validation with single embedded docs #3562 + * fixed; inline docs inherit parents $type key #3560 + * docs; bad grammar in populate docs #3559 [amaurymedeiros](https://github.com/amaurymedeiros) + * fixed; properly handle populate option for find() #2321 + +3.8.37 / 2015-11-16 +=================== + * fixed; use retainKeyOrder for cloning update op #3572 + +4.2.5 / 2015-11-09 +================== + * fixed; handle setting fields in pre update hooks with exec #3549 + * upgraded; ESLint #3547 [ChristianMurphy](https://github.com/ChristianMurphy) + * fixed; bluebird unhandled rejections with cast errors and .exec #3543 + * fixed; min/max validators handling undefined #3539 + * fixed; standalone mongos connections #3537 + * fixed; call `.toObject()` when setting a single nested doc #3535 + * fixed; single nested docs now have methods #3534 + * fixed; single nested docs with .create() #3533 #3521 [tusbar](https://github.com/tusbar) + * docs; deep populate docs #3528 + * fixed; deep populate schema ref handling #3507 + * upgraded; mongodb driver -> 2.0.48 for sort overflow issue #3493 + * docs; clarify default ids for discriminators #3482 + * fixed; properly support .update(doc) #3221 + 4.2.4 / 2015-11-02 ================== - * fixed; upgraded `ms` package for security vulnerability #3254 [fhemberger](https://github.com/fhemberger) + * fixed; upgraded `ms` package for security vulnerability #3524 [fhemberger](https://github.com/fhemberger) * fixed; ESlint rules #3517 [ChristianMurphy](https://github.com/ChristianMurphy) * docs; typo in aggregation docs #3513 [rafakato](https://github.com/rafakato) * fixed; add `dontThrowCastError` option to .update() for promises #3512 diff --git a/Makefile b/Makefile index 77731d3fca7..5a3043d7d77 100644 --- a/Makefile +++ b/Makefile @@ -3,17 +3,11 @@ DOCS_ = $(shell find lib/ -name '*.js') DOCS = $(DOCS_:.js=.json) DOCFILE = docs/source/_docs STABLE_BRANCH = master -LEGACY_BRANCH = 3.8.x +LEGACY_BRANCH = 4.x test: @MONGOOSE_DISABLE_STABILITY_WARNING=1 ./node_modules/.bin/mocha $(T) --async-only test/*.test.js -test-short: - @MONGOOSE_DISABLE_STABILITY_WARNING=1 ./node_modules/.bin/mocha $(T) -g LONG -i --async-only test/**/*.test.js - -test-long: - @MONGOOSE_DISABLE_STABILITY_WARNING=1 ./node_modules/.bin/mocha $(T) -g LONG --async-only test/**/*.test.js - docs: ghpages merge_stable docclean gendocs docs_legacy: legacy docclean_legacy gendocs copytmp gitreset ghpages copylegacy @@ -71,9 +65,3 @@ browser: npm install `node format_deps.js` ./node_modules/browserify/bin/cmd.js -o ./bin/mongoose.js lib/browser.js ./node_modules/uglify-js/bin/uglifyjs ./bin/mongoose.js -o ./bin/mongoose.min.js --screw-ie8 -c -m - -browser_debug: - ./node_modules/browserify/bin/cmd.js -o ./bin/mongoose.debug.js lib/browser.js -d - -test_browser: - ./node_modules/karma/bin/karma start karma.local.conf.js diff --git a/README.md b/README.md index 0e9a7512a98..c882a8cab8b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Mongoose -Mongoose is a [MongoDB](http://www.mongodb.org/) object modeling tool designed to work in an asynchronous environment. +Mongoose is a [MongoDB](https://www.mongodb.org/) object modeling tool designed to work in an asynchronous environment. -[![Build Status](https://api.travis-ci.org/Automattic/mongoose.png?branch=master)](https://travis-ci.org/Automattic/mongoose) -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Automattic/mongoose?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Slack Status](http://slack.mongoosejs.io/badge.svg)](http://slack.mongoosejs.io) +[![Build Status](https://api.travis-ci.org/Automattic/mongoose.svg?branch=master)](https://travis-ci.org/Automattic/mongoose) [![NPM version](https://badge.fury.io/js/mongoose.svg)](http://badge.fury.io/js/mongoose) ## Documentation @@ -13,27 +13,32 @@ Mongoose is a [MongoDB](http://www.mongodb.org/) object modeling tool designed t ## Support - [Stack Overflow](http://stackoverflow.com/questions/tagged/mongoose) - - [bug reports](https://github.com/Automattic/mongoose/issues/) - - [help forum](http://groups.google.com/group/mongoose-orm) - - [MongoDB support](http://www.mongodb.org/display/DOCS/Technical+Support) - - (irc) #mongoosejs on freenode + - [Bug Reports](https://github.com/Automattic/mongoose/issues/) + - [Mongoose Slack Channel](http://slack.mongoosejs.io/) + - [Help Forum](http://groups.google.com/group/mongoose-orm) + - [MongoDB Support](https://docs.mongodb.org/manual/support/) -## Plugins +## Importing -Check out the [plugins search site](http://plugins.mongoosejs.com/) to see hundreds of related modules from the community. +```javascript +// Using Node.js `require()` +const mongoose = require('mongoose'); -Build your own Mongoose plugin through [generator-mongoose-plugin](https://github.com/huei90/generator-mongoose-plugin). +// Using ES6 imports +import mongoose from 'mongoose'; +``` -## Contributors +## Plugins -View all 100+ [contributors](https://github.com/Automattic/mongoose/graphs/contributors). Stand up and be counted as a [contributor](https://github.com/Automattic/mongoose/blob/master/CONTRIBUTING.md) too! +Check out the [plugins search site](http://plugins.mongoosejs.io/) to see hundreds of related modules from the community. Next, learn how to write your own plugin from the [docs](http://mongoosejs.com/docs/plugins.html) or [this blog post](http://thecodebarbarian.com/2015/03/06/guide-to-mongoose-plugins). -## Live Examples - +## Contributors + +View all 300+ [contributors](https://github.com/Automattic/mongoose/graphs/contributors). Stand up and be counted as a [contributor](https://github.com/Automattic/mongoose/blob/master/CONTRIBUTING.md) too! ## Installation -First install [node.js](http://nodejs.org/) and [mongodb](http://www.mongodb.org/downloads). Then: +First install [node.js](http://nodejs.org/) and [mongodb](https://www.mongodb.org/downloads). Then: ```sh $ npm install mongoose @@ -41,7 +46,7 @@ $ npm install mongoose ## Stability -The current stable branch is [master](https://github.com/Automattic/mongoose/tree/master). The [3.8.x](https://github.com/Automattic/mongoose/tree/3.8.x) branch contains legacy support for the 3.x release series, which will continue to be actively maintained until September 1, 2015. +The current stable branch is [master](https://github.com/Automattic/mongoose/tree/master). The [3.8.x](https://github.com/Automattic/mongoose/tree/3.8.x) branch contains legacy support for the 3.x release series, which is no longer under active development as of September 2015. The [3.8.x docs](http://mongoosejs.com/docs/3.8.x/) are still available. ## Overview @@ -57,7 +62,7 @@ var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/my_database'); ``` -Once connected, the `open` event is fired on the `Connection` instance. If you're using `mongoose.connect`, the `Connection` is `mongoose.connection`. Otherwise, `mongoose.createConnection` return value is a `Connection`. +Once connected, the `open` event is fired on the `Connection` instance. If you're using `mongoose.connect`, the `Connection` is `mongoose.connection`. Otherwise, `mongoose.createConnection` return value is a `Connection`. **Note:** _If the local connection fails then try using 127.0.0.1 instead of localhost. Sometimes issues may arise when the local hostname has been changed._ @@ -68,14 +73,14 @@ Once connected, the `open` event is fired on the `Connection` instance. If you'r Models are defined through the `Schema` interface. ```js -var Schema = mongoose.Schema - , ObjectId = Schema.ObjectId; +var Schema = mongoose.Schema, + ObjectId = Schema.ObjectId; var BlogPost = new Schema({ - author : ObjectId - , title : String - , body : String - , date : Date + author : ObjectId, + title : String, + body : String, + date : Date }); ``` @@ -96,11 +101,11 @@ The following example shows some of these features: ```js var Comment = new Schema({ - name : { type: String, default: 'hahaha' } - , age : { type: Number, min: 18, index: true } - , bio : { type: String, match: /[a-z]/ } - , date : { type: Date, default: Date.now } - , buff : Buffer + name: { type: String, default: 'hahaha' }, + age: { type: Number, min: 18, index: true }, + bio: { type: String, match: /[a-z]/ }, + date: { type: Date, default: Date.now }, + buff: Buffer }); // a setter @@ -162,18 +167,18 @@ You can also `findOne`, `findById`, `update`, etc. For more details check out [t **Important!** If you opened a separate connection using `mongoose.createConnection()` but attempt to access the model through `mongoose.model('ModelName')` it will not work as expected since it is not hooked up to an active db connection. In this case access your model through the connection you created: ```js -var conn = mongoose.createConnection('your connection string') - , MyModel = conn.model('ModelName', schema) - , m = new MyModel; +var conn = mongoose.createConnection('your connection string'), + MyModel = conn.model('ModelName', schema), + m = new MyModel; m.save(); // works ``` vs ```js -var conn = mongoose.createConnection('your connection string') - , MyModel = mongoose.model('ModelName', schema) - , m = new MyModel; +var conn = mongoose.createConnection('your connection string'), + MyModel = mongoose.model('ModelName', schema), + m = new MyModel; m.save(); // does not work b/c the default connection object was never connected ``` @@ -217,7 +222,6 @@ BlogPost.findById(myId, function (err, post) { Embedded documents enjoy all the same features as your models. Defaults, validators, middleware. Whenever an error occurs, it's bubbled to the `save()` error callback, so error handling is a snap! -Mongoose interacts with your embedded documents in arrays _atomically_, out of the box. ### Middleware @@ -269,30 +273,33 @@ Moreover, you can mutate the incoming `method` arguments so that subsequent midd ```js new Schema({ - broken: { type: Boolean } - , asset : { - name: String - , type: String // uh oh, it broke. asset will be interpreted as String - } + broken: { type: Boolean }, + asset: { + name: String, + type: String // uh oh, it broke. asset will be interpreted as String + } }); new Schema({ - works: { type: Boolean } - , asset : { - name: String - , type: { type: String } // works. asset is an object with a type property - } + works: { type: Boolean }, + asset: { + name: String, + type: { type: String } // works. asset is an object with a type property + } }); ``` -### Driver access +### Driver Access -The driver being used defaults to [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) and is directly accessible through `YourModel.collection`. **Note**: using the driver directly bypasses all Mongoose power-tools like validation, getters, setters, hooks, etc. +Mongoose is built on top of the [official MongoDB Node.js driver](https://github.com/mongodb/node-mongodb-native). Each mongoose model keeps a reference to a [native MongoDB driver collection](http://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html). The collection object can be accessed using `YourModel.collection`. However, using the collection object directly bypasses all mongoose features, including hooks, validation, etc. The one +notable exception that `YourModel.collection` still buffers +commands. As such, `YourModel.collection.find()` will **not** +return a cursor. ## API Docs -Find the API docs [here](http://mongoosejs.com/docs/api.html), generated using [dox](http://github.com/visionmedia/dox) -and [acquit](http://github.com/vkarpov15/acquit). +Find the API docs [here](http://mongoosejs.com/docs/api.html), generated using [dox](https://github.com/tj/dox) +and [acquit](https://github.com/vkarpov15/acquit). ## License diff --git a/benchmarks/benchjs/casting.js b/benchmarks/benchjs/casting.js index c2a715680ee..1b9b3dbeb91 100644 --- a/benchmarks/benchjs/casting.js +++ b/benchmarks/benchjs/casting.js @@ -1,4 +1,3 @@ - var mongoose = require('../../lib'); var Benchmark = require('benchmark'); @@ -62,7 +61,7 @@ var blogData = { mixed: { thisIsRandom: true }, - numbers: [1,2,7,10,23432], + numbers: [1, 2, 7, 10, 23432], tags: ['test', 'BENCH', 'things', 'more things'], def: 'THANGS!!!', comments: [] @@ -85,39 +84,39 @@ for (i = 0; i < 10000; i++) { blogData10000.comments.push(commentData); } var commentData = { - title : 'test comment', - date : new Date(), - body : 'this be some crazzzyyyyy text that would go in a comment', - comments : [{ title : 'second level', date : new Date(), body : 'texttt'}] + title: 'test comment', + date: new Date(), + body: 'this be some crazzzyyyyy text that would go in a comment', + comments: [{title: 'second level', date: new Date(), body: 'texttt'}] }; BlogPost = mongoose.model('BlogPost', BlogPost); suite.add('Casting - Embedded Docs - 0 Docs', { - fn : function() { + fn: function() { var BlogPost = mongoose.model('BlogPost'); var bp = new BlogPost(); bp.init(blogData); } }).add('Casting - Embedded Docs - 10 Docs', { - fn : function() { + fn: function() { var BlogPost = mongoose.model('BlogPost'); var bp = new BlogPost(); bp.init(blogData10); } }).add('Casting - Embedded Docs - 100 Docs', { - fn : function() { + fn: function() { var BlogPost = mongoose.model('BlogPost'); var bp = new BlogPost(); bp.init(blogData100); } }).add('Casting - Embedded Docs - 1000 Docs', { - fn : function() { + fn: function() { var BlogPost = mongoose.model('BlogPost'); var bp = new BlogPost(); bp.init(blogData1000); } }).add('Casting - Embedded Docs - 10000 Docs', { - fn : function() { + fn: function() { var BlogPost = mongoose.model('BlogPost'); var bp = new BlogPost(); bp.init(blogData10000); @@ -135,8 +134,8 @@ suite.add('Casting - Embedded Docs - 0 Docs', { out.stats = item.stats; delete out.stats.sample; out.ops = item.hz; - outObj[item.name.replace(/\s/g, "")] = out; + outObj[item.name.replace(/\s/g, '')] = out; }); - console.log(JSON.stringify(outObj)); + console.dir(outObj, {depth: null, colors: true}); } -}).run({ async : true }); +}).run({async: true}); diff --git a/benchmarks/benchjs/delete.js b/benchmarks/benchjs/delete.js index 5937c745e10..3791ad3a5ad 100644 --- a/benchmarks/benchjs/delete.js +++ b/benchmarks/benchjs/delete.js @@ -1,4 +1,3 @@ - var mongoose = require('../../lib'); var Benchmark = require('benchmark'); @@ -19,11 +18,15 @@ var mongo = require('mongodb'); mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { - if (err) throw err; + if (err) { + throw err; + } mongo.connect('mongodb://localhost/mongoose-bench', function(err, db) { - if (err) throw err; + if (err) { + throw err; + } var UserSchema = new Schema({ - name : String, + name: String, age: Number, likes: [String], address: String @@ -36,17 +39,19 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { var dIds = []; var data = { - name : "name", - age : 0, - likes : ["dogs", "cats", "pizza"], - address : " Nowhere-ville USA" + name: 'name', + age: 0, + likes: ['dogs', 'cats', 'pizza'], + address: ' Nowhere-ville USA' }; // insert all of the data here var count = 1000; for (var i = 0; i < 500; i++) { User.create(data, function(err, u) { - if (err) throw err; + if (err) { + throw err; + } mIds.push(u.id); --count || next(); }); @@ -56,40 +61,46 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { nData.likes = data.likes; nData.address = data.address; user.insert(nData, function(err, res) { - dIds.push(res[0]._id); + dIds.push(res.insertedIds[0]); --count || next(); }); } function closeDB() { User.count(function(err, res) { - if (res != 0) { - console.log("Still mongoose entries left..."); + if (res !== 0) { + console.log('Still mongoose entries left...'); } mongoose.disconnect(); }); user.count({}, function(err, res) { - if (res != 0) { - console.log("Still driver entries left..."); + if (res !== 0) { + console.log('Still driver entries left...'); + } + if (err) { + throw err; } - if (err) throw err; db.close(); }); } suite.add('Delete - Mongoose', { - defer : true, - fn : function(deferred) { - User.remove({ _id : mIds.pop()}, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + User.remove({_id: mIds.pop()}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Delete - Driver', { - defer : true, - fn : function(deferred) { - user.remove({ _id : dIds.pop()}, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + user.remove({_id: dIds.pop()}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } @@ -107,13 +118,13 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { out.stats = item.stats; delete out.stats.sample; out.ops = item.hz; - outObj[item.name.replace(/\s/g, "")] = out; + outObj[item.name.replace(/\s/g, '')] = out; }); - console.log(JSON.stringify(outObj)); + console.dir(outObj, {depth: null, colors: true}); } }); function next() { - suite.run({ async : true }); + suite.run({async: true}); } }); }); diff --git a/benchmarks/benchjs/insert.js b/benchmarks/benchjs/insert.js index 192bf0f3b9f..a8c15117304 100644 --- a/benchmarks/benchjs/insert.js +++ b/benchmarks/benchjs/insert.js @@ -1,4 +1,3 @@ - var mongoose = require('../../lib'); var Benchmark = require('benchmark'); @@ -21,9 +20,13 @@ var ObjectId = Schema.Types.ObjectId; mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { - if (err) throw err; + if (err) { + throw err; + } mongo.connect('mongodb://localhost/mongoose-bench', function(err, db) { - if (err) throw err; + if (err) { + throw err; + } var Comments = new Schema; Comments.add({ @@ -59,10 +62,10 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { author: 'somebody', slug: 'test.post', date: new Date(), - meta: { date : new Date(), visitors: 9001}, + meta: {date: new Date(), visitors: 9001}, published: true, - mixed: { thisIsRandom : true }, - numbers: [1,2,7,10,23432], + mixed: {thisIsRandom: true}, + numbers: [1, 2, 7, 10, 23432], tags: ['test', 'BENCH', 'things', 'more things'], def: 'THANGS!!!', comments: [] @@ -81,14 +84,14 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { blogData.comments.push(commentData); } var data = { - name : "name", - age : 0, - likes : ["dogs", "cats", "pizza"], - address : " Nowhere-ville USA" + name: 'name', + age: 0, + likes: ['dogs', 'cats', 'pizza'], + address: ' Nowhere-ville USA' }; var UserSchema = new Schema({ - name : String, + name: String, age: Number, likes: [String], address: String @@ -107,39 +110,46 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { } suite.add('Insert - Mongoose - Basic', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var nData = utils.clone(data); User.create(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Insert - Driver - Basic', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var nData = utils.clone(data); user.insert(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Insert - Mongoose - Embedded Docs', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var bp = utils.clone(blogData); BlogPost.create(bp, function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Insert - Driver - Embedded Docs', { - defer : true, - fn : function(deferred) { - + defer: true, + fn: function(deferred) { var bp = utils.clone(blogData); blogpost.insert(bp, function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } @@ -157,10 +167,10 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { out.stats = item.stats; delete out.stats.sample; out.ops = item.hz; - outObj[item.name.replace(/\s/g, "")] = out; + outObj[item.name.replace(/\s/g, '')] = out; }); - console.log(JSON.stringify(outObj)); + console.dir(outObj, {depth: null, colors: true}); } - }).run({ async : true }); + }).run({async: true}); }); }); diff --git a/benchmarks/benchjs/multiop.js b/benchmarks/benchjs/multiop.js index c6830eb34f9..79fc73d38f7 100644 --- a/benchmarks/benchjs/multiop.js +++ b/benchmarks/benchjs/multiop.js @@ -1,4 +1,3 @@ - var mongoose = require('../../lib'); var Benchmark = require('benchmark'); @@ -21,9 +20,13 @@ var utils = require('../../lib/utils.js'); mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { - if (err) throw err; + if (err) { + throw err; + } mongo.connect('mongodb://localhost/mongoose-bench', function(err, db) { - if (err) throw err; + if (err) { + throw err; + } var Comments = new Schema; Comments.add({ @@ -48,7 +51,7 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { tags: [String], owners: [ObjectId], comments: [Comments], - def : { type: String, default: 'kandinsky' } + def: {type: String, default: 'kandinsky'} }); var blogData = { @@ -64,7 +67,7 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { mixed: { thisIsRandom: true }, - numbers: [1,2,7,10,23432], + numbers: [1, 2, 7, 10, 23432], tags: ['test', 'BENCH', 'things', 'more things'], def: 'THANGS!!!', comments: [] @@ -74,16 +77,16 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { date: new Date(), body: 'this be some crazzzyyyyy text that would go in a comment', comments: [{ - title : 'second level', - date : new Date(), - body : 'texttt' + title: 'second level', + date: new Date(), + body: 'texttt' }] }; for (var i = 0; i < 5; i++) { blogData.comments.push(commentData); } var UserSchema = new Schema({ - name : String, + name: String, age: Number, likes: [String], address: String @@ -101,10 +104,10 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { var bdIds = []; var data = { - name : "name", - age : 0, - likes : ["dogs", "cats", "pizza"], - address : " Nowhere-ville USA" + name: 'name', + age: 0, + likes: ['dogs', 'cats', 'pizza'], + address: ' Nowhere-ville USA' }; // insert all of the data here @@ -112,26 +115,34 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { for (i = 0; i < 1000; i++) { data.age = Math.floor(Math.random() * 50); User.create(data, function(err, u) { - if (err) throw err; + if (err) { + throw err; + } mIds.push(u.id); --count || next(); }); var nData = utils.clone(data); user.insert(nData, function(err, res) { - if (err) throw err; - dIds.push(res[0]._id); + if (err) { + throw err; + } + dIds.push(res.insertedIds[0]); --count || next(); }); BlogPost.create(blogData, function(err, bp) { - if (err) throw err; + if (err) { + throw err; + } bmIds.push(bp.id); --count || next(); }); var bpData = utils.clone(blogData); blogpost.insert(bpData, function(err, res) { - if (err) throw err; - bdIds.push(res[0]._id); + if (err) { + throw err; + } + bdIds.push(res.insertedIds[0]); --count || next(); }); } @@ -169,209 +180,252 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { } suite.add('Multi-Op - Mongoose - Heavy Read, low write', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; for (var i = 0; i < 150; i++) { - User.findOne({ _id : getNextmId() }, function(err) { - if (err) throw err; + User.findOne({_id: getNextmId()}, function(err) { + if (err) { + throw err; + } --count || deferred.resolve(); }); - if (i % 15 == 0) { + if (i % 15 === 0) { var nData = utils.clone(data); User.create(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); } } - } }).add('Multi-Op - Driver - Heavy Read, low write', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; for (var i = 0; i < 150; i++) { - user.findOne({ _id : getNextdId() }, function(err) { - if (err) throw err; + user.findOne({_id: getNextdId()}, function(err) { + if (err) { + throw err; + } --count || deferred.resolve(); }); - if (i % 15 == 0) { + if (i % 15 === 0) { var nData = utils.clone(data); user.insert(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); } } } }).add('Multi-Op - Mongoose - Embedded Docs - Heavy Read, low write', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; for (var i = 0; i < 150; i++) { - BlogPost.findOne({ _id : getNextbmId() }, function(err) { - if (err) throw err; + BlogPost.findOne({_id: getNextbmId()}, function(err) { + if (err) { + throw err; + } --count || deferred.resolve(); }); - if (i % 15 == 0) { + if (i % 15 === 0) { var nData = utils.clone(blogData); BlogPost.create(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); } } } }).add('Multi-Op - Driver - Embedded Docs - Heavy Read, low write', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; for (var i = 0; i < 150; i++) { - blogpost.findOne({ _id : getNextbdId() }, function(err) { - if (err) throw err; + blogpost.findOne({_id: getNextbdId()}, function(err) { + if (err) { + throw err; + } --count || deferred.resolve(); }); - if (i % 15 == 0) { + if (i % 15 === 0) { var nData = utils.clone(blogData); blogpost.insert(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); } } } }).add('Multi-Op - Mongoose - Heavy Write, low read', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; for (var i = 0; i < 150; i++) { var nData = utils.clone(data); User.create(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); - if (i % 15 == 0) { - User.findOne({ _id : getNextmId() }, function(err) { - if (err) throw err; + if (i % 15 === 0) { + User.findOne({_id: getNextmId()}, function(err) { + if (err) { + throw err; + } --count || deferred.resolve(); }); } } } }).add('Multi-Op - Driver - Heavy Write, low read', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; for (var i = 0; i < 150; i++) { var nData = utils.clone(data); user.insert(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); - if (i % 15 == 0) { - user.findOne({ _id : getNextdId() }, function(err) { - if (err) throw err; + if (i % 15 === 0) { + user.findOne({_id: getNextdId()}, function(err) { + if (err) { + throw err; + } --count || deferred.resolve(); }); } } } }).add('Multi-Op - Mongoose - Embedded Docs - Heavy Write, low read', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; for (var i = 0; i < 150; i++) { var nData = utils.clone(blogData); BlogPost.create(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); - if (i % 15 == 0) { - BlogPost.findOne({ _id : getNextbmId() }, function(err) { - if (err) throw err; + if (i % 15 === 0) { + BlogPost.findOne({_id: getNextbmId()}, function(err) { + if (err) { + throw err; + } --count || deferred.resolve(); }); } } } }).add('Multi-Op - Driver - Embedded Docs - Heavy Write, low read', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; for (var i = 0; i < 150; i++) { var nData = utils.clone(blogData); blogpost.insert(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); - if (i % 15 == 0) { - blogpost.findOne({ _id : getNextbdId() }, function(err) { - if (err) throw err; + if (i % 15 === 0) { + blogpost.findOne({_id: getNextbdId()}, function(err) { + if (err) { + throw err; + } --count || deferred.resolve(); }); } } } }).add('Multi-Op - Mongoose - Embedded Docs - Read-write-update', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; var updates = 0; for (var i = 0; i < 150; i++) { - BlogPost.findOne({ _id : getNextbmId() }, function(err, res) { - if (err) throw err; + BlogPost.findOne({_id: getNextbmId()}, function(err, res) { + if (err) { + throw err; + } if (updates < 20) { updates++; - res.author = "soemthing new"; + res.author = 'soemthing new'; res.comments.push(commentData); - res.title = "something newerrrr"; + res.title = 'something newerrrr'; res.save(function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); } else { --count || deferred.resolve(); } }); - if (i % 15 == 0) { + if (i % 15 === 0) { var nData = utils.clone(blogData); BlogPost.create(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); } } } }).add('Multi-Op - Driver - Embedded Docs - Read-write-update', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var count = 150; var updates = 0; for (var i = 0; i < 150; i++) { - blogpost.findOne({ _id : getNextbdId() }, function(err, bp) { - if (err) throw err; + blogpost.findOne({_id: getNextbdId()}, function(err, bp) { + if (err) { + throw err; + } if (updates < 20) { updates++; - bp.author = "soemthing new"; + bp.author = 'soemthing new'; bp.comments.push(commentData); - bp.title = "something newerrrr"; + bp.title = 'something newerrrr'; blogpost.save(bp, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); } else { --count || deferred.resolve(); } }); - if (i % 15 == 0) { + if (i % 15 === 0) { var nData = utils.clone(blogData); blogpost.insert(nData, function(err) { - if (err) throw err; + if (err) { + throw err; + } --count || deferred.resolve(); }); } @@ -391,13 +445,13 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { out.stats = item.stats; delete out.stats.sample; out.ops = item.hz; - outObj[item.name.replace(/\s/g, "")] = out; + outObj[item.name.replace(/\s/g, '')] = out; }); - console.log(JSON.stringify(outObj)); + console.dir(outObj, {depth: null, colors: true}); } }); function next() { - suite.run({ async : true }); + suite.run({async: true}); } }); }); diff --git a/benchmarks/benchjs/population.js b/benchmarks/benchjs/population.js index 73868fea41c..5942c8ac9f1 100644 --- a/benchmarks/benchjs/population.js +++ b/benchmarks/benchjs/population.js @@ -1,4 +1,3 @@ - var mongoose = require('../../lib'); var Benchmark = require('benchmark'); @@ -20,7 +19,9 @@ var utils = require('../../lib/utils.js'); mongoose.connect('mongodb://localhost/mongoose-bench-pop', function(err) { - if (err) throw err; + if (err) { + throw err; + } var commentSchema = new Schema; commentSchema.add({ @@ -78,49 +79,49 @@ mongoose.connect('mongodb://localhost/mongoose-bench-pop', function(err) { numbers: [Number], tags: [String], owners: [ObjectId], - comments: [{ type : ObjectId, ref : 'Comment' }], - dummy1: [{ type : ObjectId, ref : 'Dummy1' }], - dummy2: [{ type : ObjectId, ref : 'Dummy2' }], - dummy3: [{ type : ObjectId, ref : 'Dummy3' }], - dummy4: [{ type : ObjectId, ref : 'Dummy4' }], - dummy5: [{ type : ObjectId, ref : 'Dummy5' }], - dummy6: [{ type : ObjectId, ref : 'Dummy6' }], - dummy7: [{ type : ObjectId, ref : 'Dummy7' }], - dummy8: [{ type : ObjectId, ref : 'Dummy8' }], - dummy9: [{ type : ObjectId, ref : 'Dummy9' }], - def: { type: String, default: 'kandinsky' } + comments: [{type: ObjectId, ref: 'Comment'}], + dummy1: [{type: ObjectId, ref: 'Dummy1'}], + dummy2: [{type: ObjectId, ref: 'Dummy2'}], + dummy3: [{type: ObjectId, ref: 'Dummy3'}], + dummy4: [{type: ObjectId, ref: 'Dummy4'}], + dummy5: [{type: ObjectId, ref: 'Dummy5'}], + dummy6: [{type: ObjectId, ref: 'Dummy6'}], + dummy7: [{type: ObjectId, ref: 'Dummy7'}], + dummy8: [{type: ObjectId, ref: 'Dummy8'}], + dummy9: [{type: ObjectId, ref: 'Dummy9'}], + def: {type: String, default: 'kandinsky'} }); var blogData = { - title : 'dummy post', - author : 'somebody', - slug : 'test.post', - date : new Date(), - meta : { date : new Date(), visitors: 9001}, - published : true, - mixed : { thisIsRandom : true }, - numbers : [1,2,7,10,23432], - tags : ['test', 'BENCH', 'things', 'more things'], - def : 'THANGS!!!', - comments : [], - dummy1 : [], - dummy2 : [], - dummy3 : [], - dummy4 : [], - dummy5 : [], - dummy6 : [], - dummy7 : [], - dummy8 : [], - dummy9 : [] + title: 'dummy post', + author: 'somebody', + slug: 'test.post', + date: new Date(), + meta: {date: new Date(), visitors: 9001}, + published: true, + mixed: {thisIsRandom: true}, + numbers: [1, 2, 7, 10, 23432], + tags: ['test', 'BENCH', 'things', 'more things'], + def: 'THANGS!!!', + comments: [], + dummy1: [], + dummy2: [], + dummy3: [], + dummy4: [], + dummy5: [], + dummy6: [], + dummy7: [], + dummy8: [], + dummy9: [] }; var commentData = { - title : 'test comment', - date : new Date(), - body : 'this be some crazzzyyyyy text that would go in a comment' + title: 'test comment', + date: new Date(), + body: 'this be some crazzzyyyyy text that would go in a comment' }; var dummyData = { - title : "dummy data~", - isThisTest : true + title: 'dummy data~', + isThisTest: true }; var Comments = mongoose.model('Comment', commentSchema); BlogPost = mongoose.model('BlogPost', BlogPost); @@ -146,47 +147,65 @@ mongoose.connect('mongodb://localhost/mongoose-bench-pop', function(err) { --cn || cont(); }); Dummy1.create(dummyData, function(err, d) { - if (err) throw err; + if (err) { + throw err; + } dIds[0].push(d.id); --cn || cont(); }); Dummy2.create(dummyData, function(err, d) { - if (err) throw err; + if (err) { + throw err; + } dIds[1].push(d.id); --cn || cont(); }); Dummy3.create(dummyData, function(err, d) { - if (err) throw err; + if (err) { + throw err; + } dIds[2].push(d.id); --cn || cont(); }); Dummy4.create(dummyData, function(err, d) { - if (err) throw err; + if (err) { + throw err; + } dIds[3].push(d.id); --cn || cont(); }); Dummy5.create(dummyData, function(err, d) { - if (err) throw err; + if (err) { + throw err; + } dIds[4].push(d.id); --cn || cont(); }); Dummy6.create(dummyData, function(err, d) { - if (err) throw err; + if (err) { + throw err; + } dIds[5].push(d.id); --cn || cont(); }); Dummy7.create(dummyData, function(err, d) { - if (err) throw err; + if (err) { + throw err; + } dIds[6].push(d.id); --cn || cont(); }); Dummy8.create(dummyData, function(err, d) { - if (err) throw err; + if (err) { + throw err; + } dIds[7].push(d.id); --cn || cont(); }); Dummy9.create(dummyData, function(err, d) { - if (err) throw err; + if (err) { + throw err; + } dIds[8].push(d.id); --cn || cont(); }); @@ -237,19 +256,23 @@ mongoose.connect('mongodb://localhost/mongoose-bench-pop', function(err) { blog[6].dummy4.push(getNextdId(8)); } - // insert all of the data here var count = 7; + + function iter(c) { + BlogPost.create(blog[c], function(err, bl) { + if (err) { + throw err; + } + blog[c] = bl; + --count || next(); + }); + } + + // insert all of the data here for (i = 0; i < blog.length; i++) { // use some closure magic to make sure we retain the index - (function(c) { - BlogPost.create(blog[c], function(err, bl) { - if (err) throw err; - blog[c] = bl; - --count || next(); - }); - })(i); + iter(i); } - } var ci = 0; @@ -278,58 +301,72 @@ mongoose.connect('mongodb://localhost/mongoose-bench-pop', function(err) { } suite.add('Populate - 1 value', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { blog[4].populate('comments', function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Populate - 10 values', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { blog[0].populate('comments', function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Populate - 100 values', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { blog[1].populate('comments', function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Populate - 1000 values', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { blog[2].populate('comments', function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Populate - 10000 values', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { blog[3].populate('comments', function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Populate - 5 properties', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { blog[5].populate('comments dummy1 dummy2 dummy3 dummy4', function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Populate - 10 properties', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { blog[6].populate('comments dummy1 dummy2 dummy3 dummy4 dummy5 dummy6 dummy7 dummy8 dummy9', function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } @@ -348,12 +385,12 @@ mongoose.connect('mongodb://localhost/mongoose-bench-pop', function(err) { out.stats = item.stats; delete out.stats.sample; out.ops = item.hz; - outObj[item.name.replace(/\s/g, "")] = out; + outObj[item.name.replace(/\s/g, '')] = out; }); - console.log(JSON.stringify(outObj)); + console.dir(outObj, {depth: null, colors: true}); } }); function next() { - suite.run({ async : true }); + suite.run({async: true}); } }); diff --git a/benchmarks/benchjs/read.js b/benchmarks/benchjs/read.js index 0597de2c518..868404f670b 100644 --- a/benchmarks/benchjs/read.js +++ b/benchmarks/benchjs/read.js @@ -1,4 +1,3 @@ - var mongoose = require('../../lib'); var Benchmark = require('benchmark'); @@ -21,16 +20,20 @@ var utils = require('../../lib/utils.js'); mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { - if (err) throw err; + if (err) { + throw err; + } mongo.connect('mongodb://localhost/mongoose-bench', function(err, db) { - if (err) throw err; + if (err) { + throw err; + } var Comments = new Schema; Comments.add({ - title : String, - date : Date, - body : String, - comments : [Comments] + title: String, + date: Date, + body: String, + comments: [Comments] }); var BlogPost = new Schema({ @@ -55,29 +58,29 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { }); var blogData = { - title : 'dummy post', - author : 'somebody', - slug : 'test.post', - date : new Date(), - meta : { date : new Date(), visitors: 9001}, - published : true, - mixed : { thisIsRandom : true }, - numbers : [1,2,7,10,23432], - tags : ['test', 'BENCH', 'things', 'more things'], - def : 'THANGS!!!', - comments : [] + title: 'dummy post', + author: 'somebody', + slug: 'test.post', + date: new Date(), + meta: {date: new Date(), visitors: 9001}, + published: true, + mixed: {thisIsRandom: true}, + numbers: [1, 2, 7, 10, 23432], + tags: ['test', 'BENCH', 'things', 'more things'], + def: 'THANGS!!!', + comments: [] }; var commentData = { - title : 'test comment', - date : new Date(), - body : 'this be some crazzzyyyyy text that would go in a comment', - comments : [{ title : 'second level', date : new Date(), body : 'texttt'}] + title: 'test comment', + date: new Date(), + body: 'this be some crazzzyyyyy text that would go in a comment', + comments: [{title: 'second level', date: new Date(), body: 'texttt'}] }; for (var i = 0; i < 5; i++) { blogData.comments.push(commentData); } var UserSchema = new Schema({ - name : String, + name: String, age: Number, likes: [String], address: String @@ -95,10 +98,10 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { var bdIds = []; var data = { - name : "name", - age : 0, - likes : ["dogs", "cats", "pizza"], - address : " Nowhere-ville USA" + name: 'name', + age: 0, + likes: ['dogs', 'cats', 'pizza'], + address: ' Nowhere-ville USA' }; // insert all of the data here @@ -106,26 +109,34 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { for (i = 0; i < 1000; i++) { data.age = Math.floor(Math.random() * 50); User.create(data, function(err, u) { - if (err) throw err; + if (err) { + throw err; + } mIds.push(u.id); --count || next(); }); var nData = utils.clone(data); user.insert(nData, function(err, res) { - if (err) throw err; - dIds.push(res[0]._id); + if (err) { + throw err; + } + dIds.push(res.insertedIds[0]); --count || next(); }); BlogPost.create(blogData, function(err, bp) { - if (err) throw err; + if (err) { + throw err; + } bmIds.push(bp.id); --count || next(); }); var bpData = utils.clone(blogData); blogpost.insert(bpData, function(err, res) { - if (err) throw err; - bdIds.push(res[0]._id); + if (err) { + throw err; + } + bdIds.push(res.insertedIds[0]); --count || next(); }); } @@ -163,96 +174,118 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { } suite.add('Read - Mongoose - Basic', { - defer : true, - fn : function(deferred) { - User.findOne({ _id : getNextmId()}, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + User.findOne({_id: getNextmId()}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Read - Driver - Basic', { - defer : true, - fn : function(deferred) { - user.findOne({ _id : getNextdId() }, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + user.findOne({_id: getNextdId()}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Read - Mongoose - With lean', { - defer : true, - fn : function(deferred) { - User.findOne({ _id : getNextmId()}, {}, { lean : true}, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + User.findOne({_id: getNextmId()}, {}, {lean: true}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Read - Mongoose - Multiple Items', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var ids = []; for (var i = 0; i < 25; i++) { ids.push(getNextmId()); } - User.find({ _id : { $in : ids }}, function(err) { - if (err) throw err; + User.find({_id: {$in: ids}}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Read - Driver - Multiple Items', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var ids = []; for (var i = 0; i < 25; i++) { ids.push(getNextdId()); } - user.find({ _id : { $in : ids }}, function(err, cursor) { - if (err) throw err; + user.find({_id: {$in: ids}}, function(err, cursor) { + if (err) { + throw err; + } cursor.toArray(function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); }); } }).add('Read - Mongoose - Non-index', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var age = Math.floor(Math.random() * 50); - User.find({ age : age }, function(err) { - if (err) throw err; + User.find({age: age}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Read - Driver - Non-index', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var age = Math.floor(Math.random() * 50); - user.find({ age : age }, function(err, cursor) { - if (err) throw err; + user.find({age: age}, function(err, cursor) { + if (err) { + throw err; + } cursor.toArray(function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); }); } }).add('Read - Mongoose - Embedded Docs', { - defer : true, - fn : function(deferred) { - - BlogPost.find({ _id : getNextbmId()}, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + BlogPost.find({_id: getNextbmId()}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Read - Driver - Embedded Docs', { - defer : true, - fn : function(deferred) { - - blogpost.find({ _id : getNextbdId() }, function(err, cursor) { - if (err) throw err; + defer: true, + fn: function(deferred) { + blogpost.find({_id: getNextbdId()}, function(err, cursor) { + if (err) { + throw err; + } cursor.toArray(function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); }); @@ -271,13 +304,13 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { out.stats = item.stats; delete out.stats.sample; out.ops = item.hz; - outObj[item.name.replace(/\s/g, "")] = out; + outObj[item.name.replace(/\s/g, '')] = out; }); - console.log(JSON.stringify(outObj)); + console.dir(outObj, {depth: null, colors: true}); } }); function next() { - suite.run({ async : true }); + suite.run({async: true}); } }); }); diff --git a/benchmarks/benchjs/update.js b/benchmarks/benchjs/update.js index 7d7ed74a690..3c32a9b20ef 100644 --- a/benchmarks/benchjs/update.js +++ b/benchmarks/benchjs/update.js @@ -1,4 +1,3 @@ - var mongoose = require('../../lib'); var Benchmark = require('benchmark'); @@ -21,60 +20,64 @@ var utils = require('../../lib/utils.js'); mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { - if (err) throw err; + if (err) { + throw err; + } mongo.connect('mongodb://localhost/mongoose-bench', function(err, db) { - if (err) throw err; + if (err) { + throw err; + } var Comments = new Schema; Comments.add({ - title : String, - date : Date, - body : String, - comments : [Comments] + title: String, + date: Date, + body: String, + comments: [Comments] }); var BlogPost = new Schema({ - title : String, - author : String, - slug : String, - date : Date, - meta : { - date : Date, - visitors : Number + title: String, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number }, - published : Boolean, - mixed : {}, - numbers : [Number], - tags : [String], - owners : [ObjectId], - comments : [Comments], - def : { type: String, default: 'kandinsky' } + published: Boolean, + mixed: {}, + numbers: [Number], + tags: [String], + owners: [ObjectId], + comments: [Comments], + def: {type: String, default: 'kandinsky'} }); var blogData = { - title : 'dummy post', - author : 'somebody', - slug : 'test.post', - date : new Date(), - meta : { date : new Date(), visitors: 9001}, - published : true, - mixed : { thisIsRandom : true }, - numbers : [1,2,7,10,23432], - tags : ['test', 'BENCH', 'things', 'more things'], - def : 'THANGS!!!', - comments : [] + title: 'dummy post', + author: 'somebody', + slug: 'test.post', + date: new Date(), + meta: {date: new Date(), visitors: 9001}, + published: true, + mixed: {thisIsRandom: true}, + numbers: [1, 2, 7, 10, 23432], + tags: ['test', 'BENCH', 'things', 'more things'], + def: 'THANGS!!!', + comments: [] }; var commentData = { - title : 'test comment', - date : new Date(), - body : 'this be some crazzzyyyyy text that would go in a comment', - comments : [{ title : 'second level', date : new Date(), body : 'texttt'}] + title: 'test comment', + date: new Date(), + body: 'this be some crazzzyyyyy text that would go in a comment', + comments: [{title: 'second level', date: new Date(), body: 'texttt'}] }; for (var i = 0; i < 5; i++) { blogData.comments.push(commentData); } var UserSchema = new Schema({ - name : String, + name: String, age: Number, likes: [String], address: String @@ -92,10 +95,10 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { var bdIds = []; var data = { - name : "name", - age : 0, - likes : ["dogs", "cats", "pizza"], - address : " Nowhere-ville USA" + name: 'name', + age: 0, + likes: ['dogs', 'cats', 'pizza'], + address: ' Nowhere-ville USA' }; // this is for some of the update tests below @@ -104,17 +107,21 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { var count = 4000; for (i = 0; i < 1000; i++) { User.create(data, function(err, u) { - if (err) throw err; + if (err) { + throw err; + } mIds.push(u.id); --count || next(); }); var nData = utils.clone(data); user.insert(nData, function(err, res) { - dIds.push(res[0]._id); + dIds.push(res.insertedIds[0]); --count || next(); }); BlogPost.create(blogData, function(err, bp) { - if (err) throw err; + if (err) { + throw err; + } bmIds.push(bp.id); testBp = bp; --count || next(); @@ -122,8 +129,10 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { var bpData = utils.clone(blogData); blogpost.insert(bpData, function(err, res) { - if (err) throw err; - bdIds.push(res[0]._id); + if (err) { + throw err; + } + bdIds.push(res.insertedIds[0]); --count || next(); }); } @@ -161,28 +170,33 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { } suite.add('Update - Mongoose - Basic', { - defer : true, - fn : function(deferred) { - User.update({ _id : getNextmId() }, { $set : { age : 2 }, $push : { likes : "metal" }}, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + User.update({_id: getNextmId()}, {$set: {age: 2}, $push: {likes: 'metal'}}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Update - Driver - Basic', { - defer : true, - fn : function(deferred) { - user.update({ _id : getNextdId() }, { $set : { age : 2 }, $push : { likes : "metal" }}, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + user.update({_id: getNextdId()}, {$set: {age: 2}, $push: {likes: 'metal'}}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Update - Mongoose - Embedded Docs', { - defer : true, - fn : function(deferred) { - - BlogPost.findOne({ _id : getNextbmId() }, function(err, bp) { - if (err) throw err; - bp.comments[3].title = "this is a new title"; + defer: true, + fn: function(deferred) { + BlogPost.findOne({_id: getNextbmId()}, function(err, bp) { + if (err) { + throw err; + } + bp.comments[3].title = 'this is a new title'; bp.comments[0].date = new Date(); bp.comments.push(commentData); // save in Mongoose behaves differently than it does in the driver. @@ -190,90 +204,111 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { // and only update fields that have been changed. This is meant to // illustrate that difference between the two bp.save(function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); }); } }).add('Update - Driver - Embdedded Docs', { - defer : true, - fn : function(deferred) { - - blogpost.findOne({ _id : getNextbdId() }, function(err, bp) { - if (err) throw err; - bp.comments[3].title = "this is a new title"; + defer: true, + fn: function(deferred) { + blogpost.findOne({_id: getNextbdId()}, function(err, bp) { + if (err) { + throw err; + } + bp.comments[3].title = 'this is a new title'; bp.comments[0].date = new Date(); bp.comments.push(commentData); blogpost.save(bp, function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); }); } }).add('Update - Mongoose - Multiple Documents', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var ids = []; for (var i = 0; i < 50; i++) { ids.push(getNextmId()); } - User.update({ _id : { $in : ids} }, { $set : { age : 2 } , $push : { likes : "metal" }}, function(err) { - if (err) throw err; + User.update({_id: {$in: ids}}, {$set: {age: 2}, $push: {likes: 'metal'}}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Update - Driver - Multiple Documents', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var ids = []; for (var i = 0; i < 50; i++) { ids.push(getNextdId()); } - user.update({ _id : { $in : ids} }, { $set : { age : 2 }, $push : { likes : "metal" }}, function(err) { - if (err) throw err; + user.update({_id: {$in: ids}}, {$set: {age: 2}, $push: {likes: 'metal'}}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Update - Mongoose - pop and push', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { testBp.comments.push(commentData); testBp.comments.$shift(); testBp.save(function(err) { - if (err) throw err; + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Update - Mongoose - Array Manipulation, parallel ops', { - defer : true, - fn : function(deferred) { + defer: true, + fn: function(deferred) { var done = false; - BlogPost.update({ _id : testBp.id }, { $pop : { comments : -1 }}, function(err) { - if (err) throw err; + BlogPost.update({_id: testBp.id}, {$pop: {comments: -1}}, function(err) { + if (err) { + throw err; + } done && deferred.resolve(); done = true; }); - BlogPost.update({ _id : testBp.id }, { $push : { comments : commentData }}, function(err) { - if (err) throw err; + BlogPost.update({_id: testBp.id}, {$push: {comments: commentData}}, function(err) { + if (err) { + throw err; + } done && deferred.resolve(); done = true; }); } }).add('Update - Mongoose - findOneAndModify', { - defer : true, - fn : function(deferred) { - BlogPost.findOneAndUpdate({ _id : getNextbmId() }, { $set : { age : 2 }, $push : { likes : "metal" }}, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + BlogPost.findOneAndUpdate({_id: getNextbmId()}, {$set: {age: 2}, $push: {likes: 'metal'}}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); } }).add('Update - Mongoose - find and update, separate ops', { - defer : true, - fn : function(deferred) { - BlogPost.findOne({ _id : getNextbmId() }, function(err, bp) { - if (err) throw err; - bp.update({ $set : { age : 2 }, $push : { likes : "metal" }}, function(err) { - if (err) throw err; + defer: true, + fn: function(deferred) { + BlogPost.findOne({_id: getNextbmId()}, function(err, bp) { + if (err) { + throw err; + } + bp.update({$set: {age: 2}, $push: {likes: 'metal'}}, function(err) { + if (err) { + throw err; + } deferred.resolve(); }); }); @@ -292,9 +327,9 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { out.stats = item.stats; delete out.stats.sample; out.ops = item.hz; - outObj[item.name.replace(/\s/g, "")] = out; + outObj[item.name.replace(/\s/g, '')] = out; }); - console.log(JSON.stringify(outObj)); + console.dir(outObj, {depth: null, colors: true}); } }); function next() { @@ -302,8 +337,10 @@ mongoose.connect('mongodb://localhost/mongoose-bench', function(err) { testBp.comments.push(commentData); } testBp.save(function(err) { - if (err) throw err; - suite.run({ async : true }); + if (err) { + throw err; + } + suite.run({async: true}); }); } }); diff --git a/benchmarks/clone.js b/benchmarks/clone.js index 198c6d61308..1c84e2ebb6a 100644 --- a/benchmarks/clone.js +++ b/benchmarks/clone.js @@ -1,4 +1,3 @@ - var mongoose = require('../'), Schema = mongoose.Schema; @@ -7,39 +6,44 @@ var DocSchema = new Schema({ }); var AllSchema = new Schema({ - string: { type: String, required: true }, - number: { type: Number, min: 10 }, - date : Date, - bool : Boolean, + string: {type: String, required: true}, + number: {type: Number, min: 10}, + date: Date, + bool: Boolean, buffer: Buffer, objectid: Schema.ObjectId, - array : Array, + array: Array, strings: [String], numbers: [Number], - dates : [Date], - bools : [Boolean], + dates: [Date], + bools: [Boolean], buffers: [Buffer], objectids: [Schema.ObjectId], - docs : { type: [DocSchema], validate: function() { return true; }}, - s: { nest: String } + docs: { + type: [DocSchema], validate: function() { + return true; + } + }, + s: {nest: String} }); var A = mongoose.model('A', AllSchema); var a = new A({ - string: "hello world", + string: 'hello world', number: 444848484, date: new Date, - bool: true,buffer: new Buffer(0), + bool: true, + buffer: new Buffer(0), objectid: new mongoose.Types.ObjectId(), - array: [4,{},[],"asdfa"], - strings: ["one","two","three","four"], - numbers:[72,6493,83984643,348282.55], - dates:[new Date, new Date, new Date], - bools:[true, false, false, true, true], + array: [4, {}, [], 'asdfa'], + strings: ['one', 'two', 'three', 'four'], + numbers: [72, 6493, 83984643, 348282.55], + dates: [new Date, new Date, new Date], + bools: [true, false, false, true, true], buffers: [new Buffer([33]), new Buffer([12])], objectids: [new mongoose.Types.ObjectId], - docs: [ {title: "yo"}, {title:"nowafasdi0fas asjkdfla fa" }], - s: { nest: 'hello there everyone!' } + docs: [{title: 'yo'}, {title: 'nowafasdi0fas asjkdfla fa'}], + s: {nest: 'hello there everyone!'} }); var start = new Date; @@ -48,7 +52,7 @@ var i = total; var len; for (i = 0, len = total; i < len; ++i) { - a.toObject({ depopulate: true }); + a.toObject({depopulate: true}); } var time = (new Date - start) / 1000; diff --git a/benchmarks/create.js b/benchmarks/create.js index cd5e4babe40..afa5a1d5826 100644 --- a/benchmarks/create.js +++ b/benchmarks/create.js @@ -1,4 +1,4 @@ -//require('nodetime').profile(); +// require('nodetime').profile(); var mongoose = require('../../mongoose'); var fs = require('fs'); @@ -6,26 +6,26 @@ var fs = require('fs'); var Schema = mongoose.Schema; var CheckItem = new Schema({ - name: { type: String }, - type: { type: String }, - pos: { type: Number } + name: {type: String}, + type: {type: String}, + pos: {type: Number} }); var Checklist = new Schema({ - name: { type: String }, - checkItems: { type: [CheckItem] } + name: {type: String}, + checkItems: {type: [CheckItem]} }); var Board = new Schema({ - checklists: { type: [Checklist] } + checklists: {type: [Checklist]} }); // var start1 = new Date(); Board = mongoose.model('Board', Board); -//var Cl = mongoose.model('Checklist', Checklist); +// var Cl = mongoose.model('Checklist', Checklist); var doc = JSON.parse(fs.readFileSync(__dirname + '/bigboard.json')); // var time1 = (new Date - start1); -//console.error('reading from disk and parsing JSON took %d ms', time1); +// console.error('reading from disk and parsing JSON took %d ms', time1); var start2 = new Date(); for (var i = 0; i < 1000; ++i) { diff --git a/benchmarks/index.js b/benchmarks/index.js index e0fe6db82de..32a978e719c 100644 --- a/benchmarks/index.js +++ b/benchmarks/index.js @@ -16,18 +16,18 @@ var DocSchema = new Schema({ var AllSchema = new Schema({ string: String, number: Number, - date : Date, - bool : Boolean, + date: Date, + bool: Boolean, buffer: Buffer, objectid: Schema.ObjectId, - array : Array, + array: Array, strings: [String], numbers: [Number], - dates : [Date], - bools : [Boolean], + dates: [Date], + bools: [Boolean], buffers: [Buffer], objectids: [Schema.ObjectId], - docs : [DocSchema] + docs: [DocSchema] }); var A = mongoose.model('A', AllSchema); @@ -53,7 +53,7 @@ function run(label, fn) { while (i--) { a = fn(); if (i % 2) { - a.toObject({ depopulate: true }); + a.toObject({depopulate: true}); } else { if (a._delta) { a._delta(); @@ -73,12 +73,12 @@ function run(label, fn) { res.heapUsed = used.heapUsed - started.heapUsed; log('change: ', res); a = res = used = time = started = start = total = i = null; - //console.error(((used.vsize - started.vsize) / 1048576)+' MB'); + // console.error(((used.vsize - started.vsize) / 1048576)+' MB'); } run('string', function() { return new A({ - string: "hello world" + string: 'hello world' }); }); run('number', function() { @@ -108,27 +108,27 @@ run('objectid', function() { }); run('array of mixed', function() { return new A({ - array: [4,{},[],"asdfa"] + array: [4, {}, [], 'asdfa'] }); }); run('array of strings', function() { return new A({ - strings: ["one","two","three","four"] + strings: ['one', 'two', 'three', 'four'] }); }); run('array of numbers', function() { return new A({ - numbers:[72,6493,83984643,348282.55] + numbers: [72, 6493, 83984643, 348282.55] }); }); run('array of dates', function() { return new A({ - dates:[new Date, new Date, new Date] + dates: [new Date, new Date, new Date] }); }); run('array of bools', function() { return new A({ - bools:[true, false, false, true, true] + bools: [true, false, false, true, true] }); }); run('array of buffers', function() { @@ -143,11 +143,11 @@ run('array of objectids', function() { }); run('array of docs', function() { return new A({ - docs: [ {title: "yo"}, {title:"nowafasdi0fas asjkdfla fa" }] + docs: [{title: 'yo'}, {title: 'nowafasdi0fas asjkdfla fa'}] }); }); -//console.error(a.toObject({depopulate:true})); -console.error('completed %d docs in %d seconds (%d dps)', numdocs, totaltime,numdocs / totaltime); +// console.error(a.toObject({depopulate:true})); +console.error('completed %d docs in %d seconds (%d dps)', numdocs, totaltime, numdocs / totaltime); // --trace-opt --trace-deopt --trace-bailout diff --git a/benchmarks/mem.js b/benchmarks/mem.js index cd568e136a2..7ba33d0f3a1 100644 --- a/benchmarks/mem.js +++ b/benchmarks/mem.js @@ -1,4 +1,3 @@ - var mongoose = require('../'), Schema = mongoose.Schema; @@ -9,31 +8,35 @@ var DocSchema = new Schema({ }); var AllSchema = new Schema({ - string: { type: String, required: true }, - number: { type: Number, min: 10 }, - date : Date, - bool : Boolean, + string: {type: String, required: true}, + number: {type: Number, min: 10}, + date: Date, + bool: Boolean, buffer: Buffer, objectid: Schema.ObjectId, - array : Array, + array: Array, strings: [String], numbers: [Number], - dates : [Date], - bools : [Boolean], + dates: [Date], + bools: [Boolean], buffers: [Buffer], objectids: [Schema.ObjectId], - docs : { type: [DocSchema], validate: function() { return true; }}, - s: { nest: String } + docs: { + type: [DocSchema], validate: function() { + return true; + } + }, + s: {nest: String} }); var A = mongoose.model('A', AllSchema); var methods = []; methods.push(function(a, cb) { - A.findOne({ _id: a._id }, cb); + A.findOne({_id: a._id}, cb); }); // 2 MB methods.push(function(a, cb) { - A.find({ _id: a._id, bool: a.bool }, cb); + A.find({_id: a._id, bool: a.bool}, cb); }); // 3.8 MB methods.push(function(a, cb) { A.findById(a._id, cb); @@ -51,35 +54,35 @@ methods.push(function(a, cb) { A.where('date', a.date).where('array').in(3).limit(10).exec(cb); }); // 1.82 MB methods.push(function(a, cb) { - A.update({ _id: a._id }, { $addToset: { array: "heeeeello" }}, cb); + A.update({_id: a._id}, {$addToset: {array: 'heeeeello'}}, cb); }); // 3.32 MB methods.push(function(a, cb) { - A.remove({ _id: a._id }, cb); + A.remove({_id: a._id}, cb); }); // 3.32 MB methods.push(function(a, cb) { A.find().where('objectids').exists().select('dates').limit(10).exec(cb); }); // 3.32 MB methods.push(function(a, cb) { - A.count({ strings: a.strings[2], number: a.number }, cb); + A.count({strings: a.strings[2], number: a.number}, cb); }); // 3.32 MB methods.push(function(a, cb) { - a.string = "asdfaf"; + a.string = 'asdfaf'; a.number = 38383838; a.date = new Date; a.bool = false; a.array.push(3); a.dates.push(new Date); a.bools.push([true, false]); - a.docs.addToSet({ title: 'woot' }); - a.strings.remove("three"); + a.docs.addToSet({title: 'woot'}); + a.strings.remove('three'); a.numbers.pull(72); a.objectids.$pop(); a.docs.pull.apply(a.docs, a.docs); - a.s.nest = "aooooooga"; + a.s.nest = 'aooooooga'; - if (i % 2) - a.toObject({ depopulate: true }); - else { + if (i % 2) { + a.toObject({depopulate: true}); + } else { if (a._delta) { a._delta(); } else { @@ -95,63 +98,65 @@ var start = new Date; var total = 10000; var i = total; -mongoose.connection.on('open', function() { +function done() { + var time = (new Date - start); + var used = process.memoryUsage(); + + var res = {}; + res.rss = used.rss - started.rss; + res.heapTotal = used.heapTotal - started.heapTotal; + res.heapUsed = used.heapUsed - started.heapUsed; + + console.error('took %d ms for %d docs (%d dps)', time, total, total / (time / 1000), 'change: ', res); + mongoose.connection.db.dropDatabase(function() { + mongoose.connection.close(); + }); +} - (function cycle() { - if (0 === i--) return done(); - var a = new A({ - string: "hello world", - number: 444848484, - date: new Date, - bool: true, - buffer: new Buffer(0), - objectid: new mongoose.Types.ObjectId(), - array: [4,{},[],"asdfa"], - strings: ["one","two","three","four"], - numbers:[72,6493,83984643,348282.55], - dates:[new Date, new Date, new Date], - bools:[true, false, false, true, true], - buffers: [new Buffer([33]), new Buffer([12])], - objectids: [new mongoose.Types.ObjectId], - docs: [ {title: "yo"}, {title:"nowafasdi0fas asjkdfla fa" }] - }); - - a.save(function() { - methods[Math.random() * methods.length | 0](a, function() { - a = null; - process.nextTick(cycle); - }); - }); - - //if (i%2) - //a.toObject({ depopulate: true }); - //else - //a._delta(); - - //if (!(i%50)) { - //var u = process.memoryUsage(); - //console.error('rss: %d, vsize: %d, heapTotal: %d, heapUsed: %d', - //u.rss, u.vsize, u.heapTotal, u.heapUsed); - //} - })(); - - function done() { - var time = (new Date - start); - var used = process.memoryUsage(); - - var res = {}; - res.rss = used.rss - started.rss; - res.heapTotal = used.heapTotal - started.heapTotal; - res.heapUsed = used.heapUsed - started.heapUsed; - - console.error('took %d ms for %d docs (%d dps)', time, total, total / (time / 1000), 'change: ', res); - - mongoose.connection.db.dropDatabase(function() { - mongoose.connection.close(); - }); - } +function cycle() { + if (i-- === 0) { + return done(); + } + var a = new A({ + string: 'hello world', + number: 444848484, + date: new Date, + bool: true, + buffer: new Buffer(0), + objectid: new mongoose.Types.ObjectId(), + array: [4, {}, [], 'asdfa'], + strings: ['one', 'two', 'three', 'four'], + numbers: [72, 6493, 83984643, 348282.55], + dates: [new Date, new Date, new Date], + bools: [true, false, false, true, true], + buffers: [new Buffer([33]), new Buffer([12])], + objectids: [new mongoose.Types.ObjectId], + docs: [{title: 'yo'}, {title: 'nowafasdi0fas asjkdfla fa'}] + }); - // --trace-opt --trace-deopt --trace-bailout + a.save(function() { + methods[Math.random() * methods.length | 0](a, function() { + a = null; + process.nextTick(cycle); + }); + }); + + /* if (i%2) + a.toObject({ depopulate: true }); + else + a._delta(); + + if (!(i%50)) { + var u = process.memoryUsage(); + console.error('rss: %d, vsize: %d, heapTotal: %d, heapUsed: %d', + u.rss, u.vsize, u.heapTotal, u.heapUsed); + } */ +} + +mongoose.connection.on('open', function() { + mongoose.connection.db.dropDatabase(function() { + cycle(); + // --trace-opt --trace-deopt --trace-bailout }); }); diff --git a/benchmarks/populate.js b/benchmarks/populate.js index 1ed3187b69f..5dedecbb9ac 100644 --- a/benchmarks/populate.js +++ b/benchmarks/populate.js @@ -1,17 +1,16 @@ - var mongoose = require('../'); var Schema = mongoose.Schema; var docs = process.argv[2] ? process.argv[2] | 0 : 100; -var A = mongoose.model('A', Schema({ name: 'string' })); +var A = mongoose.model('A', Schema({name: 'string'})); var nested = Schema({ - a: { type: Schema.ObjectId, ref: 'A' } + a: {type: Schema.ObjectId, ref: 'A'} }); var B = mongoose.model('B', Schema({ - as: [{ type: Schema.ObjectId, ref: 'A' }], - a: { type: Schema.ObjectId, ref: 'A' }, + as: [{type: Schema.ObjectId, ref: 'A'}], + a: {type: Schema.ObjectId, ref: 'A'}, nested: [nested] })); @@ -19,22 +18,28 @@ var start; var count = 0; mongoose.connect('localhost', 'benchmark-populate', function(err) { - if (err) return done(err); + if (err) { + return done(err); + } - A.create({ name: 'wooooooooooooooooooooooooooooooooooooooooot' }, function(err, a) { - if (err) return done(err); + A.create({name: 'wooooooooooooooooooooooooooooooooooooooooot'}, function(err, a) { + if (err) { + return done(err); + } var pending = docs; for (var i = 0; i < pending; ++i) { new B({ - as: [a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a], + as: [a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a], a: a, - nested: [{ a: a }, { a: a }, { a: a }, { a: a }, { a: a }, { a: a }] + nested: [{a: a}, {a: a}, {a: a}, {a: a}, {a: a}, {a: a}] }).save(function(err) { - if (err) return done(err); + if (err) { + return done(err); + } --pending; - if (0 === pending) { - //console.log('inserted %d docs. beginning test ...', docs); + if (pending === 0) { + // console.log('inserted %d docs. beginning test ...', docs); start = Date.now(); test(); } @@ -50,20 +55,26 @@ function test() { B.findOne().populate('as').populate('a').populate('nested.a').exec(handle); function handle(err) { - if (err) throw err; + if (err) { + throw err; + } count++; if (Date.now() - start > 1000 * 20) { return done(); } - if (0 === --pending) return test(); + if (--pending === 0) { + return test(); + } } } function done(err) { - if (err) console.error(err.stack); + if (err) { + console.error(err.stack); + } mongoose.connection.db.dropDatabase(function() { mongoose.disconnect(); diff --git a/benchmarks/populationHeavyLoad.js b/benchmarks/populationHeavyLoad.js index f0acb09244a..25f82d0bb4e 100644 --- a/benchmarks/populationHeavyLoad.js +++ b/benchmarks/populationHeavyLoad.js @@ -1,4 +1,3 @@ -/* eslint no-unused-vars: 1 */ var tbd = require('tbd'); var mongoose = require('../'); var Schema = mongoose.Schema; @@ -11,379 +10,401 @@ var mainCnt = process.argv[4] ? process.argv[4] | 0 : 25; var load = {}; function createRandomWord(len) { - var cons = 'bcdfghjklmnpqrstvwxyz', vow = 'aeiou', rand = function(limit) { return Math.floor(Math.random() * limit); }, i, word = '', length = parseInt(len, 10), consonants = cons.split(''), vowels = vow.split(''); - for (i = 0; i < length / 2; i++) { var randConsonant = consonants[rand(consonants.length)], randVowel = vowels[rand(vowels.length)]; word += (i === 0) ? randConsonant.toUpperCase() : randConsonant; word += i * 2 < length - 1 ? randVowel : ''; + var cons = 'bcdfghjklmnpqrstvwxyz'; + var vow = 'aeiou'; + var i, word = '', length = parseInt(len, 10), consonants = cons.split(''), vowels = vow.split(''); + function rand(limit) { + return Math.floor(Math.random() * limit); + } + for (i = 0; i < length / 2; i++) { + var randConsonant = consonants[rand(consonants.length)], + randVowel = vowels[rand(vowels.length)]; + word += (i === 0) ? randConsonant.toUpperCase() : randConsonant; + word += i * 2 < length - 1 ? randVowel : ''; } return word; } -var Medication = mongoose.model('Medication', Schema({ - catCode: { type: String }, - concentration: { type: String }, - medicationName: { type: String, required: true }, - notes: { type: String }, - qty: { type: Number } +mongoose.model('Medication', Schema({ + catCode: {type: String}, + concentration: {type: String}, + medicationName: {type: String, required: true}, + notes: {type: String}, + qty: {type: Number} })); load.Medication = tbd.from({ - catCode: "{ type: String }", - concentration: "{ type: String }", - medicationName: "{ type: String, required: true }", - notes: "{ type: String }", + catCode: '{ type: String }', + concentration: '{ type: String }', + medicationName: '{ type: String, required: true }', + notes: '{ type: String }', qty: 10 }).make(dicCnt); -var Prescription = mongoose.model('Prescription', Schema({ - age: { type: Number }, - city: { type: String }, - clientID: { type: String, required: true }, - clinic: {type: String }, - customerName: { type: String}, - dateNow: { type: Date}, - district: { type: String}, - fax: { type: String}, - firstName: { type: String}, - gender: { type: String}, - lastName: { type: String}, - mobile: { type: String}, - physicianLicenseNumber: { type: Number}, - physicianName: { type: String}, - prescription: { type: String}, - refByDoctor: { type: String}, - stampImage: { type: String}, - street: { type: String}, - tel: { type: String } +mongoose.model('Prescription', Schema({ + age: {type: Number}, + city: {type: String}, + clientID: {type: String, required: true}, + clinic: {type: String}, + customerName: {type: String}, + dateNow: {type: Date}, + district: {type: String}, + fax: {type: String}, + firstName: {type: String}, + gender: {type: String}, + lastName: {type: String}, + mobile: {type: String}, + physicianLicenseNumber: {type: Number}, + physicianName: {type: String}, + prescription: {type: String}, + refByDoctor: {type: String}, + stampImage: {type: String}, + street: {type: String}, + tel: {type: String} })); load.Prescription = tbd.from({ age: 50, - city: "{ type: String }", - clientID: "{ type: String, required: true }", - clinic: "{type: String }", - customerName: "{ type: String}", + city: '{ type: String }', + clientID: '{ type: String, required: true }', + clinic: '{type: String }', + customerName: '{ type: String}', dateNow: new Date(), - district: "{ type: String}", - fax: "{ type: String}", - firstName: "{ type: String}", - gender: "{ type: String}", - lastName: "{ type: String}", - mobile: "{ type: String}", + district: '{ type: String}', + fax: '{ type: String}', + firstName: '{ type: String}', + gender: '{ type: String}', + lastName: '{ type: String}', + mobile: '{ type: String}', physicianLicenseNumber: 15, - physicianName: "{ type: String}", - prescription: "{ type: String}", - refByDoctor: "{ type: String}", - stampImage: "{ type: String}", - street: "{ type: String}", - tel: "{ type: String }" + physicianName: '{ type: String}', + prescription: '{ type: String}', + refByDoctor: '{ type: String}', + stampImage: '{ type: String}', + street: '{ type: String}', + tel: '{ type: String }' }).make(dicCnt); -var Observation = mongoose.model('Observation', Schema({ - observationCode: { type: Number}, - observationDesc: { type: String}, - observationGroup: { type: String +mongoose.model('Observation', Schema({ + observationCode: {type: Number}, + observationDesc: {type: String}, + observationGroup: { + type: String } })); load.Observation = tbd.from({ observationCode: 100, - observationDesc: "{ type: String}", - observationGroup: "{ type: String}" + observationDesc: '{ type: String}', + observationGroup: '{ type: String}' }).make(dicCnt); -var Heart = mongoose.model('Heart', Schema({ - abdominalAscites: { type: String}, - abdominalCondition: { type: String}, - abdominalLocation: { type: String}, - heartLocation: { type: String}, - heartOther: { type: String}, - murmur: { type: String}, - npeHeart: { type: Boolean}, - rythm: { type: String}, - sounds: { type: String +mongoose.model('Heart', Schema({ + abdominalAscites: {type: String}, + abdominalCondition: {type: String}, + abdominalLocation: {type: String}, + heartLocation: {type: String}, + heartOther: {type: String}, + murmur: {type: String}, + npeHeart: {type: Boolean}, + rythm: {type: String}, + sounds: { + type: String } })); load.Heart = tbd.from({ - abdominalAscites:" { type: String}", - abdominalCondition: "{ type: String}", - abdominalLocation: "{ type: String}", - heartLocation:" { type: String}", - heartOther: "{ type: String}", - murmur:"{ type: String}", + abdominalAscites: ' { type: String}', + abdominalCondition: '{ type: String}', + abdominalLocation: '{ type: String}', + heartLocation: ' { type: String}', + heartOther: '{ type: String}', + murmur: '{ type: String}', npeHeart: true, - rythm: "{ type: String}", - sounds: "{ type: String }" + rythm: '{ type: String}', + sounds: '{ type: String }' }).make(dicCnt); -var PereferialPulse = mongoose.model('PereferialPulse', Schema({ - leftDP: { type: String}, - leftFemoral: { type: String}, - leftPopliteal: { type: String}, - leftTP: { type: String}, - rightDP: { type: String}, - rightTP: { type: String}, - rigthFemoral: { type: String}, - rigthPopliteal: { type: String +mongoose.model('PereferialPulse', Schema({ + leftDP: {type: String}, + leftFemoral: {type: String}, + leftPopliteal: {type: String}, + leftTP: {type: String}, + rightDP: {type: String}, + rightTP: {type: String}, + rigthFemoral: {type: String}, + rigthPopliteal: { + type: String } })); load.PereferialPulse = tbd.from({ - leftDP: "{ type: String}", - leftFemoral: "{ type: String}", - leftPopliteal: "{ type: String}", - leftTP: "{ type: String}", - rightDP: "{ type: String}", - rightTP: "{ type: String}", - rigthFemoral: "{ type: String}", - rigthPopliteal: "{ type: String}" + leftDP: '{ type: String}', + leftFemoral: '{ type: String}', + leftPopliteal: '{ type: String}', + leftTP: '{ type: String}', + rightDP: '{ type: String}', + rightTP: '{ type: String}', + rigthFemoral: '{ type: String}', + rigthPopliteal: '{ type: String}' }).make(dicCnt); -var Treatment = mongoose.model('Treatment', Schema({ - notes: { type: String}, - treatmentCode: { type: String}, - treatmentName: { type: String, required: true +mongoose.model('Treatment', Schema({ + notes: {type: String}, + treatmentCode: {type: String}, + treatmentName: { + type: String, required: true } })); load.Treatment = tbd.from({ - notes: "{ type: String}", - treatmentCode: "{ type: String}", - treatmentName: "{ type: String, required: true}" + notes: '{ type: String}', + treatmentCode: '{ type: String}', + treatmentName: '{ type: String, required: true}' }).make(dicCnt); -var VitalSign = mongoose.model('VitalSign', Schema({ - O2: { type: String}, - diastole: { type: String}, - heartSounds: { type: String}, - height: { type: String}, - lMin: { type: String}, - pulse: { type: String}, - respiratoryRate: { type: String}, - roomAir: { type: String}, - sugar: { type: String}, - systole: { type: String}, - temperature: { type: String}, - urineE: { type: String}, - urineG: { type: String}, - urineK: { type: String}, - urineL: { type: String}, - urineN: { type: String}, - urineP: { type: String}, - weight: { type: String +mongoose.model('VitalSign', Schema({ + O2: {type: String}, + diastole: {type: String}, + heartSounds: {type: String}, + height: {type: String}, + lMin: {type: String}, + pulse: {type: String}, + respiratoryRate: {type: String}, + roomAir: {type: String}, + sugar: {type: String}, + systole: {type: String}, + temperature: {type: String}, + urineE: {type: String}, + urineG: {type: String}, + urineK: {type: String}, + urineL: {type: String}, + urineN: {type: String}, + urineP: {type: String}, + weight: { + type: String } })); load.VitalSign = tbd.from({ - O2: "{ type: String}", - diastole: "{ type: String}", - heartSounds: "{ type: String}", - height: "{ type: String}", - lMin: "{ type: String}", - pulse: "{ type: String}", - respiratoryRate:" { type: String}", - roomAir: "{ type: String}", - sugar: "{ type: String}", - systole: "{ type: String}", - temperature: "{ type: String}", - urineE: "{ type: String}", - urineG: "{ type: String}", - urineK: "{ type: String}", - urineL:" { type: String}", - urineN: "{ type: String}", - urineP: "{ type: String}", - weight: "{ type: String}" + O2: '{ type: String}', + diastole: '{ type: String}', + heartSounds: '{ type: String}', + height: '{ type: String}', + lMin: '{ type: String}', + pulse: '{ type: String}', + respiratoryRate: ' { type: String}', + roomAir: '{ type: String}', + sugar: '{ type: String}', + systole: '{ type: String}', + temperature: '{ type: String}', + urineE: '{ type: String}', + urineG: '{ type: String}', + urineK: '{ type: String}', + urineL: ' { type: String}', + urineN: '{ type: String}', + urineP: '{ type: String}', + weight: '{ type: String}' }).make(dicCnt); -var FluidsProgram = mongoose.model('FluidsProgram', Schema({ +mongoose.model('FluidsProgram', Schema({ name: String })); load.FluidsProgram = tbd.from({ - name: "String" + name: 'String' }).make(dicCnt); -var Allergy = mongoose.model('Allergy', Schema({ - allergy: { type: String, required: true}, - notes: { type: String +mongoose.model('Allergy', Schema({ + allergy: {type: String, required: true}, + notes: { + type: String } })); load.Allergy = tbd.from({ - allergy: "{ type: String, required: true}", - notes: "{ type: String }" + allergy: '{ type: String, required: true}', + notes: '{ type: String }' }).make(dicCnt); -var Head = mongoose.model('Head', Schema({ - ears: { type: String}, - neckMurmur: { type: String}, - normalEarRight: { type: Boolean}, - npeEyeRight: { type: Boolean}, - npeNeck: { type: Boolean}, - npePharynx: { type: Boolean}, - pharunxField: { type: String}, - pharynxRed: { type: Boolean}, - pharynxRedEf: { type: Boolean}, - preEyeLeft: { type: Boolean}, - pupilLeft: { type: String}, - pupilRight: { type: String}, - reactive: { type: Boolean}, - redness: { type: String}, - secreting: { type: Boolean}, - venousCongestion: { type: Boolean +mongoose.model('Head', Schema({ + ears: {type: String}, + neckMurmur: {type: String}, + normalEarRight: {type: Boolean}, + npeEyeRight: {type: Boolean}, + npeNeck: {type: Boolean}, + npePharynx: {type: Boolean}, + pharunxField: {type: String}, + pharynxRed: {type: Boolean}, + pharynxRedEf: {type: Boolean}, + preEyeLeft: {type: Boolean}, + pupilLeft: {type: String}, + pupilRight: {type: String}, + reactive: {type: Boolean}, + redness: {type: String}, + secreting: {type: Boolean}, + venousCongestion: { + type: Boolean } })); load.Head = tbd.from({ - ears: "{ type: String}", - neckMurmur: "{ type: String}", + ears: '{ type: String}', + neckMurmur: '{ type: String}', normalEarRight: false, npeEyeRight: false, npeNeck: false, npePharynx: false, - pharunxField: "{ type: String}", + pharunxField: '{ type: String}', pharynxRed: false, pharynxRedEf: false, preEyeLeft: false, - pupilLeft: "{ type: String}", - pupilRight: "{ type: String}", + pupilLeft: '{ type: String}', + pupilRight: '{ type: String}', reactive: false, - redness: "{ type: String}", + redness: '{ type: String}', secreting: false, venousCongestion: false }).make(dicCnt); -var Sckeletal = mongoose.model('Sckeletal', Schema({ - consiousness: { type: String}, - dvtSigns: { type: Boolean}, - edema: { type: String}, - limbsPeripheralPulse: { type: String}, - musculoskeletal: { type: String}, - neurCerebellum: { type: String}, - neurCranialNerves: { type: String}, - neurNeckStiffiness: { type: Boolean}, - neurSideSign: { type: Boolean}, - npeLimbs: { type: Boolean}, - npeMusco: { type: Boolean}, - npeNeurologi: { type: Boolean}, - reflexes: { type: String +mongoose.model('Sckeletal', Schema({ + consiousness: {type: String}, + dvtSigns: {type: Boolean}, + edema: {type: String}, + limbsPeripheralPulse: {type: String}, + musculoskeletal: {type: String}, + neurCerebellum: {type: String}, + neurCranialNerves: {type: String}, + neurNeckStiffiness: {type: Boolean}, + neurSideSign: {type: Boolean}, + npeLimbs: {type: Boolean}, + npeMusco: {type: Boolean}, + npeNeurologi: {type: Boolean}, + reflexes: { + type: String } })); load.Sckeletal = tbd.from({ - consiousness: "{ type: String}", + consiousness: '{ type: String}', dvtSigns: false, - edema: "{ type: String}", - limbsPeripheralPulse: "{ type: String}", - musculoskeletal: "{ type: String}", - neurCerebellum: "{ type: String}", - neurCranialNerves: "{ type: String}", + edema: '{ type: String}', + limbsPeripheralPulse: '{ type: String}', + musculoskeletal: '{ type: String}', + neurCerebellum: '{ type: String}', + neurCranialNerves: '{ type: String}', neurNeckStiffiness: false, neurSideSign: false, npeLimbs: false, npeMusco: false, npeNeurologi: false, - reflexes: "{ type: String}" + reflexes: '{ type: String}' }).make(dicCnt); -var Pulse = mongoose.model('Pulse', Schema({ +mongoose.model('Pulse', Schema({ rate: String })); load.Pulse = tbd.from({ - rate: "String" + rate: 'String' }).make(dicCnt); -var Supplier = mongoose.model('Supplier', Schema({ - supplierDescription: { type: String}, - supplierQty: { type: Number}, - supplierRemarks: { type: String +mongoose.model('Supplier', Schema({ + supplierDescription: {type: String}, + supplierQty: {type: Number}, + supplierRemarks: { + type: String } })); load.Supplier = tbd.from({ - supplierDescription:" { type: String}", + supplierDescription: ' { type: String}', supplierQty: 5, - supplierRemarks: "{ type: String }" + supplierRemarks: '{ type: String }' }).make(dicCnt); -var Gastrointestinal = mongoose.model('Gastrointestinal', Schema({ - leftKidney: { type: String}, - liver: { type: String}, - rightKidney: { type: String}, - spleen: { type: String +mongoose.model('Gastrointestinal', Schema({ + leftKidney: {type: String}, + liver: {type: String}, + rightKidney: {type: String}, + spleen: { + type: String } })); load.Gastrointestinal = tbd.from({ - leftKidney: "{ type: String}", - liver: "{ type: String}", - rightKidney: "{ type: String}", - spleen: "{ type: String }" + leftKidney: '{ type: String}', + liver: '{ type: String}', + rightKidney: '{ type: String}', + spleen: '{ type: String }' }).make(dicCnt); -var Ecg = mongoose.model('Ecg', Schema({ - ecgAxis: { type: String}, - ecgConduction: { type: String}, - ecgIschemia: { type: String}, - ecgRate: { type: String}, - ecgRythem: { type: String}, - generalCondition: { type: String +mongoose.model('Ecg', Schema({ + ecgAxis: {type: String}, + ecgConduction: {type: String}, + ecgIschemia: {type: String}, + ecgRate: {type: String}, + ecgRythem: {type: String}, + generalCondition: { + type: String } })); load.Ecg = tbd.from({ - ecgAxis: "{ type: String}", - ecgConduction: "{ type: String}", - ecgIschemia: "{ type: String}", - ecgRate: "{ type: String}", - ecgRythem: "{ type: String}", - generalCondition:" { type: String }" + ecgAxis: '{ type: String}', + ecgConduction: '{ type: String}', + ecgIschemia: '{ type: String}', + ecgRate: '{ type: String}', + ecgRythem: '{ type: String}', + generalCondition: ' { type: String }' }).make(dicCnt); -var Diagnose = mongoose.model('Diagnose', Schema({ - diagNotes: { type: String}, - diagnoseGroup: { type: String}, - diagnoseName: { type: String, required: true}, - idDiagnose: { type: String +mongoose.model('Diagnose', Schema({ + diagNotes: {type: String}, + diagnoseGroup: {type: String}, + diagnoseName: {type: String, required: true}, + idDiagnose: { + type: String } })); load.Diagnose = tbd.from({ - diagNotes: "{ type: String}", - diagnoseGroup: "{ type: String}", - diagnoseName: "{ type: String, required: true}", - idDiagnose: "{ type: String }" + diagNotes: '{ type: String}', + diagnoseGroup: '{ type: String}', + diagnoseName: '{ type: String, required: true}', + idDiagnose: '{ type: String }' }).make(dicCnt); -var Conclusion = mongoose.model('Conclusion', Schema({ - approver: { type: String}, - daysScheduleVisits: { type: String}, - doctorLicense: { type: String}, - doctorName: { type: String}, - emergencyRoomReferral: { type: String}, - futureVisitIn: { type: Number}, - labelAddition: { type: String}, - lavelVisit: { type: String}, - nowData: { type: Date}, - periodical: { type: String}, - presentedTo: { type: String}, - stampImage: { type: String +mongoose.model('Conclusion', Schema({ + approver: {type: String}, + daysScheduleVisits: {type: String}, + doctorLicense: {type: String}, + doctorName: {type: String}, + emergencyRoomReferral: {type: String}, + futureVisitIn: {type: Number}, + labelAddition: {type: String}, + lavelVisit: {type: String}, + nowData: {type: Date}, + periodical: {type: String}, + presentedTo: {type: String}, + stampImage: { + type: String } })); load.Conclusion = tbd.from({ - approver: "{ type: String}", - daysScheduleVisits: "{ type: String}", - doctorLicense: "{ type: String}", - doctorName: "{ type: String}", - emergencyRoomReferral: "{ type: String}", + approver: '{ type: String}', + daysScheduleVisits: '{ type: String}', + doctorLicense: '{ type: String}', + doctorName: '{ type: String}', + emergencyRoomReferral: '{ type: String}', futureVisitIn: 112, - labelAddition: "{ type: String}", - lavelVisit: "{ type: String}", + labelAddition: '{ type: String}', + lavelVisit: '{ type: String}', nowData: new Date(), - periodical: "{ type: String}", - presentedTo: "{ type: String}", - stampImage: "{ type: String }" + periodical: '{ type: String}', + presentedTo: '{ type: String}', + stampImage: '{ type: String }' }).make(dicCnt); function getItem(list) { @@ -393,26 +414,26 @@ function getItem(list) { }; } -var MedicalCard = mongoose.model('MedicalCard', Schema({ +mongoose.model('MedicalCard', Schema({ firstName: String, lastName: String, - medication: [{ type: ObjectId, ref: 'Medication'}], - prescription: [{ type: ObjectId, ref: 'Prescription'}], - observation: [{ type: ObjectId, ref: 'Observation'}], - heart: [{ type: ObjectId, ref: 'Heart'}], - pereferialPulse: [{ type: ObjectId, ref: 'PereferialPulse'}], - treatment: [{ type: ObjectId, ref: 'Treatment'}], - vitalSign: [{ type: ObjectId, ref: 'VitalSign'}], - fluidsProgram: [{ type: ObjectId, ref: 'FluidsProgram'}], - allergy: [{ type: ObjectId, ref: 'Allergy'}], - head: [{ type: ObjectId, ref: 'Head'}], - sckeletal: [{ type: ObjectId, ref: 'Sckeletal'}], - pulse: [{ type: ObjectId, ref: 'Pulse'}], - supplier: [{ type: ObjectId, ref: 'Supplier'}], - gastrointestinal: [{ type: ObjectId, ref: 'Gastrointestinal'}], - ecg: [{ type: ObjectId, ref: 'Ecg'}], - diagnose: { type: ObjectId, ref: 'Diagnose'}, - conclusion: { type: ObjectId, ref: 'Conclusion'} + medication: [{type: ObjectId, ref: 'Medication'}], + prescription: [{type: ObjectId, ref: 'Prescription'}], + observation: [{type: ObjectId, ref: 'Observation'}], + heart: [{type: ObjectId, ref: 'Heart'}], + pereferialPulse: [{type: ObjectId, ref: 'PereferialPulse'}], + treatment: [{type: ObjectId, ref: 'Treatment'}], + vitalSign: [{type: ObjectId, ref: 'VitalSign'}], + fluidsProgram: [{type: ObjectId, ref: 'FluidsProgram'}], + allergy: [{type: ObjectId, ref: 'Allergy'}], + head: [{type: ObjectId, ref: 'Head'}], + sckeletal: [{type: ObjectId, ref: 'Sckeletal'}], + pulse: [{type: ObjectId, ref: 'Pulse'}], + supplier: [{type: ObjectId, ref: 'Supplier'}], + gastrointestinal: [{type: ObjectId, ref: 'Gastrointestinal'}], + ecg: [{type: ObjectId, ref: 'Ecg'}], + diagnose: {type: ObjectId, ref: 'Diagnose'}, + conclusion: {type: ObjectId, ref: 'Conclusion'} })); var collectionsNames = Object.keys(load); @@ -421,13 +442,15 @@ var len = collectionsNames.length; var creatList = []; for (i = 0; i < len; i++) { cn = collectionsNames[i]; - creatList.push({list:load[cn], collection: mongoose.model(cn).collection.name, model: cn}); + creatList.push({list: load[cn], collection: mongoose.model(cn).collection.name, model: cn}); } -var db = mongoose.createConnection('localhost', 'HeavyLoad', function(err) { +var db = mongoose.createConnection('localhost', 'HeavyLoad', function() { function getItems(list) { - if (!list) done(); + if (!list) { + done(); + } return function() { var cnt = Math.floor(Math.random() * dicCnt); var i, res = []; @@ -447,161 +470,176 @@ var db = mongoose.createConnection('localhost', 'HeavyLoad', function(err) { var run = []; run.push({ - name:'findAll lean=false', - qry:mc.find({}) - .populate('medication') - .populate('prescription') - .populate('observation') - .populate('heart') - .populate('pereferialPulse') - .populate('treatment') - .populate('vitalSign') - .populate('fluidsProgram') - .populate('allergy') - .populate('head') - .populate('sckeletal') - .populate('pulse') - .populate('supplier') - .populate('gastrointestinal') - .populate('ecg') - .populate('diagnose') - .populate('conclusion')}); + name: 'findAll lean=false', + qry: mc.find({}) + .populate('medication') + .populate('prescription') + .populate('observation') + .populate('heart') + .populate('pereferialPulse') + .populate('treatment') + .populate('vitalSign') + .populate('fluidsProgram') + .populate('allergy') + .populate('head') + .populate('sckeletal') + .populate('pulse') + .populate('supplier') + .populate('gastrointestinal') + .populate('ecg') + .populate('diagnose') + .populate('conclusion') + }); run.push({ - name:'findOne lean=false', - qry:mc.findOne({}) - .populate('medication') - .populate('prescription') - .populate('observation') - .populate('heart') - .populate('pereferialPulse') - .populate('treatment') - .populate('vitalSign') - .populate('fluidsProgram') - .populate('allergy') - .populate('head') - .populate('sckeletal') - .populate('pulse') - .populate('supplier') - .populate('gastrointestinal') - .populate('ecg') - .populate('diagnose') - .populate('conclusion')}); + name: 'findOne lean=false', + qry: mc.findOne({}) + .populate('medication') + .populate('prescription') + .populate('observation') + .populate('heart') + .populate('pereferialPulse') + .populate('treatment') + .populate('vitalSign') + .populate('fluidsProgram') + .populate('allergy') + .populate('head') + .populate('sckeletal') + .populate('pulse') + .populate('supplier') + .populate('gastrointestinal') + .populate('ecg') + .populate('diagnose') + .populate('conclusion') + }); run.push({ - name:'findAll lean=true', - qry:mc.find({}) - .lean() - .populate('medication') - .populate('prescription') - .populate('observation') - .populate('heart') - .populate('pereferialPulse') - .populate('treatment') - .populate('vitalSign') - .populate('fluidsProgram') - .populate('allergy') - .populate('head') - .populate('sckeletal') - .populate('pulse') - .populate('supplier') - .populate('gastrointestinal') - .populate('ecg') - .populate('diagnose') - .populate('conclusion')}); + name: 'findAll lean=true', + qry: mc.find({}) + .lean() + .populate('medication') + .populate('prescription') + .populate('observation') + .populate('heart') + .populate('pereferialPulse') + .populate('treatment') + .populate('vitalSign') + .populate('fluidsProgram') + .populate('allergy') + .populate('head') + .populate('sckeletal') + .populate('pulse') + .populate('supplier') + .populate('gastrointestinal') + .populate('ecg') + .populate('diagnose') + .populate('conclusion') + }); run.push({ - name:'findOne lean=true', - qry:mc.findOne({}) - .lean() - .populate('medication') - .populate('prescription') - .populate('observation') - .populate('heart') - .populate('pereferialPulse') - .populate('treatment') - .populate('vitalSign') - .populate('fluidsProgram') - .populate('allergy') - .populate('head') - .populate('sckeletal') - .populate('pulse') - .populate('supplier') - .populate('gastrointestinal') - .populate('ecg') - .populate('diagnose') - .populate('conclusion')}); + name: 'findOne lean=true', + qry: mc.findOne({}) + .lean() + .populate('medication') + .populate('prescription') + .populate('observation') + .populate('heart') + .populate('pereferialPulse') + .populate('treatment') + .populate('vitalSign') + .populate('fluidsProgram') + .populate('allergy') + .populate('head') + .populate('sckeletal') + .populate('pulse') + .populate('supplier') + .populate('gastrointestinal') + .populate('ecg') + .populate('diagnose') + .populate('conclusion') + }); var gcnt = run.length; var qryName; var turn = turns; var nextQuery = function(err) { - - if (err) done(); - else if (--gcnt >= 0) { + if (err) { + done(); + } else if (--gcnt >= 0) { qryName = run[gcnt].name; - console.log('query ',gcnt); + console.log('query ', gcnt); runResults[qryName] = {}; turn = turns; nextRun(); - } else done(); + } else { + done(); + } }; - var nextRun = function(err, data) { - if (err) return nextQuery(err); + var nextRun = function(err) { + if (err) { + return nextQuery(err); + } if (turn < turns) { var res = runResults[qryName][turn]; res.finish = new Date(); console.log(res.finish - res.start); } if (--turn >= 0) { - console.log('turn ',turn); - runResults[qryName][turn] = {start:new Date()}; + console.log('turn ', turn); + runResults[qryName][turn] = {start: new Date()}; run[gcnt].qry.exec(nextRun); - } else nextQuery(); + } else { + nextQuery(); + } }; - console.log("start benchmark"); + console.log('start benchmark'); nextQuery(); }; - var createRelations = function(err) { + var createRelations = function() { var coll = db.db.collection('medicalcards'); console.log('Main Collection prepare'); coll.remove({}, function() { console.log('clean collection done'); var loadMedicalCard = tbd.from({}) - .prop('firstName').use(function() { return createRandomWord(10);}).done() - .prop('lastName').use(function() { return createRandomWord(10);}).done() - .prop('medication').use(getItems(load.Medication)).done() - .prop('prescription').use(getItems(load.Prescription)).done() - .prop('observation').use(getItems(load.Observation)).done() - .prop('heart').use(getItems(load.Heart)).done() - .prop('pereferialPulse').use(getItems(load.PereferialPulse)).done() - .prop('treatment').use(getItems(load.Treatment)).done() - .prop('vitalSign').use(getItems(load.VitalSign)).done() - .prop('fluidsProgram').use(getItems(load.FluidsProgram)).done() - .prop('allergy').use(getItems(load.Allergy)).done() - .prop('head').use(getItems(load.Head)).done() - .prop('sckeletal').use(getItems(load.Sckeletal)).done() - .prop('pulse').use(getItems(load.Pulse)).done() - .prop('supplier').use(getItems(load.Supplier)).done() - .prop('gastrointestinal').use(getItems(load.Gastrointestinal)).done() - .prop('ecg').use(getItems(load.Ecg)).done() - .prop('diagnose').use(getItem(load.Diagnose)).done() - .prop('conclusion').use(getItem(load.Conclusion)).done() - .make(mainCnt); - - var saveAll = function(err, data) { - if (err) done(); - else if (--res === 0) { - console.log('save done'); - doBenchmark(); - } + .prop('firstName').use(function() { + return createRandomWord(10); + }).done() + .prop('lastName').use(function() { + return createRandomWord(10); + }).done() + .prop('medication').use(getItems(load.Medication)).done() + .prop('prescription').use(getItems(load.Prescription)).done() + .prop('observation').use(getItems(load.Observation)).done() + .prop('heart').use(getItems(load.Heart)).done() + .prop('pereferialPulse').use(getItems(load.PereferialPulse)).done() + .prop('treatment').use(getItems(load.Treatment)).done() + .prop('vitalSign').use(getItems(load.VitalSign)).done() + .prop('fluidsProgram').use(getItems(load.FluidsProgram)).done() + .prop('allergy').use(getItems(load.Allergy)).done() + .prop('head').use(getItems(load.Head)).done() + .prop('sckeletal').use(getItems(load.Sckeletal)).done() + .prop('pulse').use(getItems(load.Pulse)).done() + .prop('supplier').use(getItems(load.Supplier)).done() + .prop('gastrointestinal').use(getItems(load.Gastrointestinal)).done() + .prop('ecg').use(getItems(load.Ecg)).done() + .prop('diagnose').use(getItem(load.Diagnose)).done() + .prop('conclusion').use(getItem(load.Conclusion)).done() + .make(mainCnt); + + var saveAll = function(err) { + if (err) { + done(); + } else if (--res === 0) { + console.log('save done'); + doBenchmark(); + } }; var res = loadMedicalCard.length; var mc = db.model('MedicalCard'); - var i, rec; + var i; var len = loadMedicalCard.length; for (i = 0; i < len; i++) { new mc(loadMedicalCard[i]).save(saveAll); @@ -642,7 +680,8 @@ var db = mongoose.createConnection('localhost', 'HeavyLoad', function(err) { 'findOne lean=true': 34, 'findAll lean=true': 241.2, 'findOne lean=false': 84, - 'findAll lean=false': 325 }); + 'findAll lean=false': 325 + }); console.log(); console.log('actual results', finalResults); @@ -661,18 +700,22 @@ var db = mongoose.createConnection('localhost', 'HeavyLoad', function(err) { items.push.apply(items, data); } - if (err) return done(err); + if (err) { + return done(err); + } if (--cnt >= 0) { var item = creatList[cnt]; var coll = db.db.collection(item.collection); console.log(item.model); coll.remove({}, function() { - coll.save(item.list, function(err, data) { + coll.save(item.list, function() { var mdl = db.model(item.model); - mdl.find({},next); + mdl.find({}, next); }); }); - } else createRelations(err); + } else { + createRelations(err); + } }; console.log('Dictionaries:'); next(); diff --git a/bin/mongoose.debug.js b/bin/mongoose.debug.js index c7d0253b309..0db5f07b756 100644 --- a/bin/mongoose.debug.js +++ b/bin/mongoose.debug.js @@ -263,7 +263,7 @@ var cast = module.exports = function(schema, obj) { throw new Error("Must have a string or function for $where"); } - if ('function' === type) { + if (type === 'function') { obj[path] = val.toString(); } @@ -379,7 +379,7 @@ var cast = module.exports = function(schema, obj) { } else if (val === null || val === undefined) { continue; - } else if ('Object' === val.constructor.name) { + } else if (val.constructor.name === 'Object') { any$conditionals = Object.keys(val).some(function (k) { return k.charAt(0) === '$' && k !== '$id' && k !== '$ref'; @@ -577,7 +577,7 @@ Document.prototype.$__buildDoc = function (obj, fields, skipId) { // determine if this doc is a result of a query with // excluded fields - if (fields && 'Object' === utils.getFunctionName(fields.constructor)) { + if (fields && utils.getFunctionName(fields.constructor) === 'Object') { keys = Object.keys(fields); ki = keys.length; @@ -895,7 +895,7 @@ Document.prototype.set = function (path, val, type, options) { ) { this.set(path[key], prefix + key, constructing); } else if (strict) { - if ('real' === pathtype || 'virtual' === pathtype) { + if ('real' === pathtype || pathtype === 'virtual') { this.set(prefix + key, path[key], constructing); } else if ('throw' == strict) { throw new Error("Field `" + key + "` is not in schema."); @@ -1088,7 +1088,7 @@ Document.prototype.$__set = function ( if (last) { obj[parts[i]] = val; } else { - if (obj[parts[i]] && 'Object' === utils.getFunctionName(obj[parts[i]].constructor)) { + if (obj[parts[i]] && utils.getFunctionName(obj[parts[i]].constructor) === 'Object') { obj = obj[parts[i]]; } else if (obj[parts[i]] && obj[parts[i]] instanceof Embedded) { obj = obj[parts[i]]; @@ -1658,7 +1658,7 @@ function compile (tree, proto, prefix) { limb = tree[key]; define(key - , (('Object' === utils.getFunctionName(limb.constructor) + , ((utils.getFunctionName(limb.constructor === 'Object') && Object.keys(limb).length) && (!limb.type || limb.type.type) ? limb @@ -4204,7 +4204,7 @@ function SchemaArray (key, cast, options) { if (cast) { var castOptions = {}; - if ('Object' === utils.getFunctionName(cast.constructor)) { + if (utils.getFunctionName(cast.constructor) === 'Object') { if (cast.type) { // support { type: Woot } castOptions = utils.clone(cast); // do not alter user arguments @@ -4307,7 +4307,7 @@ SchemaArray.prototype.cast = function (value, doc, init) { for (var i = 0, l = indexes.length; i < l; ++i) { var pathIndex = indexes[i][0][this.path]; - if ('2dsphere' === pathIndex || '2d' === pathIndex) { + if ('2dsphere' === pathIndex || pathIndex === '2d') { return; } } @@ -4602,9 +4602,9 @@ SchemaBoolean.prototype.checkRequired = function (value) { SchemaBoolean.prototype.cast = function (value) { if (null === value) return value; - if ('0' === value) return false; - if ('true' === value) return true; - if ('false' === value) return false; + if (value === '0') return false; + if (value === 'true') return true; + if (value === 'false') return false; return !! value; } @@ -6647,7 +6647,7 @@ SchemaType.prototype.get = function (fn) { */ SchemaType.prototype.validate = function (obj, message, type) { - if ('function' == typeof obj || obj && 'RegExp' === utils.getFunctionName(obj.constructor)) { + if ('function' == typeof obj || obj && utils.getFunctionName(obj.constructor) === 'RegExp') { var properties; if (message instanceof Object && !type) { properties = utils.clone(message); @@ -6670,7 +6670,7 @@ SchemaType.prototype.validate = function (obj, message, type) { for (i=0, length=arguments.length; i 0 && + val[0] instanceof Document && + val[0].constructor.modelName && + schema.options.type[0].ref === val[0].constructor.modelName) { + if (this.ownerDocument) { + popOpts = { model: val[0].constructor }; + this.ownerDocument().populated(this.$__fullPath(path), + val.map(function(v) { return v._id; }), popOpts); + } else { + popOpts = { model: val[0].constructor }; + this.populated(path, val.map(function(v) { return v._id; }), popOpts); + } didPopulate = true; } val = schema.applySetters(val, this, false, priorVal); @@ -1071,15 +1222,15 @@ Document.prototype.set = function(path, val, type, options) { this.$markValid(path); } catch (e) { - var reason; - if (!(e instanceof MongooseError.CastError)) { - reason = e; - } this.invalidate(path, - new MongooseError.CastError(schema.instance, val, path, reason)); + new MongooseError.CastError(schema.instance, val, path, e)); shouldSet = false; } + if (schema.$isSingleNested) { + cleanModifiedSubpaths(this, path); + } + if (shouldSet) { this.$__set(pathToMark, path, constructing, parts, schema, val, priorVal); } @@ -1087,6 +1238,20 @@ Document.prototype.set = function(path, val, type, options) { return this; }; +/*! + * ignore + */ + +function cleanModifiedSubpaths(doc, path) { + var _modifiedPaths = Object.keys(doc.$__.activePaths.states.modify); + var _numModifiedPaths = _modifiedPaths.length; + for (var j = 0; j < _numModifiedPaths; ++j) { + if (_modifiedPaths[j].indexOf(path + '.') === 0) { + delete doc.$__.activePaths.states.modify[_modifiedPaths[j]]; + } + } +} + /** * Determine if we should mark this change as modified. * @@ -1096,10 +1261,10 @@ Document.prototype.set = function(path, val, type, options) { * @memberOf Document */ -Document.prototype.$__shouldModify = function( - pathToMark, path, constructing, parts, schema, val, priorVal) { - - if (this.isNew) return true; +Document.prototype.$__shouldModify = function(pathToMark, path, constructing, parts, schema, val, priorVal) { + if (this.isNew) { + return true; + } if (undefined === val && !this.isSelected(path)) { // when a path is not selected in a query, its initial @@ -1112,12 +1277,21 @@ Document.prototype.$__shouldModify = function( return false; } + // gh-3992: if setting a populated field to a doc, don't mark modified + // if they have the same _id + if (this.populated(path) && + val instanceof Document && + deepEqual(val._id, priorVal)) { + return false; + } + if (!deepEqual(val, priorVal || this.get(path))) { return true; } if (!constructing && - null != val && + val !== null && + val !== undefined && path in this.$__.activePaths.states.default && deepEqual(val, schema.getDefault(this, constructing))) { // a path with a default was $unset on the server @@ -1135,11 +1309,11 @@ Document.prototype.$__shouldModify = function( * @memberOf Document */ -Document.prototype.$__set = function( - pathToMark, path, constructing, parts, schema, val, priorVal) { +Document.prototype.$__set = function(pathToMark, path, constructing, parts, schema, val, priorVal) { Embedded = Embedded || require('./types/embedded'); - var shouldModify = this.$__shouldModify.apply(this, arguments); + var shouldModify = this.$__shouldModify(pathToMark, path, constructing, parts, + schema, val, priorVal); var _this = this; if (shouldModify) { @@ -1160,25 +1334,30 @@ Document.prototype.$__set = function( } } - var obj = this._doc, - i = 0, - l = parts.length; + var obj = this._doc; + var i = 0; + var l = parts.length; + var cur = ''; for (; i < l; i++) { - var next = i + 1, - last = next === l; + var next = i + 1; + var last = next === l; + cur += (cur ? '.' + parts[i] : parts[i]); if (last) { obj[parts[i]] = val; } else { - if (obj[parts[i]] && 'Object' === utils.getFunctionName(obj[parts[i]].constructor)) { + if (obj[parts[i]] && utils.getFunctionName(obj[parts[i]].constructor) === 'Object') { obj = obj[parts[i]]; } else if (obj[parts[i]] && obj[parts[i]] instanceof Embedded) { obj = obj[parts[i]]; + } else if (obj[parts[i]] && obj[parts[i]].$isSingleNested) { + obj = obj[parts[i]]; } else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) { obj = obj[parts[i]]; } else { - obj = obj[parts[i]] = {}; + this.set(cur, {}); + obj = obj[parts[i]]; } } } @@ -1220,7 +1399,7 @@ Document.prototype.setValue = function(path, val) { * doc.get('age', String) // "47" * * @param {String} path - * @param {Schema|String|Number|Buffer|etc..} [type] optionally specify a type for on-the-fly attributes + * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for on-the-fly attributes * @api public */ @@ -1235,9 +1414,9 @@ Document.prototype.get = function(path, type) { obj = this._doc; for (var i = 0, l = pieces.length; i < l; i++) { - obj = undefined === obj || null === obj - ? undefined - : obj[pieces[i]]; + obj = obj === null || obj === void 0 + ? undefined + : obj[pieces[i]]; } if (adhoc) { @@ -1268,9 +1447,8 @@ Document.prototype.$__path = function(path) { if (adhocType) { return adhocType; - } else { - return this.schema.path(path); } + return this.schema.path(path); }; /** @@ -1292,6 +1470,40 @@ Document.prototype.markModified = function(path) { this.$__.activePaths.modify(path); }; +/** + * Clears the modified state on the specified path. + * + * ####Example: + * + * doc.foo = 'bar'; + * doc.unmarkModified('foo'); + * doc.save() // changes to foo will not be persisted + * + * @param {String} path the path to unmark modified + * @api public + */ + +Document.prototype.unmarkModified = function(path) { + this.$__.activePaths.init(path); +}; + +/** + * Don't run validation on this path or persist changes to this path. + * + * ####Example: + * + * doc.foo = null; + * doc.$ignore('foo'); + * doc.save() // changes to foo will not be persisted and validators won't be run + * + * @param {String} path the path to ignore + * @api public + */ + +Document.prototype.$ignore = function(path) { + this.$__.activePaths.ignore(path); +}; + /** * Returns the list of paths that have been modified. * @@ -1301,12 +1513,13 @@ Document.prototype.markModified = function(path) { Document.prototype.modifiedPaths = function() { var directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); - return directModifiedPaths.reduce(function(list, path) { var parts = path.split('.'); return list.concat(parts.reduce(function(chains, part, i) { return chains.concat(parts.slice(0, i).concat(part).join('.')); - }, [])); + }, []).filter(function(chain) { + return (list.indexOf(chain) === -1); + })); }, []); }; @@ -1318,20 +1531,34 @@ Document.prototype.modifiedPaths = function() { * ####Example * * doc.set('documents.0.title', 'changed'); - * doc.isModified() // true - * doc.isModified('documents') // true - * doc.isModified('documents.0.title') // true - * doc.isDirectModified('documents') // false + * doc.isModified() // true + * doc.isModified('documents') // true + * doc.isModified('documents.0.title') // true + * doc.isModified('documents otherProp') // true + * doc.isDirectModified('documents') // false * * @param {String} [path] optional * @return {Boolean} * @api public */ -Document.prototype.isModified = function(path) { - return path - ? !!~this.modifiedPaths().indexOf(path) - : this.$__.activePaths.some('modify'); +Document.prototype.isModified = function(paths) { + if (paths) { + if (!Array.isArray(paths)) { + paths = paths.split(' '); + } + var modified = this.modifiedPaths(); + var directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); + var isModifiedChild = paths.some(function(path) { + return !!~modified.indexOf(path); + }); + return isModifiedChild || paths.some(function(path) { + return directModifiedPaths.some(function(mod) { + return mod === path || path.indexOf(mod + '.') === 0; + }); + }); + } + return this.$__.activePaths.some('modify'); }; /** @@ -1400,9 +1627,8 @@ Document.prototype.isInit = function(path) { Document.prototype.isSelected = function isSelected(path) { if (this.$__.selected) { - - if ('_id' === path) { - return 0 !== this.$__.selected._id; + if (path === '_id') { + return this.$__.selected._id !== 0; } var paths = Object.keys(this.$__.selected), @@ -1410,14 +1636,16 @@ Document.prototype.isSelected = function isSelected(path) { inclusive = false, cur; - if (1 === i && '_id' === paths[0]) { + if (i === 1 && paths[0] === '_id') { // only _id was selected. - return 0 === this.$__.selected._id; + return this.$__.selected._id === 0; } while (i--) { cur = paths[i]; - if ('_id' == cur) continue; + if (cur === '_id') { + continue; + } inclusive = !!this.$__.selected[cur]; break; } @@ -1431,13 +1659,15 @@ Document.prototype.isSelected = function isSelected(path) { while (i--) { cur = paths[i]; - if ('_id' == cur) continue; + if (cur === '_id') { + continue; + } - if (0 === cur.indexOf(pathDot)) { + if (cur.indexOf(pathDot) === 0) { return inclusive; } - if (0 === pathDot.indexOf(cur + '.')) { + if (pathDot.indexOf(cur + '.') === 0) { return inclusive; } } @@ -1463,71 +1693,99 @@ Document.prototype.isSelected = function isSelected(path) { * }); * * @param {Object} optional options internal options - * @param {Function} optional callback called after validation completes, passing an error if one occurred + * @param {Function} callback optional callback called after validation completes, passing an error if one occurred * @return {Promise} Promise * @api public */ Document.prototype.validate = function(options, callback) { - var _this = this; - var Promise = PromiseProvider.get(); if (typeof options === 'function') { callback = options; options = null; } - if (options && options.__noPromise) { - this.$__validate(callback); - return; + this.$__validate(callback); +}; + +/*! + * ignore + */ + +function _getPathsToValidate(doc) { + // only validate required fields when necessary + var paths = Object.keys(doc.$__.activePaths.states.require).filter(function(path) { + if (!doc.isSelected(path) && !doc.isModified(path)) { + return false; + } + var p = doc.schema.path(path); + if (typeof p.originalRequiredValue === 'function') { + return p.originalRequiredValue.call(doc); + } + return true; + }); + + paths = paths.concat(Object.keys(doc.$__.activePaths.states.init)); + paths = paths.concat(Object.keys(doc.$__.activePaths.states.modify)); + paths = paths.concat(Object.keys(doc.$__.activePaths.states.default)); + + // gh-661: if a whole array is modified, make sure to run validation on all + // the children as well + for (var i = 0; i < paths.length; ++i) { + var path = paths[i]; + var val = doc.getValue(path); + if (val && val.isMongooseArray && !Buffer.isBuffer(val) && !val.isMongooseDocumentArray) { + var numElements = val.length; + for (var j = 0; j < numElements; ++j) { + paths.push(path + '.' + j); + } + } } - return new Promise.ES6(function(resolve, reject) { - _this.$__validate(function(error) { - callback && callback(error); - if (error) { - reject(error); - return; + var flattenOptions = { skipArrays: true }; + for (i = 0; i < paths.length; ++i) { + var pathToCheck = paths[i]; + if (doc.schema.nested[pathToCheck]) { + var _v = doc.getValue(pathToCheck); + if (isMongooseObject(_v)) { + _v = _v.toObject({ transform: false }); } - resolve(); - }); - }); -}; + var flat = flatten(_v, '', flattenOptions); + var _subpaths = Object.keys(flat).map(function(p) { + return pathToCheck + '.' + p; + }); + paths = paths.concat(_subpaths); + } + } + + return paths; +} /*! * ignore */ Document.prototype.$__validate = function(callback) { - var self = this; + var _this = this; var _complete = function() { - var err = self.$__.validationError; - self.$__.validationError = undefined; - self.emit('validate', self); + var err = _this.$__.validationError; + _this.$__.validationError = undefined; + _this.emit('validate', _this); if (err) { for (var key in err.errors) { // Make sure cast errors persist - if (!self.__parent && err.errors[key] instanceof MongooseError.CastError) { - self.invalidate(key, err.errors[key]); + if (!_this.__parent && err.errors[key] instanceof MongooseError.CastError) { + _this.invalidate(key, err.errors[key]); } } return err; - } else { - return; } }; // only validate required fields when necessary - var paths = Object.keys(this.$__.activePaths.states.require).filter(function(path) { - if (!self.isSelected(path) && !self.isModified(path)) return false; - return true; - }); + var paths = _getPathsToValidate(this); - paths = paths.concat(Object.keys(this.$__.activePaths.states.init)); - paths = paths.concat(Object.keys(this.$__.activePaths.states.modify)); - paths = paths.concat(Object.keys(this.$__.activePaths.states.default)); - - if (0 === paths.length) { + if (paths.length === 0) { process.nextTick(function() { var err = _complete(); if (err) { @@ -1541,20 +1799,6 @@ Document.prototype.$__validate = function(callback) { var validating = {}, total = 0; - // gh-661: if a whole array is modified, make sure to run validation on all - // the children as well - for (var i = 0; i < paths.length; ++i) { - var path = paths[i]; - var val = self.getValue(path); - if (val && val.isMongooseArray && !Buffer.isBuffer(val) && - !val.isMongooseDocumentArray) { - var numElements = val.length; - for (var j = 0; j < numElements; ++j) { - paths.push(path + '.' + j); - } - } - } - var complete = function() { var err = _complete(); if (err) { @@ -1565,30 +1809,32 @@ Document.prototype.$__validate = function(callback) { }; var validatePath = function(path) { - if (validating[path]) return; + if (validating[path]) { + return; + } validating[path] = true; total++; process.nextTick(function() { - var p = self.schema.path(path); + var p = _this.schema.path(path); if (!p) { return --total || complete(); } // If user marked as invalid or there was a cast error, don't validate - if (!self.$isValid(path)) { + if (!_this.$isValid(path)) { --total || complete(); return; } - var val = self.getValue(path); + var val = _this.getValue(path); p.doValidate(val, function(err) { if (err) { - self.invalidate(path, err, undefined, true); + _this.invalidate(path, err, undefined, true); } --total || complete(); - }, self); + }, _this); }); }; @@ -1617,21 +1863,14 @@ Document.prototype.$__validate = function(callback) { */ Document.prototype.validateSync = function(pathsToValidate) { - var self = this; + var _this = this; if (typeof pathsToValidate === 'string') { pathsToValidate = pathsToValidate.split(' '); } // only validate required fields when necessary - var paths = Object.keys(this.$__.activePaths.states.require).filter(function(path) { - if (!self.isSelected(path) && !self.isModified(path)) return false; - return true; - }); - - paths = paths.concat(Object.keys(this.$__.activePaths.states.init)); - paths = paths.concat(Object.keys(this.$__.activePaths.states.modify)); - paths = paths.concat(Object.keys(this.$__.activePaths.states.default)); + var paths = _getPathsToValidate(this); if (pathsToValidate && pathsToValidate.length) { var tmp = []; @@ -1646,32 +1885,36 @@ Document.prototype.validateSync = function(pathsToValidate) { var validating = {}; paths.forEach(function(path) { - if (validating[path]) return; + if (validating[path]) { + return; + } validating[path] = true; - var p = self.schema.path(path); - if (!p) return; - if (!self.$isValid(path)) { + var p = _this.schema.path(path); + if (!p) { + return; + } + if (!_this.$isValid(path)) { return; } - var val = self.getValue(path); - var err = p.doValidateSync(val, self); + var val = _this.getValue(path); + var err = p.doValidateSync(val, _this); if (err) { - self.invalidate(path, err, undefined, true); + _this.invalidate(path, err, undefined, true); } }); - var err = self.$__.validationError; - self.$__.validationError = undefined; - self.emit('validate', self); + var err = _this.$__.validationError; + _this.$__.validationError = undefined; + _this.emit('validate', _this); if (err) { for (var key in err.errors) { // Make sure cast errors persist if (err.errors[key] instanceof MongooseError.CastError) { - self.invalidate(key, err.errors[key]); + _this.invalidate(key, err.errors[key]); } } } @@ -1705,28 +1948,35 @@ Document.prototype.validateSync = function(pathsToValidate) { * @param {String} path the field to invalidate * @param {String|Error} errorMsg the error which states the reason `path` was invalid * @param {Object|String|Number|any} value optional invalid value + * @param {String} [kind] optional `kind` property for the error + * @return {ValidationError} the current ValidationError, with all currently invalidated paths * @api public */ -Document.prototype.invalidate = function(path, err, val) { +Document.prototype.invalidate = function(path, err, val, kind) { if (!this.$__.validationError) { this.$__.validationError = new ValidationError(this); } - if (this.$__.validationError.errors[path]) return; - - if (!err || 'string' === typeof err) { + if (this.$__.validationError.errors[path]) { + return; + } + + if (!err || typeof err === 'string') { err = new ValidatorError({ path: path, message: err, - type: 'user defined', + type: kind || 'user defined', value: val }); } - if (this.$__.validationError == err) return; + if (this.$__.validationError === err) { + return this.$__.validationError; + } this.$__.validationError.errors[path] = err; + return this.$__.validationError; }; /** @@ -1772,12 +2022,12 @@ Document.prototype.$isValid = function(path) { */ Document.prototype.$__reset = function reset() { - var self = this; + var _this = this; DocumentArray || (DocumentArray = require('./types/documentarray')); this.$__.activePaths .map('init', 'modify', function(i) { - return self.getValue(i); + return _this.getValue(i); }) .filter(function(val) { return val && val instanceof Array && val.isMongooseDocumentArray && val.length; @@ -1786,7 +2036,9 @@ Document.prototype.$__reset = function reset() { var i = array.length; while (i--) { var doc = array[i]; - if (!doc) continue; + if (!doc) { + continue; + } doc.$__reset(); } }); @@ -1804,9 +2056,9 @@ Document.prototype.$__reset = function reset() { this.$__.activePaths.clear('default'); this.$__.validationError = undefined; this.errors = undefined; - self = this; + _this = this; this.schema.requiredPaths().forEach(function(path) { - self.$__.activePaths.require(path); + _this.$__.activePaths.require(path); }); return this; @@ -1821,26 +2073,26 @@ Document.prototype.$__reset = function reset() { */ Document.prototype.$__dirty = function() { - var self = this; + var _this = this; var all = this.$__.activePaths.map('modify', function(path) { return { path: path, - value: self.getValue(path), - schema: self.$__path(path) + value: _this.getValue(path), + schema: _this.$__path(path) }; }); // gh-2558: if we had to set a default and the value is not undefined, // we have to save as well all = all.concat(this.$__.activePaths.map('default', function(path) { - if (path === '_id' || !self.getValue(path)) { + if (path === '_id' || !_this.getValue(path)) { return; } return { path: path, - value: self.getValue(path), - schema: self.$__path(path) + value: _this.getValue(path), + schema: _this.$__path(path) }; })); @@ -1854,7 +2106,7 @@ Document.prototype.$__dirty = function() { lastPath, top; - all.forEach(function(item, i) { + all.forEach(function(item) { if (!item) { return; } @@ -1892,12 +2144,12 @@ function compile(tree, proto, prefix, options) { key = keys[i]; limb = tree[key]; - defineKey(key - , (('Object' === utils.getFunctionName(limb.constructor) - && Object.keys(limb).length) - && (!limb[options.typeKey] || (options.typeKey === 'type' && limb.type.type)) - ? limb - : null) + defineKey(key, + ((utils.getFunctionName(limb.constructor) === 'Object' + && Object.keys(limb).length) + && (!limb[options.typeKey] || (options.typeKey === 'type' && limb.type.type)) + ? limb + : null) , proto , prefix , keys @@ -1927,28 +2179,30 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) { prefix = prefix || ''; if (subprops) { - Object.defineProperty(prototype, prop, { enumerable: true, configurable: true, get: function() { - var _self = this; - if (!this.$__.getters) + var _this = this; + if (!this.$__.getters) { this.$__.getters = {}; + } if (!this.$__.getters[path]) { var nested = Object.create(Object.getPrototypeOf(this), getOwnPropertyDescriptors(this)); - // save scope for nested getters/setters - if (!prefix) nested.$__.scope = this; + // save scope for nested getters/setters + if (!prefix) { + nested.$__.scope = this; + } - // shadow inherited getters from sub-objects so - // thing.nested.nested.nested... doesn't occur (gh-366) + // shadow inherited getters from sub-objects so + // thing.nested.nested.nested... doesn't occur (gh-366) var i = 0, len = keys.length; for (; i < len; ++i) { - // over-write the parents getter without triggering it + // over-write the parents getter without triggering it Object.defineProperty(nested, keys[i], { enumerable: false, // It doesn't show up. writable: true, // We can set it later. @@ -1957,13 +2211,30 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) { }); } - nested.toObject = function() { - return _self.get(path); - }; + Object.defineProperty(nested, 'toObject', { + enumerable: true, + configurable: true, + writable: false, + value: function() { + return _this.get(path); + } + }); - nested.toJSON = nested.toObject; + Object.defineProperty(nested, 'toJSON', { + enumerable: true, + configurable: true, + writable: false, + value: function() { + return _this.get(path); + } + }); - nested.$__isNested = true; + Object.defineProperty(nested, '$__isNested', { + enumerable: true, + configurable: true, + writable: false, + value: true + }); compile(subprops, nested, path, options); this.$__.getters[path] = nested; @@ -1972,17 +2243,22 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) { return this.$__.getters[path]; }, set: function(v) { - if (v instanceof Document) v = v.toObject(); + if (v instanceof Document) { + v = v.toObject({ transform: false }); + } return (this.$__.scope || this).set(path, v); } }); - } else { Object.defineProperty(prototype, prop, { enumerable: true, configurable: true, - get: function() { return this.get.call(this.$__.scope || this, path); }, - set: function(v) { return this.set.call(this.$__.scope || this, path, v); } + get: function() { + return this.get.call(this.$__.scope || this, path); + }, + set: function(v) { + return this.set.call(this.$__.scope || this, path, v); + } }); } } @@ -2015,15 +2291,17 @@ Document.prototype.$__getArrayPathsToValidate = function() { // validate all document arrays. return this.$__.activePaths - .map('init', 'modify', function(i) { - return this.getValue(i); - }.bind(this)) - .filter(function(val) { - return val && val instanceof Array && val.isMongooseDocumentArray && val.length; - }).reduce(function(seed, array) { - return seed.concat(array); - }, []) - .filter(function(doc) { return doc;}); + .map('init', 'modify', function(i) { + return this.getValue(i); + }.bind(this)) + .filter(function(val) { + return val && val instanceof Array && val.isMongooseDocumentArray && val.length; + }).reduce(function(seed, array) { + return seed.concat(array); + }, []) + .filter(function(doc) { + return doc; + }); }; @@ -2042,14 +2320,21 @@ Document.prototype.$__getAllSubdocs = function() { function docReducer(seed, path) { var val = this[path]; - if (val instanceof Embedded) seed.push(val); + if (val instanceof Embedded) { + seed.push(val); + } if (val && val.$isSingleNested) { + seed = Object.keys(val._doc).reduce(docReducer.bind(val._doc), seed); seed.push(val); } if (val && val.isMongooseDocumentArray) { val.forEach(function _docReduce(doc) { - if (!doc || !doc._doc) return; - if (doc instanceof Embedded) seed.push(doc); + if (!doc || !doc._doc) { + return; + } + if (doc instanceof Embedded) { + seed.push(doc); + } seed = Object.keys(doc._doc).reduce(docReducer.bind(doc._doc), seed); }); } else if (val instanceof Document && val.$__isNested) { @@ -2078,19 +2363,23 @@ Document.prototype.$__registerHooksFromSchema = function() { Embedded = Embedded || require('./types/embedded'); var Promise = PromiseProvider.get(); - var self = this; - var q = self.schema && self.schema.callQueue; - if (!q.length) return self; + var _this = this; + var q = _this.schema && _this.schema.callQueue; + if (!q.length) { + return _this; + } // we are only interested in 'pre' hooks, and group by point-cut var toWrap = q.reduce(function(seed, pair) { if (pair[0] !== 'pre' && pair[0] !== 'post' && pair[0] !== 'on') { - self[pair[0]].apply(self, pair[1]); + _this[pair[0]].apply(_this, pair[1]); return seed; } var args = [].slice.call(pair[1]); var pointCut = pair[0] === 'on' ? 'post' : args[0]; - if (!(pointCut in seed)) seed[pointCut] = { post: [], pre: [] }; + if (!(pointCut in seed)) { + seed[pointCut] = {post: [], pre: []}; + } if (pair[0] === 'post') { seed[pointCut].post.push(args); } else if (pair[0] === 'on') { @@ -2103,20 +2392,20 @@ Document.prototype.$__registerHooksFromSchema = function() { // 'post' hooks are simpler toWrap.post.forEach(function(args) { - self.on.apply(self, args); + _this.on.apply(_this, args); }); delete toWrap.post; // 'init' should be synchronous on subdocuments - if (toWrap.init && self instanceof Embedded) { + if (toWrap.init && _this instanceof Embedded) { if (toWrap.init.pre) { toWrap.init.pre.forEach(function(args) { - self.pre.apply(self, args); + _this.$pre.apply(_this, args); }); } if (toWrap.init.post) { toWrap.init.post.forEach(function(args) { - self.post.apply(self, args); + _this.$post.apply(_this, args); }); } delete toWrap.init; @@ -2124,12 +2413,12 @@ Document.prototype.$__registerHooksFromSchema = function() { // Set hooks also need to be sync re: gh-3479 if (toWrap.set.pre) { toWrap.set.pre.forEach(function(args) { - self.pre.apply(self, args); + _this.$pre.apply(_this, args); }); } if (toWrap.set.post) { toWrap.set.post.forEach(function(args) { - self.post.apply(self, args); + _this.$post.apply(_this, args); }); } delete toWrap.set; @@ -2138,47 +2427,73 @@ Document.prototype.$__registerHooksFromSchema = function() { Object.keys(toWrap).forEach(function(pointCut) { // this is so we can wrap everything into a promise; var newName = ('$__original_' + pointCut); - if (!self[pointCut]) { + if (!_this[pointCut]) { return; } - self[newName] = self[pointCut]; - self[pointCut] = function wrappedPointCut() { + _this[newName] = _this[pointCut]; + _this[pointCut] = function wrappedPointCut() { var args = [].slice.call(arguments); var lastArg = args.pop(); var fn; + var originalStack = new Error().stack; + var $results; + if (lastArg && typeof lastArg !== 'function') { + args.push(lastArg); + } else { + fn = lastArg; + } - return new Promise.ES6(function(resolve, reject) { - if (lastArg && typeof lastArg !== 'function') { - args.push(lastArg); - } else { - fn = lastArg; - } - args.push(function(error, result) { + var promise = new Promise.ES6(function(resolve, reject) { + args.push(function(error) { if (error) { - self.$__handleReject(error); - fn && fn(error); + // gh-2633: since VersionError is very generic, take the + // stack trace of the original save() function call rather + // than the async trace + if (error instanceof VersionError) { + error.stack = originalStack; + } + _this.$__handleReject(error); reject(error); return; } - fn && fn.apply(null, [null].concat(Array.prototype.slice.call(arguments, 1))); - resolve(result); + // There may be multiple results and promise libs other than + // mpromise don't support passing multiple values to `resolve()` + $results = Array.prototype.slice.call(arguments, 1); + resolve.apply(promise, $results); }); - self[newName].apply(self, args); + _this[newName].apply(_this, args); }); + if (fn) { + if (_this.constructor.$wrapCallback) { + fn = _this.constructor.$wrapCallback(fn); + } + return promise.then( + function() { + process.nextTick(function() { + fn.apply(null, [null].concat($results)); + }); + }, + function(error) { + process.nextTick(function() { + fn(error); + }); + }); + } + return promise; }; toWrap[pointCut].pre.forEach(function(args) { args[0] = newName; - self.pre.apply(self, args); + _this.$pre.apply(_this, args); }); toWrap[pointCut].post.forEach(function(args) { args[0] = newName; - self.post.apply(self, args); + _this.$post.apply(_this, args); }); }); - return self; + return _this; }; Document.prototype.$__handleReject = function handleReject(err) { @@ -2201,7 +2516,7 @@ Document.prototype.$__handleReject = function handleReject(err) { */ Document.prototype.$toObject = function(options, json) { - var defaultOptions = { transform: true, json: json }; + var defaultOptions = {transform: true, json: json}; if (options && options.depopulate && !options._skipDepopulateTopLevel && this.$__.wasPopulated) { // populated paths that we set to a document @@ -2216,7 +2531,7 @@ Document.prototype.$toObject = function(options, json) { // When internally saving this document we always pass options, // bypassing the custom schema options. - if (!(options && 'Object' == utils.getFunctionName(options.constructor)) || + if (!(options && utils.getFunctionName(options.constructor) === 'Object') || (options && options._useSchemaOptions)) { if (json) { options = this.schema.options.toJSON ? @@ -2247,10 +2562,6 @@ Document.prototype.$toObject = function(options, json) { var ret = clone(this._doc, options) || {}; - if (options.virtuals || options.getters && false !== options.virtuals) { - applyGetters(this, ret, 'virtuals', options); - } - if (options.getters) { applyGetters(this, ret, 'paths', options); // applyGetters for paths will add nested empty objects; @@ -2260,6 +2571,10 @@ Document.prototype.$toObject = function(options, json) { } } + if (options.virtuals || options.getters && options.virtuals !== false) { + applyGetters(this, ret, 'virtuals', options); + } + if (options.versionKey === false && this.schema.options.versionKey) { delete ret[this.schema.options.versionKey]; } @@ -2271,9 +2586,8 @@ Document.prototype.$toObject = function(options, json) { // child schema has a transform (this.schema.options.toObject) In this case, // we need to adjust options.transform to be the child schema's transform and // not the parent schema's - if (true === transform || + if (transform === true || (this.schema.options.toObject && transform)) { - var opts = options.json ? this.schema.options.toJSON : this.schema.options.toObject; if (opts) { @@ -2283,9 +2597,11 @@ Document.prototype.$toObject = function(options, json) { options.transform = originalTransform; } - if ('function' == typeof transform) { + if (typeof transform === 'function') { var xformed = transform(this, ret, options); - if ('undefined' != typeof xformed) ret = xformed; + if (typeof xformed !== 'undefined') { + ret = xformed; + } } return ret; @@ -2343,6 +2659,7 @@ Document.prototype.$toObject = function(options, json) { * schema.options.toObject.transform = function (doc, ret, options) { * // remove the _id of every document before returning the result * delete ret._id; + * return ret; * } * * // without the transformation in the schema @@ -2385,6 +2702,7 @@ Document.prototype.$toObject = function(options, json) { * delete ret[prop]; * }); * } + * return ret; * } * * var doc = new Doc({ _id: 'anId', secret: 47, name: 'Wreck-it Ralph' }); @@ -2441,8 +2759,8 @@ function minimize(obj) { } return hasKeys - ? obj - : undefined; + ? obj + : undefined; } /*! @@ -2508,10 +2826,15 @@ Document.prototype.toJSON = function(options) { */ Document.prototype.inspect = function(options) { - var opts = options && 'Object' == utils.getFunctionName(options.constructor) ? options : {}; - opts.minimize = false; - opts.retainKeyOrder = true; - return inspect(this.toObject(opts)); + var isPOJO = options && + utils.getFunctionName(options.constructor) === 'Object'; + var opts; + if (isPOJO) { + opts = options; + opts.minimize = false; + opts.retainKeyOrder = true; + } + return this.toObject(opts); }; /** @@ -2521,7 +2844,9 @@ Document.prototype.inspect = function(options) { * @method toString */ -Document.prototype.toString = Document.prototype.inspect; +Document.prototype.toString = function() { + return inspect(this.inspect()); +}; /** * Returns true if the Document stores the same data as doc. @@ -2536,18 +2861,24 @@ Document.prototype.toString = Document.prototype.inspect; */ Document.prototype.equals = function(doc) { + if (!doc) { + return false; + } + var tid = this.get('_id'); var docid = doc.get ? doc.get('_id') : doc; if (!tid && !docid) { return deepEqual(this, doc); } return tid && tid.equals - ? tid.equals(docid) - : tid === docid; + ? tid.equals(docid) + : tid === docid; }; /** * Populates document references, executing the `callback` when complete. + * If you want to use promises instead, use this function with + * [`execPopulate()`](#document_Document-execPopulate) * * ####Example: * @@ -2560,15 +2891,16 @@ Document.prototype.equals = function(doc) { * model: 'modelName' * options: opts * }, function (err, user) { - * assert(doc._id == user._id) // the document itself is passed + * assert(doc._id === user._id) // the document itself is passed * }) * * // summary - * doc.populate(path) // not executed - * doc.populate(options); // not executed - * doc.populate(path, callback) // executed - * doc.populate(options, callback); // executed - * doc.populate(callback); // executed + * doc.populate(path) // not executed + * doc.populate(options); // not executed + * doc.populate(path, callback) // executed + * doc.populate(options, callback); // executed + * doc.populate(callback); // executed + * doc.populate(options).execPopulate() // executed, returns promise * * * ####NOTE: @@ -2579,6 +2911,7 @@ Document.prototype.equals = function(doc) { * See [Model.populate()](#model_Model.populate) for explaination of options. * * @see Model.populate #model_Model.populate + * @see Document.execPopulate #document_Document-execPopulate * @param {String|Object} [path] The path to populate or an options object * @param {Function} [callback] When passed, population is invoked * @api public @@ -2586,13 +2919,15 @@ Document.prototype.equals = function(doc) { */ Document.prototype.populate = function populate() { - if (0 === arguments.length) return this; + if (arguments.length === 0) { + return this; + } var pop = this.$__.populate || (this.$__.populate = {}); var args = utils.args(arguments); var fn; - if ('function' == typeof args[args.length - 1]) { + if (typeof args[args.length - 1] === 'function') { fn = args.pop(); } @@ -2608,6 +2943,7 @@ Document.prototype.populate = function populate() { if (fn) { var paths = utils.object.vals(pop); this.$__.populate = undefined; + paths.__noPromise = true; this.constructor.populate(this, paths, fn); } @@ -2615,7 +2951,7 @@ Document.prototype.populate = function populate() { }; /** - * Explicitly executes population and returns a promise. Useful for ES6 + * Explicitly executes population and returns a promise. Useful for ES2015 * integration. * * ####Example: @@ -2632,16 +2968,10 @@ Document.prototype.populate = function populate() { * execPopulate(); * * // summary - * doc.execPopulate() - * - * - * ####NOTE: + * doc.execPopulate().then(resolve, reject); * - * Population does not occur unless a `callback` is passed. - * Passing the same path a second time will overwrite the previous path options. - * See [Model.populate()](#model_Model.populate) for explaination of options. * - * @see Document.populate #Document_model.populate + * @see Document.populate #document_Document-populate * @api public * @return {Promise} promise that resolves to the document when population is done */ @@ -2650,11 +2980,12 @@ Document.prototype.execPopulate = function() { var Promise = PromiseProvider.get(); var _this = this; return new Promise.ES6(function(resolve, reject) { - _this.populate(function(error) { + _this.populate(function(error, res) { if (error) { - return reject(error); + reject(error); + } else { + resolve(res); } - resolve(_this); }); }); }; @@ -2679,22 +3010,28 @@ Document.prototype.execPopulate = function() { Document.prototype.populated = function(path, val, options) { // val and options are internal - if (val == null) { - if (!this.$__.populated) return undefined; + if (val === null || val === void 0) { + if (!this.$__.populated) { + return undefined; + } var v = this.$__.populated[path]; - if (v) return v.value; + if (v) { + return v.value; + } return undefined; } // internal - if (true === val) { - if (!this.$__.populated) return undefined; + if (val === true) { + if (!this.$__.populated) { + return undefined; + } return this.$__.populated[path]; } this.$__.populated || (this.$__.populated = {}); - this.$__.populated[path] = { value: val, options: options }; + this.$__.populated[path] = {value: val, options: options}; return val; }; @@ -2712,6 +3049,7 @@ Document.prototype.populated = function(path, val, options) { * If the path was not populated, this is a no-op. * * @param {String} path + * @see Document.populate #document_Document-populate * @api public */ @@ -2747,10 +3085,12 @@ Document.prototype.$__fullPath = function(path) { Document.ValidationError = ValidationError; module.exports = exports = Document; -}).call(this,require("FWaASH"),require("buffer").Buffer) -},{"./error":12,"./internal":21,"./promise_provider":23,"./schema":24,"./schema/mixed":32,"./schematype":36,"./types/array":38,"./types/documentarray":40,"./types/embedded":41,"./utils":45,"FWaASH":55,"buffer":49,"events":53,"hooks-fixed":76,"util":57}],6:[function(require,module,exports){ +}).call(this,require("g5I+bs"),require("buffer").Buffer) +},{"./error":12,"./error/objectExpected":17,"./error/strict":19,"./internal":23,"./promise_provider":25,"./schema":26,"./schema/mixed":34,"./schematype":41,"./services/common":42,"./types/array":44,"./types/documentarray":46,"./types/embedded":47,"./utils":51,"buffer":84,"events":87,"g5I+bs":133,"hooks-fixed":88,"util":138}],6:[function(require,module,exports){ 'use strict'; +/* eslint-env browser */ + /*! * Module dependencies. */ @@ -2765,10 +3105,10 @@ var BrowserDocument = require('./browserDocument.js'); module.exports = function() { if (typeof window !== 'undefined' && typeof document !== 'undefined' && document === window.document) { return BrowserDocument; - } else { - return Document; } + return Document; }; + },{"./browserDocument.js":3,"./document.js":5}],7:[function(require,module,exports){ /*! * ignore @@ -2790,7 +3130,7 @@ var Binary = require('bson').Binary; module.exports = exports = Binary; -},{"bson":60}],9:[function(require,module,exports){ +},{"bson":66}],9:[function(require,module,exports){ /*! * Module exports. */ @@ -2815,7 +3155,7 @@ var ObjectId = require('bson').ObjectID; module.exports = exports = ObjectId; -},{"bson":60}],11:[function(require,module,exports){ +},{"bson":66}],11:[function(require,module,exports){ (function (global){ /*! * ignore @@ -2847,7 +3187,11 @@ module.exports = driver; function MongooseError(msg) { Error.call(this); - this.stack = new Error().stack; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } this.message = msg; this.name = 'MongooseError'; } @@ -2889,12 +3233,13 @@ MongooseError.OverwriteModelError = require('./error/overwriteModel'); MongooseError.MissingSchemaError = require('./error/missingSchema'); MongooseError.DivergentArrayError = require('./error/divergentArray'); -},{"./error/cast":13,"./error/divergentArray":14,"./error/messages":15,"./error/missingSchema":16,"./error/overwriteModel":17,"./error/validation":18,"./error/validator":19,"./error/version":20}],13:[function(require,module,exports){ +},{"./error/cast":13,"./error/divergentArray":14,"./error/messages":15,"./error/missingSchema":16,"./error/overwriteModel":18,"./error/validation":20,"./error/validator":21,"./error/version":22}],13:[function(require,module,exports){ /*! * Module dependencies. */ var MongooseError = require('../error.js'); +var util = require('util'); /** * Casting Error constructor. @@ -2906,8 +3251,18 @@ var MongooseError = require('../error.js'); */ function CastError(type, value, path, reason) { - MongooseError.call(this, 'Cast to ' + type + ' failed for value "' + value + '" at path "' + path + '"'); - this.stack = new Error().stack; + var stringValue = util.inspect(value); + stringValue = stringValue.replace(/^'/, '"').replace(/'$/, '"'); + if (stringValue.charAt(0) !== '"') { + stringValue = '"' + stringValue + '"'; + } + MongooseError.call(this, 'Cast to ' + type + ' failed for value ' + + stringValue + ' at path "' + path + '"'); + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } this.name = 'CastError'; this.kind = type; this.value = value; @@ -2929,7 +3284,7 @@ CastError.prototype.constructor = MongooseError; module.exports = CastError; -},{"../error.js":12}],14:[function(require,module,exports){ +},{"../error.js":12,"util":138}],14:[function(require,module,exports){ /*! * Module dependencies. @@ -2992,7 +3347,7 @@ module.exports = DivergentArrayError; * * Click the "show code" link below to see all defaults. * - * @property messages + * @static messages * @receiver MongooseError * @api public */ @@ -3000,23 +3355,22 @@ module.exports = DivergentArrayError; var msg = module.exports = exports = {}; msg.general = {}; -msg.general.default = "Validator failed for path `{PATH}` with value `{VALUE}`"; -msg.general.required = "Path `{PATH}` is required."; +msg.general.default = 'Validator failed for path `{PATH}` with value `{VALUE}`'; +msg.general.required = 'Path `{PATH}` is required.'; msg.Number = {}; -msg.Number.min = "Path `{PATH}` ({VALUE}) is less than minimum allowed value ({MIN})."; -msg.Number.max = "Path `{PATH}` ({VALUE}) is more than maximum allowed value ({MAX})."; +msg.Number.min = 'Path `{PATH}` ({VALUE}) is less than minimum allowed value ({MIN}).'; +msg.Number.max = 'Path `{PATH}` ({VALUE}) is more than maximum allowed value ({MAX}).'; msg.Date = {}; -msg.Date.min = "Path `{PATH}` ({VALUE}) is before minimum allowed value ({MIN})."; -msg.Date.max = "Path `{PATH}` ({VALUE}) is after maximum allowed value ({MAX})."; +msg.Date.min = 'Path `{PATH}` ({VALUE}) is before minimum allowed value ({MIN}).'; +msg.Date.max = 'Path `{PATH}` ({VALUE}) is after maximum allowed value ({MAX}).'; msg.String = {}; -msg.String.enum = "`{VALUE}` is not a valid enum value for path `{PATH}`."; -msg.String.match = "Path `{PATH}` is invalid ({VALUE})."; -msg.String.minlength = "Path `{PATH}` (`{VALUE}`) is shorter than the minimum allowed length ({MINLENGTH})."; -msg.String.maxlength = "Path `{PATH}` (`{VALUE}`) is longer than the maximum allowed length ({MAXLENGTH})."; - +msg.String.enum = '`{VALUE}` is not a valid enum value for path `{PATH}`.'; +msg.String.match = 'Path `{PATH}` is invalid ({VALUE}).'; +msg.String.minlength = 'Path `{PATH}` (`{VALUE}`) is shorter than the minimum allowed length ({MINLENGTH}).'; +msg.String.maxlength = 'Path `{PATH}` (`{VALUE}`) is longer than the maximum allowed length ({MAXLENGTH}).'; },{}],16:[function(require,module,exports){ @@ -3054,6 +3408,43 @@ MissingSchemaError.prototype.constructor = MongooseError; module.exports = MissingSchemaError; },{"../error.js":12}],17:[function(require,module,exports){ +/*! + * Module dependencies. + */ + +var MongooseError = require('../error.js'); + +/** + * Strict mode error constructor + * + * @param {String} type + * @param {String} value + * @inherits MongooseError + * @api private + */ + +function ObjectExpectedError(path, val) { + MongooseError.call(this, 'Tried to set nested object field `' + path + + '` to primitive value `' + val + '` and strict mode is set to throw.'); + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } + this.name = 'ObjectExpectedError'; + this.path = path; +} + +/*! + * Inherits from MongooseError. + */ + +ObjectExpectedError.prototype = Object.create(MongooseError.prototype); +ObjectExpectedError.prototype.constructor = MongooseError; + +module.exports = ObjectExpectedError; + +},{"../error.js":12}],18:[function(require,module,exports){ /*! * Module dependencies. @@ -3086,8 +3477,44 @@ OverwriteModelError.prototype.constructor = MongooseError; module.exports = OverwriteModelError; -},{"../error.js":12}],18:[function(require,module,exports){ +},{"../error.js":12}],19:[function(require,module,exports){ +/*! + * Module dependencies. + */ + +var MongooseError = require('../error.js'); + +/** + * Strict mode error constructor + * + * @param {String} type + * @param {String} value + * @inherits MongooseError + * @api private + */ + +function StrictModeError(path) { + MongooseError.call(this, 'Field `' + path + '` is not in schema and strict ' + + 'mode is set to throw.'); + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } + this.name = 'StrictModeError'; + this.path = path; +} + +/*! + * Inherits from MongooseError. + */ +StrictModeError.prototype = Object.create(MongooseError.prototype); +StrictModeError.prototype.constructor = MongooseError; + +module.exports = StrictModeError; + +},{"../error.js":12}],20:[function(require,module,exports){ /*! * Module requirements */ @@ -3104,11 +3531,15 @@ var MongooseError = require('../error.js'); function ValidationError(instance) { if (instance && instance.constructor.name === 'model') { - MongooseError.call(this, instance.constructor.modelName + " validation failed"); + MongooseError.call(this, instance.constructor.modelName + ' validation failed'); + } else { + MongooseError.call(this, 'Validation failed'); + } + if (Error.captureStackTrace) { + Error.captureStackTrace(this); } else { - MongooseError.call(this, "Validation failed"); + this.stack = new Error().stack; } - this.stack = new Error().stack; this.name = 'ValidationError'; this.errors = {}; if (instance) { @@ -3133,7 +3564,9 @@ ValidationError.prototype.toString = function() { var msgs = []; Object.keys(this.errors).forEach(function(key) { - if (this == this.errors[key]) return; + if (this === this.errors[key]) { + return; + } msgs.push(String(this.errors[key])); }, this); @@ -3146,13 +3579,12 @@ ValidationError.prototype.toString = function() { module.exports = exports = ValidationError; -},{"../error.js":12}],19:[function(require,module,exports){ +},{"../error.js":12}],21:[function(require,module,exports){ /*! * Module dependencies. */ var MongooseError = require('../error.js'); -var errorMessages = MongooseError.messages; /** * Schema validator error @@ -3165,13 +3597,17 @@ var errorMessages = MongooseError.messages; function ValidatorError(properties) { var msg = properties.message; if (!msg) { - msg = errorMessages.general.default; + msg = MongooseError.messages.general.default; } - this.properties = properties; var message = this.formatMessage(msg, properties); MongooseError.call(this, message); - this.stack = new Error().stack; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } + this.properties = properties; this.name = 'ValidatorError'; this.kind = properties.type; this.path = properties.path; @@ -3185,6 +3621,17 @@ function ValidatorError(properties) { ValidatorError.prototype = Object.create(MongooseError.prototype); ValidatorError.prototype.constructor = MongooseError; +/*! + * The object used to define this validator. Not enumerable to hide + * it from `require('util').inspect()` output re: gh-3925 + */ + +Object.defineProperty(ValidatorError.prototype, 'properties', { + enumerable: false, + writable: true, + value: null +}); + /*! * Formats error messages */ @@ -3215,7 +3662,8 @@ ValidatorError.prototype.toString = function() { module.exports = ValidatorError; -},{"../error.js":12}],20:[function(require,module,exports){ +},{"../error.js":12}],22:[function(require,module,exports){ +'use strict'; /*! * Module dependencies. @@ -3230,9 +3678,9 @@ var MongooseError = require('../error.js'); * @api private */ -function VersionError() { - MongooseError.call(this, 'No matching document found.'); - Error.captureStackTrace && Error.captureStackTrace(this, arguments.callee); +function VersionError(doc) { + MongooseError.call(this, 'No matching document found for id "' + doc._id + + '"'); this.name = 'VersionError'; } @@ -3249,7 +3697,7 @@ VersionError.prototype.constructor = MongooseError; module.exports = VersionError; -},{"../error.js":12}],21:[function(require,module,exports){ +},{"../error.js":12}],23:[function(require,module,exports){ /*! * Dependencies */ @@ -3282,7 +3730,7 @@ function InternalCache() { this.fullPath = undefined; } -},{"./statemachine":37}],22:[function(require,module,exports){ +},{"./statemachine":43}],24:[function(require,module,exports){ /*! * Module dependencies */ @@ -3351,6 +3799,29 @@ Promise.prototype = Object.create(MPromise.prototype, { } }); +/*! + * ignore + */ + +Promise.prototype.then = util.deprecate(Promise.prototype.then, + 'Mongoose: mpromise (mongoose\'s default promise library) is deprecated, ' + + 'plug in your own promise library instead: ' + + 'http://mongoosejs.com/docs/promises.html'); + +/** + * ES6-style `.catch()` shorthand + * + * @method catch + * @memberOf Promise + * @param {Function} onReject + * @return {Promise} + * @api public + */ + +Promise.prototype.catch = function(onReject) { + return this.then(null, onReject); +}; + /*! * Override event names for backward compatibility. */ @@ -3575,7 +4046,7 @@ Promise.prototype.addErrback = Promise.prototype.onReject; module.exports = Promise; -},{"mpromise":80,"util":57}],23:[function(require,module,exports){ +},{"mpromise":121,"util":138}],25:[function(require,module,exports){ /*! * Module dependencies. */ @@ -3628,7 +4099,7 @@ Promise.reset = function() { module.exports = Promise; -},{"./ES6Promise":1,"./promise":22,"mquery":85}],24:[function(require,module,exports){ +},{"./ES6Promise":1,"./promise":24,"mquery":126}],26:[function(require,module,exports){ (function (Buffer){ /*! * Module dependencies. @@ -3640,15 +4111,16 @@ var VirtualType = require('./virtualtype'); var utils = require('./utils'); var MongooseTypes; var Kareem = require('kareem'); -var async = require('async'); -var PromiseProvider = require('./promise_provider'); +var each = require('async/each'); +var SchemaType = require('./schematype'); -var IS_QUERY_HOOK = { +var IS_KAREEM_HOOK = { count: true, find: true, findOne: true, findOneAndUpdate: true, findOneAndRemove: true, + insertMany: true, update: true }; @@ -3681,26 +4153,31 @@ var IS_QUERY_HOOK = { * - [toJSON](/docs/guide.html#toJSON) - object - no default * - [toObject](/docs/guide.html#toObject) - object - no default * - [typeKey](/docs/guide.html#typeKey) - string - defaults to 'type' + * - [useNestedStrict](/docs/guide.html#useNestedStrict) - boolean - defaults to false * - [validateBeforeSave](/docs/guide.html#validateBeforeSave) - bool - defaults to `true` - * - [versionKey](/docs/guide.html#versionKey): bool - defaults to "__v" + * - [versionKey](/docs/guide.html#versionKey): string - defaults to "__v" * * ####Note: * * _When nesting schemas, (`children` in the example above), always declare the child schema first before passing it into its parent._ * * @param {Object} definition + * @param {Object} [options] * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter * @event `init`: Emitted after the schema is compiled into a `Model`. * @api public */ function Schema(obj, options) { - if (!(this instanceof Schema)) + if (!(this instanceof Schema)) { return new Schema(obj, options); + } + this.obj = obj; this.paths = {}; this.subpaths = {}; this.virtuals = {}; + this.singleNestedPaths = {}; this.nested = {}; this.inherits = {}; this.callQueue = []; @@ -3711,10 +4188,12 @@ function Schema(obj, options) { this._requiredpaths = undefined; this.discriminatorMapping = undefined; this._indexedpaths = undefined; + this.query = {}; + this.childSchemas = []; this.s = { hooks: new Kareem(), - queryHooks: IS_QUERY_HOOK + kareemHooks: IS_KAREEM_HOOK }; this.options = this.defaultOptions(options); @@ -3729,17 +4208,17 @@ function Schema(obj, options) { // ensure the documents get an auto _id unless disabled var auto_id = !this.paths['_id'] && - (!this.options.noId && this.options._id) && !_idSubDoc; + (!this.options.noId && this.options._id) && !_idSubDoc; if (auto_id) { - obj = { _id: { auto: true } }; + obj = {_id: {auto: true}}; obj._id[this.options.typeKey] = Schema.ObjectId; this.add(obj); } // ensure the documents receive an id getter unless disabled var autoid = !this.paths['id'] && - (!this.options.noVirtualId && this.options.id); + (!this.options.noVirtualId && this.options.id); if (autoid) { this.virtual('id').get(idGetter); } @@ -3749,53 +4228,9 @@ function Schema(obj, options) { this[m.kind](m.hook, !!m.isAsync, m.fn); } - // adds updatedAt and createdAt timestamps to documents if enabled - var timestamps = this.options.timestamps; - if (timestamps) { - var createdAt = timestamps.createdAt || 'createdAt', - updatedAt = timestamps.updatedAt || 'updatedAt', - schemaAdditions = {}; - - schemaAdditions[updatedAt] = Date; - - if (!this.paths[createdAt]) { - schemaAdditions[createdAt] = Date; - } - - this.add(schemaAdditions); - - this.pre('save', function(next) { - var defaultTimestamp = new Date(); - - if (!this[createdAt]) { - this[createdAt] = auto_id ? this._id.getTimestamp() : defaultTimestamp; - } - - this[updatedAt] = this.isNew ? this[createdAt] : defaultTimestamp; - - next(); - }); - - var genUpdates = function() { - var now = new Date(); - var updates = {$set: {}, $setOnInsert: {}}; - updates.$set[updatedAt] = now; - updates.$setOnInsert[createdAt] = now; - - return updates; - }; - - this.pre('findOneAndUpdate', function(next) { - this.findOneAndUpdate({}, genUpdates()); - next(); - }); - - this.pre('update', function(next) { - this.update({}, genUpdates()); - next(); - }); + if (this.options.timestamps) { + this.setupTimestamp(this.options.timestamps); } - } /*! @@ -3807,16 +4242,18 @@ function idGetter() { return this.$__._id; } - return this.$__._id = null == this._id - ? null - : String(this._id); + this.$__._id = this._id == null + ? null + : String(this._id); + return this.$__._id; } /*! * Inherit from EventEmitter. */ -Schema.prototype = Object.create( EventEmitter.prototype ); +Schema.prototype = Object.create(EventEmitter.prototype); Schema.prototype.constructor = Schema; +Schema.prototype.instanceOfSchema = true; /** * Default middleware attached to a schema. Cannot be changed. @@ -3832,59 +4269,65 @@ Object.defineProperty(Schema.prototype, '_defaultMiddleware', { configurable: false, enumerable: false, writable: false, - value: [{ - kind: 'pre', - hook: 'save', - fn: function(next, options) { - // Nested docs have their own presave - if (this.ownerDocument) { - return next(); - } - - var hasValidateBeforeSaveOption = options && - (typeof options === 'object') && - ('validateBeforeSave' in options); + value: [ + { + kind: 'pre', + hook: 'save', + fn: function(next, options) { + var _this = this; + // Nested docs have their own presave + if (this.ownerDocument) { + return next(); + } - var shouldValidate; - if (hasValidateBeforeSaveOption) { - shouldValidate = !!options.validateBeforeSave; - } else { - shouldValidate = this.schema.options.validateBeforeSave; - } + var hasValidateBeforeSaveOption = options && + (typeof options === 'object') && + ('validateBeforeSave' in options); - // Validate - if (shouldValidate) { - // HACK: use $__original_validate to avoid promises so bluebird doesn't - // complain - if (this.$__original_validate) { - this.$__original_validate({ __noPromise: true }, function(error) { - next(error); - }); + var shouldValidate; + if (hasValidateBeforeSaveOption) { + shouldValidate = !!options.validateBeforeSave; } else { - this.validate({ __noPromise: true }, function(error) { - next(error); - }); + shouldValidate = this.schema.options.validateBeforeSave; } - } else { - next(); - } - } - }, { - kind: 'pre', - hook: 'save', - isAsync: true, - fn: function(next, done) { - var Promise = PromiseProvider.get(), - subdocs = this.$__getAllSubdocs(); - if (!subdocs.length || this.$__preSavingFromParent) { - done(); - next(); - return; + // Validate + if (shouldValidate) { + // HACK: use $__original_validate to avoid promises so bluebird doesn't + // complain + if (this.$__original_validate) { + this.$__original_validate({__noPromise: true}, function(error) { + return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + next(error); + }); + }); + } else { + this.validate({__noPromise: true}, function(error) { + return _this.schema.s.hooks.execPost('save:error', _this, [ _this], { error: error }, function(error) { + next(error); + }); + }); + } + } else { + next(); + } } + }, + { + kind: 'pre', + hook: 'save', + isAsync: true, + fn: function(next, done) { + var _this = this; + var subdocs = this.$__getAllSubdocs(); + + if (!subdocs.length || this.$__preSavingFromParent) { + done(); + next(); + return; + } - new Promise.ES6(function(resolve, reject) { - async.each(subdocs, function(subdoc, cb) { + each(subdocs, function(subdoc, cb) { subdoc.$__preSavingFromParent = true; subdoc.save(function(err) { cb(err); @@ -3894,19 +4337,76 @@ Object.defineProperty(Schema.prototype, '_defaultMiddleware', { delete subdocs[i].$__preSavingFromParent; } if (error) { - reject(error); - return; + return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + done(error); + }); } - resolve(); + next(); + done(); }); - }).then(function() { + } + }, + { + kind: 'pre', + hook: 'validate', + isAsync: true, + fn: function(next, done) { + // Hack to ensure that we always wrap validate() in a promise next(); done(); - }, done); + } + }, + { + kind: 'pre', + hook: 'remove', + isAsync: true, + fn: function(next, done) { + if (this.ownerDocument) { + done(); + next(); + return; + } + + var subdocs = this.$__getAllSubdocs(); + + if (!subdocs.length || this.$__preSavingFromParent) { + done(); + next(); + return; + } + + each(subdocs, function(subdoc, cb) { + subdoc.remove({ noop: true }, function(err) { + cb(err); + }); + }, function(error) { + if (error) { + done(error); + return; + } + next(); + done(); + }); + } } - }] + ] }); + +/** + * The original object passed to the schema constructor + * + * ####Example: + * + * var schema = new Schema({ a: String }).add({ b: String }); + * schema.obj; // { a: String } + * + * @api public + * @property obj + */ + +Schema.prototype.obj; + /** * Schema as flat paths * @@ -3948,11 +4448,11 @@ Schema.prototype.tree; */ Schema.prototype.defaultOptions = function(options) { - if (options && false === options.safe) { - options.safe = { w: 0 }; + if (options && options.safe === false) { + options.safe = {w: 0}; } - if (options && options.safe && 0 === options.safe.w) { + if (options && options.safe && options.safe.w === 0) { // if you turn off safe writes, then versioning goes off as well options.versionKey = false; } @@ -4003,25 +4503,31 @@ Schema.prototype.add = function add(obj, prefix) { for (var i = 0; i < keys.length; ++i) { var key = keys[i]; - if (null == obj[key]) { + if (obj[key] == null) { throw new TypeError('Invalid value for schema path `' + prefix + key + '`'); } - if (Array.isArray(obj[key]) && obj[key].length === 1 && null == obj[key][0]) { + if (Array.isArray(obj[key]) && obj[key].length === 1 && obj[key][0] == null) { throw new TypeError('Invalid value for schema Array path `' + prefix + key + '`'); } if (utils.isObject(obj[key]) && - (!obj[key].constructor || 'Object' == utils.getFunctionName(obj[key].constructor)) && + (!obj[key].constructor || utils.getFunctionName(obj[key].constructor) === 'Object') && (!obj[key][this.options.typeKey] || (this.options.typeKey === 'type' && obj[key].type.type))) { if (Object.keys(obj[key]).length) { // nested object { last: { name: String }} this.nested[prefix + key] = true; this.add(obj[key], prefix + key + '.'); } else { + if (prefix) { + this.nested[prefix.substr(0, prefix.length - 1)] = true; + } this.path(prefix + key, obj[key]); // mixed type } } else { + if (prefix) { + this.nested[prefix.substr(0, prefix.length - 1)] = true; + } this.path(prefix + key, obj[key]); } } @@ -4046,6 +4552,8 @@ var reserved = Schema.reserved; reserved.emit = reserved.on = reserved.once = +reserved.listeners = +reserved.removeListener = // document properties and functions reserved.collection = reserved.db = @@ -4063,13 +4571,13 @@ reserved.validate = // hooks.js reserved._pres = reserved._posts = 1; -/** +/*! * Document keys to print warnings for */ var warnings = {}; warnings.increment = '`increment` should not be used as a schema path name ' + - 'unless you have disabled versioning.'; + 'unless you have disabled versioning.'; /** * Gets/sets schema paths. @@ -4088,19 +4596,26 @@ warnings.increment = '`increment` should not be used as a schema path name ' + */ Schema.prototype.path = function(path, obj) { - if (obj == undefined) { - if (this.paths[path]) return this.paths[path]; - if (this.subpaths[path]) return this.subpaths[path]; + if (obj === undefined) { + if (this.paths[path]) { + return this.paths[path]; + } + if (this.subpaths[path]) { + return this.subpaths[path]; + } + if (this.singleNestedPaths[path]) { + return this.singleNestedPaths[path]; + } // subpaths? return /\.\d+\.?.*$/.test(path) - ? getPositionalPath(this, path) - : undefined; + ? getPositionalPath(this, path) + : undefined; } // some path names conflict with document methods if (reserved[path]) { - throw new Error("`" + path + "` may not be used as a schema pathname"); + throw new Error('`' + path + '` may not be used as a schema pathname'); } if (warnings[path]) { @@ -4113,13 +4628,15 @@ Schema.prototype.path = function(path, obj) { branch = this.tree; subpaths.forEach(function(sub, i) { - if (!branch[sub]) branch[sub] = {}; - if ('object' != typeof branch[sub]) { + if (!branch[sub]) { + branch[sub] = {}; + } + if (typeof branch[sub] !== 'object') { var msg = 'Cannot set nested path `' + path + '`. ' - + 'Parent path `' - + subpaths.slice(0, i).concat([sub]).join('.') - + '` already set to type ' + branch[sub].name - + '.'; + + 'Parent path `' + + subpaths.slice(0, i).concat([sub]).join('.') + + '` already set to type ' + branch[sub].name + + '.'; throw new Error(msg); } branch = branch[sub]; @@ -4128,6 +4645,21 @@ Schema.prototype.path = function(path, obj) { branch[last] = utils.clone(obj); this.paths[path] = Schema.interpretAsType(path, obj, this.options); + + if (this.paths[path].$isSingleNested) { + for (var key in this.paths[path].schema.paths) { + this.singleNestedPaths[path + '.' + key] = + this.paths[path].schema.paths[key]; + } + for (key in this.paths[path].schema.singleNestedPaths) { + this.singleNestedPaths[path + '.' + key] = + this.paths[path].schema.singleNestedPaths[key]; + } + + this.childSchemas.push(this.paths[path].schema); + } else if (this.paths[path].$isMongooseDocumentArray) { + this.childSchemas.push(this.paths[path].schema); + } return this; }; @@ -4142,7 +4674,7 @@ Schema.prototype.path = function(path, obj) { Schema.interpretAsType = function(path, obj, options) { if (obj.constructor) { var constructorName = utils.getFunctionName(obj.constructor); - if (constructorName != 'Object') { + if (constructorName !== 'Object') { var oldObj = obj; obj = {}; obj[options.typeKey] = oldObj; @@ -4153,36 +4685,67 @@ Schema.interpretAsType = function(path, obj, options) { // and default to mixed if not specified. // { type: { type: String, default: 'freshcut' } } var type = obj[options.typeKey] && (options.typeKey !== 'type' || !obj.type.type) - ? obj[options.typeKey] - : {}; + ? obj[options.typeKey] + : {}; - if ('Object' == utils.getFunctionName(type.constructor) || 'mixed' == type) { + if (utils.getFunctionName(type.constructor) === 'Object' || type === 'mixed') { return new MongooseTypes.Mixed(path, obj); } - if (Array.isArray(type) || Array == type || 'array' == type) { + if (Array.isArray(type) || Array === type || type === 'array') { // if it was specified through { type } look for `cast` - var cast = (Array == type || 'array' == type) - ? obj.cast - : type[0]; + var cast = (Array === type || type === 'array') + ? obj.cast + : type[0]; - if (cast instanceof Schema) { + if (cast && cast.instanceOfSchema) { return new MongooseTypes.DocumentArray(path, cast, obj); } - if ('string' == typeof cast) { + if (Array.isArray(cast)) { + return new MongooseTypes.Array(path, Schema.interpretAsType(path, cast, options), obj); + } + + if (typeof cast === 'string') { cast = MongooseTypes[cast.charAt(0).toUpperCase() + cast.substring(1)]; } else if (cast && (!cast[options.typeKey] || (options.typeKey === 'type' && cast.type.type)) - && 'Object' == utils.getFunctionName(cast.constructor) - && Object.keys(cast).length) { - var opts = { minimize: options.minimize }; - return new MongooseTypes.DocumentArray(path, new Schema(cast, opts), obj); + && utils.getFunctionName(cast.constructor) === 'Object') { + if (Object.keys(cast).length) { + // The `minimize` and `typeKey` options propagate to child schemas + // declared inline, like `{ arr: [{ val: { $type: String } }] }`. + // See gh-3560 + var childSchemaOptions = {minimize: options.minimize}; + if (options.typeKey) { + childSchemaOptions.typeKey = options.typeKey; + } + var childSchema = new Schema(cast, childSchemaOptions); + childSchema.$implicitlyCreated = true; + return new MongooseTypes.DocumentArray(path, childSchema, obj); + } else { + // Special case: empty object becomes mixed + return new MongooseTypes.Array(path, MongooseTypes.Mixed, obj); + } + } + + if (cast) { + type = cast[options.typeKey] && (options.typeKey !== 'type' || !cast.type.type) + ? cast[options.typeKey] + : cast; + + name = typeof type === 'string' + ? type + : type.schemaName || utils.getFunctionName(type); + + if (!(name in MongooseTypes)) { + throw new TypeError('Undefined type `' + name + '` at array `' + path + + '`'); + } } - return new MongooseTypes.Array(path, cast || MongooseTypes.Mixed, obj); + return new MongooseTypes.Array(path, cast || MongooseTypes.Mixed, obj, options); } - if (type instanceof Schema) { + if (type && type.instanceOfSchema) { return new MongooseTypes.Embedded(type, path, obj); } @@ -4190,11 +4753,11 @@ Schema.interpretAsType = function(path, obj, options) { if (Buffer.isBuffer(type)) { name = 'Buffer'; } else { - name = 'string' == typeof type - ? type + name = typeof type === 'string' + ? type // If not string, `type` is a function. Outside of IE, function.name // gives you the function name. In IE, you need to compute it - : type.schemaName || utils.getFunctionName(type); + : type.schemaName || utils.getFunctionName(type); } if (name) { @@ -4240,7 +4803,9 @@ Schema.prototype.eachPath = function(fn) { */ Schema.prototype.requiredPaths = function requiredPaths(invalidate) { - if (this._requiredpaths && !invalidate) return this._requiredpaths; + if (this._requiredpaths && !invalidate) { + return this._requiredpaths; + } var paths = Object.keys(this.paths), i = paths.length, @@ -4248,10 +4813,12 @@ Schema.prototype.requiredPaths = function requiredPaths(invalidate) { while (i--) { var path = paths[i]; - if (this.paths[path].isRequired) ret.push(path); + if (this.paths[path].isRequired) { + ret.push(path); + } } - - return this._requiredpaths = ret; + this._requiredpaths = ret; + return this._requiredpaths; }; /** @@ -4262,9 +4829,11 @@ Schema.prototype.requiredPaths = function requiredPaths(invalidate) { */ Schema.prototype.indexedPaths = function indexedPaths() { - if (this._indexedpaths) return this._indexedpaths; - - return this._indexedpaths = this.indexes(); + if (this._indexedpaths) { + return this._indexedpaths; + } + this._indexedpaths = this.indexes(); + return this._indexedpaths; }; /** @@ -4278,16 +4847,26 @@ Schema.prototype.indexedPaths = function indexedPaths() { */ Schema.prototype.pathType = function(path) { - if (path in this.paths) return 'real'; - if (path in this.virtuals) return 'virtual'; - if (path in this.nested) return 'nested'; - if (path in this.subpaths) return 'real'; + if (path in this.paths) { + return 'real'; + } + if (path in this.virtuals) { + return 'virtual'; + } + if (path in this.nested) { + return 'nested'; + } + if (path in this.subpaths) { + return 'real'; + } + if (path in this.singleNestedPaths) { + return 'real'; + } if (/\.\d+\.|\.\d+$/.test(path)) { return getPositionalPathType(this, path); - } else { - return 'adhocOrUndefined'; } + return 'adhocOrUndefined'; }; /** @@ -4312,6 +4891,137 @@ Schema.prototype.hasMixedParent = function(path) { return false; }; +/** + * Setup updatedAt and createdAt timestamps to documents if enabled + * + * @param {Boolean|Object} timestamps timestamps options + * @api private + */ +Schema.prototype.setupTimestamp = function(timestamps) { + if (timestamps) { + var createdAt = timestamps.createdAt || 'createdAt'; + var updatedAt = timestamps.updatedAt || 'updatedAt'; + var schemaAdditions = {}; + + schemaAdditions[updatedAt] = Date; + + if (!this.paths[createdAt]) { + schemaAdditions[createdAt] = Date; + } + + this.add(schemaAdditions); + + this.pre('save', function(next) { + var defaultTimestamp = new Date(); + var auto_id = this._id && this._id.auto; + + if (!this[createdAt] && this.isSelected(createdAt)) { + this[createdAt] = auto_id ? this._id.getTimestamp() : defaultTimestamp; + } + + if (this.isNew || this.isModified()) { + this[updatedAt] = this.isNew ? this[createdAt] : defaultTimestamp; + } + + next(); + }); + + var genUpdates = function() { + var now = new Date(); + var updates = { $set: {}, $setOnInsert: {} }; + updates.$set[updatedAt] = now; + updates.$setOnInsert[createdAt] = now; + + return updates; + }; + + this.methods.initializeTimestamps = function() { + if (!this[createdAt]) { + this[createdAt] = new Date(); + } + if (!this[updatedAt]) { + this[updatedAt] = new Date(); + } + return this; + }; + + this.pre('findOneAndUpdate', function(next) { + this.findOneAndUpdate({}, genUpdates()); + applyTimestampsToChildren(this); + next(); + }); + + this.pre('update', function(next) { + this.update({}, genUpdates()); + applyTimestampsToChildren(this); + next(); + }); + } +}; + +/*! + * ignore + */ + +function applyTimestampsToChildren(query) { + var now = new Date(); + var update = query.getUpdate(); + var keys = Object.keys(update); + var key; + var schema = query.model.schema; + var len; + var createdAt; + var updatedAt; + var timestamps; + var path; + + var hasDollarKey = keys.length && keys[0].charAt(0) === '$'; + + if (hasDollarKey) { + if (update.$push) { + for (key in update.$push) { + if (update.$push[key] && + schema.path(key).$isMongooseDocumentArray && + schema.path(key).schema.options.timestamps) { + timestamps = schema.path(key).schema.options.timestamps; + createdAt = timestamps.createdAt || 'createdAt'; + updatedAt = timestamps.updatedAt || 'updatedAt'; + update.$push[key][updatedAt] = now; + update.$push[key][createdAt] = now; + } + } + } + if (update.$set) { + for (key in update.$set) { + path = schema.path(key); + if (!path) { + continue; + } + if (Array.isArray(update.$set[key]) && path.$isMongooseDocumentArray) { + len = update.$set[key].length; + timestamps = schema.path(key).schema.options.timestamps; + if (timestamps) { + createdAt = timestamps.createdAt || 'createdAt'; + updatedAt = timestamps.updatedAt || 'updatedAt'; + for (var i = 0; i < len; ++i) { + update.$set[key][i][updatedAt] = now; + update.$set[key][i][createdAt] = now; + } + } + } else if (update.$set[key] && path.$isSingleNested) { + timestamps = schema.path(key).schema.options.timestamps; + if (timestamps) { + createdAt = timestamps.createdAt || 'createdAt'; + updatedAt = timestamps.updatedAt || 'updatedAt'; + update.$set[key][updatedAt] = now; + update.$set[key][createdAt] = now; + } + } + } + } + } +} + /*! * ignore */ @@ -4324,7 +5034,9 @@ function getPositionalPathType(self, path) { var val = self.path(subpaths[0]); var isNested = false; - if (!val) return val; + if (!val) { + return val; + } var last = subpaths.length - 1, subpath, @@ -4334,8 +5046,14 @@ function getPositionalPathType(self, path) { isNested = false; subpath = subpaths[i]; - if (i === last && val && !val.schema && !/\D/.test(subpath)) { - if (val instanceof MongooseTypes.Array) { + if (i === last && val && !/\D/.test(subpath)) { + if (val.$isMongooseDocumentArray) { + var oldVal = val; + val = new SchemaType(subpath); + val.cast = function(value, doc, init) { + return oldVal.cast(value, doc, init)[0]; + }; + } else if (val instanceof MongooseTypes.Array) { // StringSchema, NumberSchema, etc val = val.caster; } else { @@ -4345,7 +5063,9 @@ function getPositionalPathType(self, path) { } // ignore if its just a position segment: path.0.subpath - if (!/\D/.test(subpath)) continue; + if (!/\D/.test(subpath)) { + continue; + } if (!(val && val.schema)) { val = undefined; @@ -4403,7 +5123,7 @@ Schema.prototype.queue = function(name, args) { * }) * * toySchema.pre('validate', function (next) { - * if (this.name != 'Woody') this.name = 'Woody'; + * if (this.name !== 'Woody') this.name = 'Woody'; * next(); * }) * @@ -4415,7 +5135,7 @@ Schema.prototype.queue = function(name, args) { Schema.prototype.pre = function() { var name = arguments[0]; - if (IS_QUERY_HOOK[name]) { + if (IS_KAREEM_HOOK[name]) { this.s.hooks.pre.apply(this.s.hooks, arguments); return this; } @@ -4425,28 +5145,36 @@ Schema.prototype.pre = function() { /** * Defines a post hook for the document * - * Post hooks fire `on` the event emitted from document instances of Models compiled from this schema. - * * var schema = new Schema(..); * schema.post('save', function (doc) { * console.log('this fired after a document was saved'); * }); * + * shema.post('find', function(docs) { + * console.log('this fired after you run a find query'); + * }); + * * var Model = mongoose.model('Model', schema); * * var m = new Model(..); - * m.save(function (err) { + * m.save(function(err) { * console.log('this fires after the `post` hook'); * }); * + * m.find(function(err, docs) { + * console.log('this fires after the post find hook'); + * }); + * * @param {String} method name of the method to hook * @param {Function} fn callback - * @see hooks.js https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3 + * @see middleware http://mongoosejs.com/docs/middleware.html + * @see hooks.js https://www.npmjs.com/package/hooks-fixed + * @see kareem http://npmjs.org/package/kareem * @api public */ Schema.prototype.post = function(method, fn) { - if (IS_QUERY_HOOK[method]) { + if (IS_KAREEM_HOOK[method]) { this.s.hooks.post.apply(this.s.hooks, arguments); return this; } @@ -4457,13 +5185,18 @@ Schema.prototype.post = function(method, fn) { }]); } + if (fn.length === 3) { + this.s.hooks.post(method + ':error', fn); + return this; + } + return this.queue('post', [arguments[0], function(next) { // wrap original function so that the callback goes last, // for compatibility with old code that is using synchronous post hooks - var self = this; + var _this = this; var args = Array.prototype.slice.call(arguments, 1); fn.call(this, this, function(err) { - return next.apply(self, [err].concat(args)); + return next.apply(_this, [err].concat(args)); }); }]); }; @@ -4515,11 +5248,13 @@ Schema.prototype.plugin = function(fn, opts) { */ Schema.prototype.method = function(name, fn) { - if ('string' != typeof name) - for (var i in name) + if (typeof name !== 'string') { + for (var i in name) { this.methods[i] = name[i]; - else + } + } else { this.methods[name] = fn; + } return this; }; @@ -4540,17 +5275,19 @@ Schema.prototype.method = function(name, fn) { * * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as statics. * - * @param {String} name - * @param {Function} fn + * @param {String|Object} name + * @param {Function} [fn] * @api public */ Schema.prototype.static = function(name, fn) { - if ('string' != typeof name) - for (var i in name) + if (typeof name !== 'string') { + for (var i in name) { this.statics[i] = name[i]; - else + } + } else { this.statics[name] = fn; + } return this; }; @@ -4570,8 +5307,9 @@ Schema.prototype.static = function(name, fn) { Schema.prototype.index = function(fields, options) { options || (options = {}); - if (options.expires) + if (options.expires) { utils.expires(options); + } this._indexes.push([fields, options]); return this; @@ -4593,7 +5331,7 @@ Schema.prototype.index = function(fields, options) { */ Schema.prototype.set = function(key, value, _tags) { - if (1 === arguments.length) { + if (arguments.length === 1) { return this.options[key]; } @@ -4602,9 +5340,13 @@ Schema.prototype.set = function(key, value, _tags) { this.options[key] = readPref(value, _tags); break; case 'safe': - this.options[key] = false === value - ? { w: 0 } - : value; + this.options[key] = value === false + ? {w: 0} + : value; + break; + case 'timestamps': + this.setupTimestamp(value); + this.options[key] = value; break; default: this.options[key] = value; @@ -4635,8 +5377,12 @@ Schema.prototype.get = function(key) { var indexTypes = '2d 2dsphere hashed text'.split(' '); Object.defineProperty(Schema, 'indexTypes', { - get: function() { return indexTypes; }, - set: function() { throw new Error('Cannot overwrite Schema.indexTypes'); } + get: function() { + return indexTypes; + }, + set: function() { + throw new Error('Cannot overwrite Schema.indexTypes'); + } }); /** @@ -4665,21 +5411,24 @@ Schema.prototype.indexes = function() { key = keys[i]; path = schema.paths[key]; - if (path instanceof MongooseTypes.DocumentArray) { + if ((path instanceof MongooseTypes.DocumentArray) || path.$isSingleNested) { collectIndexes(path.schema, key + '.'); } else { index = path._index; - if (false !== index && null != index) { + if (index !== false && index !== null && index !== undefined) { field = {}; isObject = utils.isObject(index); options = isObject ? index : {}; - type = 'string' == typeof index ? index : - isObject ? index.type : - false; + type = typeof index === 'string' ? index : + isObject ? index.type : + false; if (type && ~Schema.indexTypes.indexOf(type)) { field[prefix + key] = type; + } else if (options.text) { + field[prefix + key] = 'text'; + delete options.text; } else { field[prefix + key] = 1; } @@ -4698,11 +5447,12 @@ Schema.prototype.indexes = function() { fixSubIndexPaths(schema, prefix); } else { schema._indexes.forEach(function(index) { - if (!('background' in index[1])) index[1].background = true; + if (!('background' in index[1])) { + index[1].background = true; + } }); indexes = indexes.concat(schema._indexes); } - }; collectIndexes(this); @@ -4752,14 +5502,65 @@ Schema.prototype.indexes = function() { */ Schema.prototype.virtual = function(name, options) { + if (options && options.ref) { + if (!options.localField) { + throw new Error('Reference virtuals require `localField` option'); + } + + if (!options.foreignField) { + throw new Error('Reference virtuals require `foreignField` option'); + } + + this.pre('init', function(next, obj) { + if (name in obj) { + if (!this.$$populatedVirtuals) { + this.$$populatedVirtuals = {}; + } + + if (options.justOne) { + this.$$populatedVirtuals[name] = Array.isArray(obj[name]) ? + obj[name][0] : + obj[name]; + } else { + this.$$populatedVirtuals[name] = Array.isArray(obj[name]) ? + obj[name] : + obj[name] == null ? [] : [obj[name]]; + } + + delete obj[name]; + } + next(); + }); + + var virtual = this.virtual(name); + virtual.options = options; + return virtual. + get(function() { + if (!this.$$populatedVirtuals) { + this.$$populatedVirtuals = {}; + } + if (name in this.$$populatedVirtuals) { + return this.$$populatedVirtuals[name]; + } + return null; + }). + set(function(v) { + if (!this.$$populatedVirtuals) { + this.$$populatedVirtuals = {}; + } + this.$$populatedVirtuals[name] = v; + }); + } + var virtuals = this.virtuals; var parts = name.split('.'); - return virtuals[name] = parts.reduce(function(mem, part, i) { + virtuals[name] = parts.reduce(function(mem, part, i) { mem[part] || (mem[part] = (i === parts.length - 1) - ? new VirtualType(options, name) - : {}); + ? new VirtualType(options, name) + : {}); return mem[part]; }, this.tree); + return virtuals[name]; }; /** @@ -4788,6 +5589,14 @@ Schema.prototype.remove = function(path) { path.forEach(function(name) { if (this.path(name)) { delete this.paths[name]; + + var pieces = name.split('.'); + var last = pieces.pop(); + var branch = this.tree; + for (var i = 0; i < pieces.length; ++i) { + branch = branch[pieces[i]]; + } + delete branch[last]; } }, this); } @@ -4798,15 +5607,16 @@ Schema.prototype.remove = function(path) { */ Schema.prototype._getSchema = function(path) { - var schema = this; - var pathschema = schema.path(path); + var _this = this; + var pathschema = _this.path(path); + var resultPath = []; if (pathschema) { + pathschema.$fullPath = path; return pathschema; } - // look for arrays - return (function search(parts, schema) { + function search(parts, schema) { var p = parts.length + 1, foundschema, trypath; @@ -4815,9 +5625,12 @@ Schema.prototype._getSchema = function(path) { trypath = parts.slice(0, p).join('.'); foundschema = schema.path(trypath); if (foundschema) { + resultPath.push(trypath); + if (foundschema.caster) { // array of Mixed? if (foundschema.caster instanceof MongooseTypes.Mixed) { + foundschema.caster.$fullPath = resultPath.join('.'); return foundschema.caster; } @@ -4828,21 +5641,88 @@ Schema.prototype._getSchema = function(path) { // If there is no foundschema.schema we are dealing with // a path like array.$ if (p !== parts.length && foundschema.schema) { - if ('$' === parts[p]) { + if (parts[p] === '$') { // comments.$.comments.$.title return search(parts.slice(p + 1), foundschema.schema); - } else { - // this is the last path of the selector - return search(parts.slice(p), foundschema.schema); } + // this is the last path of the selector + return search(parts.slice(p), foundschema.schema); } } + + foundschema.$fullPath = resultPath.join('.'); + return foundschema; } } - })(path.split('.'), schema); + } + + // look for arrays + return search(path.split('.'), _this); +}; + +/*! + * ignore + */ + +Schema.prototype._getPathType = function(path) { + var _this = this; + var pathschema = _this.path(path); + + if (pathschema) { + return 'real'; + } + + function search(parts, schema) { + var p = parts.length + 1, + foundschema, + trypath; + + while (p--) { + trypath = parts.slice(0, p).join('.'); + foundschema = schema.path(trypath); + if (foundschema) { + if (foundschema.caster) { + // array of Mixed? + if (foundschema.caster instanceof MongooseTypes.Mixed) { + return { schema: foundschema, pathType: 'mixed' }; + } + + // Now that we found the array, we need to check if there + // are remaining document paths to look up for casting. + // Also we need to handle array.$.path since schema.path + // doesn't work for that. + // If there is no foundschema.schema we are dealing with + // a path like array.$ + if (p !== parts.length && foundschema.schema) { + if (parts[p] === '$') { + if (p === parts.length - 1) { + return { schema: foundschema, pathType: 'nested' }; + } + // comments.$.comments.$.title + return search(parts.slice(p + 1), foundschema.schema); + } + // this is the last path of the selector + return search(parts.slice(p), foundschema.schema); + } + return { + schema: foundschema, + pathType: foundschema.$isSingleNested ? 'nested' : 'array' + }; + } + return { schema: foundschema, pathType: 'real' }; + } else if (p === parts.length && schema.nested[trypath]) { + return { schema: schema, pathType: 'nested' }; + } + } + return { schema: foundschema || schema, pathType: 'undefined' }; + } + + // look for arrays + return search(path.split('.'), _this); }; + /*! * Module exports. */ @@ -4887,27 +5767,29 @@ Schema.Types = MongooseTypes = require('./schema/index'); exports.ObjectId = MongooseTypes.ObjectId; }).call(this,require("buffer").Buffer) -},{"./drivers":11,"./promise_provider":23,"./schema/index":31,"./utils":45,"./virtualtype":46,"async":47,"buffer":49,"events":53,"kareem":77}],25:[function(require,module,exports){ +},{"./drivers":11,"./schema/index":33,"./schematype":41,"./utils":51,"./virtualtype":52,"async/each":54,"buffer":84,"events":87,"kareem":91}],27:[function(require,module,exports){ /*! * Module dependencies. */ -var SchemaType = require('../schematype'), - CastError = SchemaType.CastError, - Types = { - Boolean: require('./boolean'), - Date: require('./date'), - Number: require('./number'), - String: require('./string'), - ObjectId: require('./objectid'), - Buffer: require('./buffer') - }, - MongooseArray = require('../types').Array, - EmbeddedDoc = require('../types').Embedded, - Mixed = require('./mixed'), - cast = require('../cast'), - utils = require('../utils'), - isMongooseObject = utils.isMongooseObject; +var SchemaType = require('../schematype'); +var CastError = SchemaType.CastError; +var Types = { + Boolean: require('./boolean'), + Date: require('./date'), + Number: require('./number'), + String: require('./string'), + ObjectId: require('./objectid'), + Buffer: require('./buffer') +}; +var MongooseArray = require('../types').Array; +var EmbeddedDoc = require('../types').Embedded; +var Mixed = require('./mixed'); +var cast = require('../cast'); +var util = require('util'); +var utils = require('../utils'); +var castToNumber = require('./operators/helpers').castToNumber; +var geospatial = require('./operators/geospatial'); /** * Array SchemaType constructor @@ -4916,35 +5798,45 @@ var SchemaType = require('../schematype'), * @param {SchemaType} cast * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ -function SchemaArray(key, cast, options) { +function SchemaArray(key, cast, options, schemaOptions) { + var typeKey = 'type'; + if (schemaOptions && schemaOptions.typeKey) { + typeKey = schemaOptions.typeKey; + } + if (cast) { var castOptions = {}; - if ('Object' === utils.getFunctionName(cast.constructor)) { - if (cast.type) { + if (utils.getFunctionName(cast.constructor) === 'Object') { + if (cast[typeKey]) { // support { type: Woot } castOptions = utils.clone(cast); // do not alter user arguments - delete castOptions.type; - cast = cast.type; + delete castOptions[typeKey]; + cast = cast[typeKey]; } else { cast = Mixed; } } // support { type: 'String' } - var name = 'string' == typeof cast - ? cast - : utils.getFunctionName(cast); + var name = typeof cast === 'string' + ? cast + : utils.getFunctionName(cast); var caster = name in Types - ? Types[name] - : cast; + ? Types[name] + : cast; this.casterConstructor = caster; - this.caster = new caster(null, castOptions); + if (typeof caster === 'function') { + this.caster = new caster(null, castOptions); + } else { + this.caster = caster; + } + if (!(this.caster instanceof EmbeddedDoc)) { this.caster.path = key; } @@ -4952,40 +5844,44 @@ function SchemaArray(key, cast, options) { SchemaType.call(this, key, options, 'Array'); - var self = this, - defaultArr, - fn; + var defaultArr; + var fn; if (this.defaultValue) { defaultArr = this.defaultValue; - fn = 'function' == typeof defaultArr; + fn = typeof defaultArr === 'function'; } - this.default(function() { - var arr = fn ? defaultArr() : defaultArr || []; - return new MongooseArray(arr, self.path, this); - }); + if (!('defaultValue' in this) || this.defaultValue !== void 0) { + this.default(function() { + var arr = fn ? defaultArr() : defaultArr || []; + // Leave it up to `cast()` to convert the array + return arr; + }); + } } /** * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaArray.schemaName = 'Array'; /*! * Inherits from SchemaType. */ -SchemaArray.prototype = Object.create( SchemaType.prototype ); +SchemaArray.prototype = Object.create(SchemaType.prototype); SchemaArray.prototype.constructor = SchemaArray; /** - * Check required + * Check if the given value satisfies a required validator. The given value + * must be not null nor undefined, and have a non-zero length. * - * @param {Array} value - * @api private + * @param {Any} value + * @return {Boolean} + * @api public */ SchemaArray.prototype.checkRequired = function(value) { @@ -5020,13 +5916,12 @@ SchemaArray.prototype.applyGetters = function(value, scope) { SchemaArray.prototype.cast = function(value, doc, init) { if (Array.isArray(value)) { - if (!value.length && doc) { var indexes = doc.schema.indexedPaths(); for (var i = 0, l = indexes.length; i < l; ++i) { var pathIndex = indexes[i][0][this.path]; - if ('2dsphere' === pathIndex || '2d' === pathIndex) { + if (pathIndex === '2dsphere' || pathIndex === '2d') { return; } } @@ -5034,6 +5929,10 @@ SchemaArray.prototype.cast = function(value, doc, init) { if (!(value && value.isMongooseArray)) { value = new MongooseArray(value, this.path, doc); + } else if (value && value.isMongooseArray) { + // We need to create a new array, otherwise change tracking will + // update the old doc (gh-4449) + value = new MongooseArray(value, this.path, doc); } if (this.caster) { @@ -5043,19 +5942,18 @@ SchemaArray.prototype.cast = function(value, doc, init) { } } catch (e) { // rethrow - throw new CastError(e.type, value, this.path); + throw new CastError('[' + e.kind + ']', util.inspect(value), this.path, e); } } return value; - } else { - // gh-2442: if we're loading this from the db and its not an array, mark - // the whole array as modified. - if (!!doc && !!init) { - doc.markModified(this.path); - } - return this.cast([value], doc, init); } + // gh-2442: if we're loading this from the db and its not an array, mark + // the whole array as modified. + if (!!doc && !!init) { + doc.markModified(this.path); + } + return this.cast([value], doc, init); }; /** @@ -5074,16 +5972,14 @@ SchemaArray.prototype.castForQuery = function($conditional, value) { handler = this.$conditionalHandlers[$conditional]; if (!handler) { - throw new Error("Can't use " + $conditional + " with Array."); + throw new Error('Can\'t use ' + $conditional + ' with Array.'); } val = handler.call(this, value); - } else { - val = $conditional; var proto = this.casterConstructor.prototype; - var method = proto.castForQuery || proto.cast; + var method = proto && (proto.castForQuery || proto.cast); var caster = this.caster; if (Array.isArray(val)) { @@ -5091,112 +5987,18 @@ SchemaArray.prototype.castForQuery = function($conditional, value) { if (utils.isObject(v) && v.$elemMatch) { return v; } - if (method) v = method.call(caster, v); - return isMongooseObject(v) ? - v.toObject({ virtuals: false }) : - v; + if (method) { + v = method.call(caster, v); + } + return v; }); - } else if (method) { val = method.call(caster, val); } } - return val && isMongooseObject(val) ? - val.toObject({ virtuals: false }) : - val; -}; - -/*! - * @ignore - * - * $atomic cast helpers - */ - -function castToNumber(val) { - return Types.Number.prototype.cast.call(this, val); -} - -function castArraysOfNumbers(arr, self) { - self || (self = this); - - arr.forEach(function(v, i) { - if (Array.isArray(v)) { - castArraysOfNumbers(v, self); - } else { - arr[i] = castToNumber.call(self, v); - } - }); -} - -function cast$near(val) { - if (Array.isArray(val)) { - castArraysOfNumbers(val, this); - return val; - } - - if (val && val.$geometry) { - return cast$geometry(val, this); - } - - return SchemaArray.prototype.castForQuery.call(this, val); -} - -function cast$geometry(val, self) { - switch (val.$geometry.type) { - case 'Polygon': - case 'LineString': - case 'Point': - castArraysOfNumbers(val.$geometry.coordinates, self); - break; - default: - // ignore unknowns - break; - } - - if (val.$maxDistance) { - val.$maxDistance = castToNumber.call(self, val.$maxDistance); - } - - return val; -} - -function cast$within(val) { - var self = this; - - if (val.$maxDistance) { - val.$maxDistance = castToNumber.call(self, val.$maxDistance); - } - - if (val.$box || val.$polygon) { - var type = val.$box ? '$box' : '$polygon'; - val[type].forEach(function(arr) { - if (!Array.isArray(arr)) { - var msg = 'Invalid $within $box argument. ' - + 'Expected an array, received ' + arr; - throw new TypeError(msg); - } - arr.forEach(function(v, i) { - arr[i] = castToNumber.call(this, v); - }); - }); - } else if (val.$center || val.$centerSphere) { - type = val.$center ? '$center' : '$centerSphere'; - val[type].forEach(function(item, i) { - if (Array.isArray(item)) { - item.forEach(function(v, j) { - item[j] = castToNumber.call(this, v); - }); - } else { - val[type][i] = castToNumber.call(this, item); - } - }); - } else if (val.$geometry) { - cast$geometry(val, this); - } - return val; -} +}; function cast$all(val) { if (!Array.isArray(val)) { @@ -5231,20 +6033,12 @@ function cast$elemMatch(val) { return cast(this.casterConstructor.schema, val); } -function cast$geoIntersects(val) { - var geo = val.$geometry; - if (!geo) return; - - cast$geometry(val, this); - return val; -} - var handle = SchemaArray.prototype.$conditionalHandlers = {}; handle.$all = cast$all; handle.$options = String; handle.$elemMatch = cast$elemMatch; -handle.$geoIntersects = cast$geoIntersects; +handle.$geoIntersects = geospatial.cast$geoIntersects; handle.$or = handle.$and = function(val) { if (!Array.isArray(val)) { throw new TypeError('conditional $or/$and require array'); @@ -5259,12 +6053,13 @@ handle.$or = handle.$and = function(val) { }; handle.$near = -handle.$nearSphere = cast$near; +handle.$nearSphere = geospatial.cast$near; handle.$within = -handle.$geoWithin = cast$within; +handle.$geoWithin = geospatial.cast$within; handle.$size = +handle.$minDistance = handle.$maxDistance = castToNumber; handle.$eq = @@ -5283,7 +6078,7 @@ handle.$regex = SchemaArray.prototype.castForQuery; module.exports = SchemaArray; -},{"../cast":4,"../schematype":36,"../types":42,"../utils":45,"./boolean":26,"./buffer":27,"./date":28,"./mixed":32,"./number":33,"./objectid":34,"./string":35}],26:[function(require,module,exports){ +},{"../cast":4,"../schematype":41,"../types":48,"../utils":51,"./boolean":28,"./buffer":29,"./date":30,"./mixed":34,"./number":35,"./objectid":36,"./operators/geospatial":38,"./operators/helpers":39,"./string":40,"util":138}],28:[function(require,module,exports){ /*! * Module dependencies. */ @@ -5298,7 +6093,7 @@ var SchemaType = require('../schematype'); * @param {String} path * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function SchemaBoolean(path, options) { @@ -5309,20 +6104,24 @@ function SchemaBoolean(path, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaBoolean.schemaName = 'Boolean'; /*! * Inherits from SchemaType. */ -SchemaBoolean.prototype = Object.create( SchemaType.prototype ); +SchemaBoolean.prototype = Object.create(SchemaType.prototype); SchemaBoolean.prototype.constructor = SchemaBoolean; /** - * Required validator + * Check if the given value satisfies a required validator. For a boolean + * to satisfy a required validator, it must be strictly equal to true or to + * false. * - * @api private + * @param {Any} value + * @return {Boolean} + * @api public */ SchemaBoolean.prototype.checkRequired = function(value) { @@ -5337,15 +6136,23 @@ SchemaBoolean.prototype.checkRequired = function(value) { */ SchemaBoolean.prototype.cast = function(value) { - if (null === value) return value; - if ('0' === value) return false; - if ('true' === value) return true; - if ('false' === value) return false; + if (value === null) { + return value; + } + if (value === '0') { + return false; + } + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } return !!value; }; SchemaBoolean.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, {}); + utils.options(SchemaType.prototype.$conditionalHandlers, {}); /** * Casts contents for queries. @@ -5357,7 +6164,7 @@ SchemaBoolean.$conditionalHandlers = SchemaBoolean.prototype.castForQuery = function($conditional, val) { var handler; - if (2 === arguments.length) { + if (arguments.length === 2) { handler = SchemaBoolean.$conditionalHandlers[$conditional]; if (handler) { @@ -5376,12 +6183,13 @@ SchemaBoolean.prototype.castForQuery = function($conditional, val) { module.exports = SchemaBoolean; -},{"../schematype":36,"../utils":45}],27:[function(require,module,exports){ +},{"../schematype":41,"../utils":51}],29:[function(require,module,exports){ (function (Buffer){ /*! * Module dependencies. */ +var handleBitwiseOperator = require('./operators/bitwise'); var utils = require('../utils'); var MongooseBuffer = require('../types').Buffer; @@ -5395,9 +6203,9 @@ var Document; * Buffer SchemaType constructor * * @param {String} key - * @param {SchemaType} cast + * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function SchemaBuffer(key, options) { @@ -5408,28 +6216,32 @@ function SchemaBuffer(key, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaBuffer.schemaName = 'Buffer'; /*! * Inherits from SchemaType. */ -SchemaBuffer.prototype = Object.create( SchemaType.prototype ); +SchemaBuffer.prototype = Object.create(SchemaType.prototype); SchemaBuffer.prototype.constructor = SchemaBuffer; /** - * Check required + * Check if the given value satisfies a required validator. To satisfy a + * required validator, a buffer must not be null or undefined and have + * non-zero length. * - * @api private + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ SchemaBuffer.prototype.checkRequired = function(value, doc) { if (SchemaType._isRef(this, value, doc, true)) { - return null != value; - } else { - return !!(value && value.length); + return !!value; } + return !!(value && value.length); }; /** @@ -5446,7 +6258,7 @@ SchemaBuffer.prototype.cast = function(value, doc, init) { if (SchemaType._isRef(this, value, doc, init)) { // wait! we may need to cast this to a document - if (null == value) { + if (value === null || value === undefined) { return value; } @@ -5500,10 +6312,15 @@ SchemaBuffer.prototype.cast = function(value, doc, init) { return ret; } - if (null === value) return value; + if (value === null) { + return value; + } var type = typeof value; - if ('string' == type || 'number' == type || Array.isArray(value)) { + if (type === 'string' || type === 'number' || Array.isArray(value)) { + if (type === 'number') { + value = [value]; + } ret = new MongooseBuffer(value, [this.path, doc]); return ret; } @@ -5519,12 +6336,16 @@ function handleSingle(val) { } SchemaBuffer.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$gt' : handleSingle, - '$gte': handleSingle, - '$lt' : handleSingle, - '$lte': handleSingle - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $bitsAllClear: handleBitwiseOperator, + $bitsAnyClear: handleBitwiseOperator, + $bitsAllSet: handleBitwiseOperator, + $bitsAnySet: handleBitwiseOperator, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle + }); /** * Casts contents for queries. @@ -5538,13 +6359,13 @@ SchemaBuffer.prototype.castForQuery = function($conditional, val) { var handler; if (arguments.length === 2) { handler = this.$conditionalHandlers[$conditional]; - if (!handler) - throw new Error("Can't use " + $conditional + " with Buffer."); + if (!handler) { + throw new Error('Can\'t use ' + $conditional + ' with Buffer.'); + } return handler.call(this, val); - } else { - val = $conditional; - return this.cast(val).toObject(); } + val = $conditional; + return this.cast(val).toObject({ transform: false }); }; /*! @@ -5554,12 +6375,12 @@ SchemaBuffer.prototype.castForQuery = function($conditional, val) { module.exports = SchemaBuffer; }).call(this,require("buffer").Buffer) -},{"../schematype":36,"../types":42,"../utils":45,"./../document":5,"buffer":49}],28:[function(require,module,exports){ +},{"../schematype":41,"../types":48,"../utils":51,"./../document":5,"./operators/bitwise":37,"buffer":84}],30:[function(require,module,exports){ /*! * Module requirements. */ -var errorMessages = require('../error').messages; +var MongooseError = require('../error'); var utils = require('../utils'); var SchemaType = require('../schematype'); @@ -5572,7 +6393,7 @@ var CastError = SchemaType.CastError; * @param {String} key * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function SchemaDate(key, options) { @@ -5583,20 +6404,20 @@ function SchemaDate(key, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaDate.schemaName = 'Date'; /*! * Inherits from SchemaType. */ -SchemaDate.prototype = Object.create( SchemaType.prototype ); +SchemaDate.prototype = Object.create(SchemaType.prototype); SchemaDate.prototype.constructor = SchemaDate; /** * Declares a TTL index (rounded to the nearest second) for _Date_ types only. * - * This sets the `expiresAfterSeconds` index option available in MongoDB >= 2.1.2. + * This sets the `expireAfterSeconds` index option available in MongoDB >= 2.1.2. * This index type is only compatible with Date types. * * ####Example: @@ -5625,7 +6446,7 @@ SchemaDate.prototype.constructor = SchemaDate; */ SchemaDate.prototype.expires = function(when) { - if (!this._index || 'Object' !== this._index.constructor.name) { + if (!this._index || this._index.constructor.name !== 'Object') { this._index = {}; } @@ -5635,9 +6456,13 @@ SchemaDate.prototype.expires = function(when) { }; /** - * Required validator for date + * Check if the given value satisfies a required validator. To satisfy + * a required validator, the given value must be an instance of `Date`. * - * @api private + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ SchemaDate.prototype.checkRequired = function(value) { @@ -5678,17 +6503,17 @@ SchemaDate.prototype.checkRequired = function(value) { SchemaDate.prototype.min = function(value, message) { if (this.minValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.minValidator; + return v.validator !== this.minValidator; }, this); } if (value) { - var msg = message || errorMessages.Date.min; + var msg = message || MongooseError.messages.Date.min; msg = msg.replace(/{MIN}/, (value === Date.now ? 'Date.now()' : this.cast(value).toString())); - var self = this; + var _this = this; this.validators.push({ validator: this.minValidator = function(val) { - var min = (value === Date.now ? value() : self.cast(value)); + var min = (value === Date.now ? value() : _this.cast(value)); return val === null || val.valueOf() >= min.valueOf(); }, message: msg, @@ -5734,17 +6559,17 @@ SchemaDate.prototype.min = function(value, message) { SchemaDate.prototype.max = function(value, message) { if (this.maxValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.maxValidator; + return v.validator !== this.maxValidator; }, this); } if (value) { - var msg = message || errorMessages.Date.max; + var msg = message || MongooseError.messages.Date.max; msg = msg.replace(/{MAX}/, (value === Date.now ? 'Date.now()' : this.cast(value).toString())); - var self = this; + var _this = this; this.validators.push({ validator: this.maxValidator = function(val) { - var max = (value === Date.now ? value() : self.cast(value)); + var max = (value === Date.now ? value() : _this.cast(value)); return val === null || val.valueOf() <= max.valueOf(); }, message: msg, @@ -5765,27 +6590,35 @@ SchemaDate.prototype.max = function(value, message) { SchemaDate.prototype.cast = function(value) { // If null or undefined - if (value == null || value === '') + if (value === null || value === void 0 || value === '') { return null; + } + + if (value instanceof Date) { + if (isNaN(value.valueOf())) { + throw new CastError('date', value, this.path); + } - if (value instanceof Date) return value; + } var date; - // support for timestamps - if (typeof value !== 'undefined') { - if (value instanceof Number || 'number' == typeof value - || String(value) == Number(value)) { - date = new Date(Number(value)); - } else if (value.toString) { - // support for date strings - date = new Date(value.toString()); - } + if (typeof value === 'boolean') { + throw new CastError('date', value, this.path); + } - if (date.toString() != 'Invalid Date') { - return date; - } + if (value instanceof Number || typeof value === 'number' + || String(value) == Number(value)) { + // support for timestamps + date = new Date(Number(value)); + } else if (value.valueOf) { + // support for moment.js + date = new Date(value.valueOf()); + } + + if (!isNaN(date.valueOf())) { + return date; } throw new CastError('date', value, this.path); @@ -5802,12 +6635,12 @@ function handleSingle(val) { } SchemaDate.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$gt': handleSingle, - '$gte': handleSingle, - '$lt': handleSingle, - '$lte': handleSingle - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle + }); /** @@ -5821,14 +6654,14 @@ SchemaDate.prototype.$conditionalHandlers = SchemaDate.prototype.castForQuery = function($conditional, val) { var handler; - if (2 !== arguments.length) { + if (arguments.length !== 2) { return this.cast($conditional); } handler = this.$conditionalHandlers[$conditional]; if (!handler) { - throw new Error("Can't use " + $conditional + " with Date."); + throw new Error('Can\'t use ' + $conditional + ' with Date.'); } return handler.call(this, val); @@ -5840,7 +6673,7 @@ SchemaDate.prototype.castForQuery = function($conditional, val) { module.exports = SchemaDate; -},{"../error":12,"../schematype":36,"../utils":45}],29:[function(require,module,exports){ +},{"../error":12,"../schematype":41,"../utils":51}],31:[function(require,module,exports){ /* eslint no-empty: 1 */ /*! @@ -5852,6 +6685,7 @@ var CastError = require('../error/cast'); var MongooseDocumentArray = require('../types/documentarray'); var SchemaType = require('../schematype'); var Subdocument = require('../types/embedded'); +var util = require('util'); /** * SubdocsArray SchemaType constructor @@ -5860,7 +6694,7 @@ var Subdocument = require('../types/embedded'); * @param {Schema} schema * @param {Object} options * @inherits SchemaArray - * @api private + * @api public */ function DocumentArray(key, schema, options) { @@ -5874,41 +6708,47 @@ function DocumentArray(key, schema, options) { EmbeddedDocument.schema = schema; // apply methods - for (var i in schema.methods) + for (var i in schema.methods) { EmbeddedDocument.prototype[i] = schema.methods[i]; + } // apply statics - for (i in schema.statics) + for (i in schema.statics) { EmbeddedDocument[i] = schema.statics[i]; + } EmbeddedDocument.options = options; - this.schema = schema; ArrayType.call(this, key, EmbeddedDocument, options); this.schema = schema; - var path = this.path; + this.$isMongooseDocumentArray = true; var fn = this.defaultValue; - this.default(function() { - var arr = fn.call(this); - if (!Array.isArray(arr)) arr = [arr]; - return new MongooseDocumentArray(arr, path, this); - }); + if (!('defaultValue' in this) || fn !== void 0) { + this.default(function() { + var arr = fn.call(this); + if (!Array.isArray(arr)) { + arr = [arr]; + } + // Leave it up to `cast()` to convert this to a documentarray + return arr; + }); + } } /** * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ DocumentArray.schemaName = 'DocumentArray'; /*! * Inherits from ArrayType. */ -DocumentArray.prototype = Object.create( ArrayType.prototype ); +DocumentArray.prototype = Object.create(ArrayType.prototype); DocumentArray.prototype.constructor = DocumentArray; /** @@ -5917,7 +6757,8 @@ DocumentArray.prototype.constructor = DocumentArray; * @api private */ -DocumentArray.prototype.doValidate = function(array, fn, scope) { +DocumentArray.prototype.doValidate = function(array, fn, scope, options) { + var _this = this; SchemaType.prototype.doValidate.call(this, array, function(err) { if (err) { return fn(err); @@ -5926,12 +6767,24 @@ DocumentArray.prototype.doValidate = function(array, fn, scope) { var count = array && array.length; var error; - if (!count) return fn(); + if (!count) { + return fn(); + } + if (options && options.updateValidator) { + return fn(); + } // handle sparse arrays, do not use array.forEach which does not // iterate over sparse elements yet reports array.length including // them :( + function callback(err) { + if (err) { + error = err; + } + --count || fn(error); + } + for (var i = 0, len = count; i < len; ++i) { // sidestep sparse entries var doc = array[i]; @@ -5940,12 +6793,20 @@ DocumentArray.prototype.doValidate = function(array, fn, scope) { continue; } - doc.validate(function(err) { - if (err) { - error = err; - } - --count || fn(error); - }); + // If you set the array index directly, the doc might not yet be + // a full fledged mongoose subdoc, so make it into one. + if (!(doc instanceof Subdocument)) { + doc = array[i] = new _this.casterConstructor(doc, array, undefined, + undefined, i); + } + + // HACK: use $__original_validate to avoid promises so bluebird doesn't + // complain + if (doc.$__original_validate) { + doc.$__original_validate({__noPromise: true}, callback); + } else { + doc.validate({__noPromise: true}, callback); + } } }, scope); }; @@ -5963,12 +6824,16 @@ DocumentArray.prototype.doValidate = function(array, fn, scope) { DocumentArray.prototype.doValidateSync = function(array, scope) { var schemaTypeError = SchemaType.prototype.doValidateSync.call(this, array, scope); - if (schemaTypeError) return schemaTypeError; + if (schemaTypeError) { + return schemaTypeError; + } var count = array && array.length, resultError = null; - if (!count) return; + if (!count) { + return; + } // handle sparse arrays, do not use array.forEach which does not // iterate over sparse elements yet reports array.length including @@ -5976,10 +6841,14 @@ DocumentArray.prototype.doValidateSync = function(array, scope) { for (var i = 0, len = count; i < len; ++i) { // only first error - if ( resultError ) break; + if (resultError) { + break; + } // sidestep sparse entries var doc = array[i]; - if (!doc) continue; + if (!doc) { + continue; + } var subdocValidateError = doc.validateSync(); @@ -5999,7 +6868,7 @@ DocumentArray.prototype.doValidateSync = function(array, scope) { * @api private */ -DocumentArray.prototype.cast = function(value, doc, init, prev) { +DocumentArray.prototype.cast = function(value, doc, init, prev, options) { var selected, subdoc, i; @@ -6013,18 +6882,31 @@ DocumentArray.prototype.cast = function(value, doc, init, prev) { return this.cast([value], doc, init, prev); } - if (!(value && value.isMongooseDocumentArray)) { + if (!(value && value.isMongooseDocumentArray) && + (!options || !options.skipDocumentArrayCast)) { value = new MongooseDocumentArray(value, this.path, doc); if (prev && prev._handlers) { for (var key in prev._handlers) { doc.removeListener(key, prev._handlers[key]); } } + } else if (value && value.isMongooseDocumentArray) { + // We need to create a new array, otherwise change tracking will + // update the old doc (gh-4449) + value = new MongooseDocumentArray(value, this.path, doc); } i = value.length; while (i--) { + if (!value[i]) { + continue; + } + // Check if the document has a different schema (re gh-3701) + if ((value[i] instanceof Subdocument) && + value[i].schema !== this.casterConstructor.schema) { + value[i] = value[i].toObject({transform: false}); + } if (!(value[i] instanceof Subdocument) && value[i]) { if (init) { selected || (selected = scopePaths(this, doc.$__.selected, init)); @@ -6033,7 +6915,8 @@ DocumentArray.prototype.cast = function(value, doc, init, prev) { } else { try { subdoc = prev.id(value[i]._id); - } catch (e) {} + } catch (e) { + } if (prev && subdoc) { // handle resetting doc with existing id but differing data @@ -6045,12 +6928,14 @@ DocumentArray.prototype.cast = function(value, doc, init, prev) { } else { try { subdoc = new this.casterConstructor(value[i], value, undefined, - undefined, i); + undefined, i); // if set() is hooked it will have no return value // see gh-746 value[i] = subdoc; } catch (error) { - throw new CastError('embedded', value[i], value._path); + var valueInErrorMessage = util.inspect(value[i]); + throw new CastError('embedded', valueInErrorMessage, + value._path, error); } } } @@ -6070,7 +6955,9 @@ DocumentArray.prototype.cast = function(value, doc, init, prev) { */ function scopePaths(array, fields, init) { - if (!(init && fields)) return undefined; + if (!(init && fields)) { + return undefined; + } var path = array.path + '.', keys = Object.keys(fields), @@ -6081,7 +6968,7 @@ function scopePaths(array, fields, init) { while (i--) { key = keys[i]; - if (0 === key.indexOf(path)) { + if (key.indexOf(path) === 0) { hasKeys || (hasKeys = true); selected[key.substring(path.length)] = fields[key]; } @@ -6096,9 +6983,17 @@ function scopePaths(array, fields, init) { module.exports = DocumentArray; -},{"../error/cast":13,"../schematype":36,"../types/documentarray":40,"../types/embedded":41,"./array":25}],30:[function(require,module,exports){ +},{"../error/cast":13,"../schematype":41,"../types/documentarray":46,"../types/embedded":47,"./array":27,"util":138}],32:[function(require,module,exports){ +'use strict'; + +/*! + * Module dependencies. + */ + var SchemaType = require('../schematype'); var Subdocument = require('../types/subdocument'); +var castToNumber = require('./operators/helpers').castToNumber; +var geospatial = require('./operators/geospatial'); module.exports = Embedded; @@ -6109,18 +7004,43 @@ module.exports = Embedded; * @param {String} key * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function Embedded(schema, path, options) { - var _embedded = function() { + var _embedded = function(value, path, parent) { + var _this = this; Subdocument.apply(this, arguments); + this.$parent = parent; + if (parent) { + parent.on('save', function() { + _this.emit('save', _this); + }); + + parent.on('isNew', function(val) { + _this.isNew = val; + _this.emit('isNew', val); + }); + } }; _embedded.prototype = Object.create(Subdocument.prototype); _embedded.prototype.$__setSchema(schema); _embedded.schema = schema; _embedded.$isSingleNested = true; _embedded.prototype.$basePath = path; + _embedded.prototype.toBSON = function() { + return this.toObject({ transform: false }); + }; + + // apply methods + for (var i in schema.methods) { + _embedded.prototype[i] = schema.methods[i]; + } + + // apply statics + for (i in schema.statics) { + _embedded[i] = schema.statics[i]; + } this.caster = _embedded; this.schema = schema; @@ -6131,16 +7051,51 @@ function Embedded(schema, path, options) { Embedded.prototype = Object.create(SchemaType.prototype); /** - * Casts contents + * Special case for when users use a common location schema to represent + * locations for use with $geoWithin. + * https://docs.mongodb.org/manual/reference/operator/query/geoWithin/ * - * @param {Object} value + * @param {Object} val * @api private */ -Embedded.prototype.cast = function(val, doc) { - var subdoc = new this.caster(); - subdoc = subdoc.init(val); - subdoc.$parent = doc; +Embedded.prototype.$conditionalHandlers.$geoWithin = function(val) { + return { $geometry: this.castForQuery(val.$geometry) }; +}; + +/*! + * ignore + */ + +Embedded.prototype.$conditionalHandlers.$near = +Embedded.prototype.$conditionalHandlers.$nearSphere = geospatial.cast$near; + +Embedded.prototype.$conditionalHandlers.$within = +Embedded.prototype.$conditionalHandlers.$geoWithin = geospatial.cast$within; + +Embedded.prototype.$conditionalHandlers.$geoIntersects = + geospatial.cast$geoIntersects; + +Embedded.prototype.$conditionalHandlers.$minDistance = castToNumber; +Embedded.prototype.$conditionalHandlers.$maxDistance = castToNumber; + +/** + * Casts contents + * + * @param {Object} value + * @api private + */ + +Embedded.prototype.cast = function(val, doc, init) { + if (val && val.$isSingleNested) { + return val; + } + var subdoc = new this.caster(void 0, doc ? doc.$__.selected : void 0, doc); + if (init) { + subdoc.init(val); + } else { + subdoc.set(val, undefined, true); + } return subdoc; }; @@ -6160,22 +7115,51 @@ Embedded.prototype.castForQuery = function($conditional, val) { throw new Error('Can\'t use ' + $conditional); } return handler.call(this, val); - } else { - val = $conditional; - return new this.caster(val). - toObject({ virtuals: false }); } + val = $conditional; + if (val == null) { + return val; + } + + return new this.caster(val); }; +/** + * Async validation on this single nested doc. + * + * @api private + */ + Embedded.prototype.doValidate = function(value, fn) { - value.validate(fn, { __noPromise: true }); + SchemaType.prototype.doValidate.call(this, value, function(error) { + if (error) { + return fn(error); + } + if (!value) { + return fn(null); + } + value.validate(fn, {__noPromise: true}); + }); }; +/** + * Synchronously validate this single nested doc + * + * @api private + */ + Embedded.prototype.doValidateSync = function(value) { + var schemaTypeError = SchemaType.prototype.doValidateSync.call(this, value); + if (schemaTypeError) { + return schemaTypeError; + } + if (!value) { + return; + } return value.validateSync(); }; -},{"../schematype":36,"../types/subdocument":44}],31:[function(require,module,exports){ +},{"../schematype":41,"../types/subdocument":50,"./operators/geospatial":38,"./operators/helpers":39}],33:[function(require,module,exports){ /*! * Module exports. @@ -6207,8 +7191,7 @@ exports.Oid = exports.ObjectId; exports.Object = exports.Mixed; exports.Bool = exports.Boolean; -},{"./array":25,"./boolean":26,"./buffer":27,"./date":28,"./documentarray":29,"./embedded":30,"./mixed":32,"./number":33,"./objectid":34,"./string":35}],32:[function(require,module,exports){ - +},{"./array":27,"./boolean":28,"./buffer":29,"./date":30,"./documentarray":31,"./embedded":32,"./mixed":34,"./number":35,"./objectid":36,"./string":40}],34:[function(require,module,exports){ /*! * Module dependencies. */ @@ -6222,18 +7205,16 @@ var utils = require('../utils'); * @param {String} path * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function Mixed(path, options) { if (options && options.default) { var def = options.default; - if (Array.isArray(def) && 0 === def.length) { + if (Array.isArray(def) && def.length === 0) { // make sure empty array defaults are handled options.default = Array; - } else if (!options.shared && - utils.isObject(def) && - 0 === Object.keys(def).length) { + } else if (!options.shared && utils.isObject(def) && Object.keys(def).length === 0) { // prevent odd "shared" objects between documents options.default = function() { return {}; @@ -6248,26 +7229,16 @@ function Mixed(path, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ Mixed.schemaName = 'Mixed'; /*! * Inherits from SchemaType. */ -Mixed.prototype = Object.create( SchemaType.prototype ); +Mixed.prototype = Object.create(SchemaType.prototype); Mixed.prototype.constructor = Mixed; -/** - * Required validator - * - * @api private - */ - -Mixed.prototype.checkRequired = function(val) { - return (val !== undefined) && (val !== null); -}; - /** * Casts `val` for Mixed. * @@ -6290,7 +7261,9 @@ Mixed.prototype.cast = function(val) { */ Mixed.prototype.castForQuery = function($cond, val) { - if (arguments.length === 2) return val; + if (arguments.length === 2) { + return val; + } return $cond; }; @@ -6300,17 +7273,18 @@ Mixed.prototype.castForQuery = function($cond, val) { module.exports = Mixed; -},{"../schematype":36,"../utils":45}],33:[function(require,module,exports){ +},{"../schematype":41,"../utils":51}],35:[function(require,module,exports){ (function (Buffer){ /*! * Module requirements. */ -var SchemaType = require('../schematype'), - CastError = SchemaType.CastError, - errorMessages = require('../error').messages, - utils = require('../utils'), - Document; +var SchemaType = require('../schematype'); +var CastError = SchemaType.CastError; +var handleBitwiseOperator = require('./operators/bitwise'); +var MongooseError = require('../error'); +var utils = require('../utils'); +var Document; /** * Number SchemaType constructor. @@ -6318,7 +7292,7 @@ var SchemaType = require('../schematype'), * @param {String} key * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function SchemaNumber(key, options) { @@ -6329,28 +7303,30 @@ function SchemaNumber(key, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaNumber.schemaName = 'Number'; /*! * Inherits from SchemaType. */ -SchemaNumber.prototype = Object.create( SchemaType.prototype ); +SchemaNumber.prototype = Object.create(SchemaType.prototype); SchemaNumber.prototype.constructor = SchemaNumber; /** - * Required validator for number + * Check if the given value satisfies a required validator. * - * @api private + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ SchemaNumber.prototype.checkRequired = function checkRequired(value, doc) { if (SchemaType._isRef(this, value, doc, true)) { - return null != value; - } else { - return typeof value == 'number' || value instanceof Number; + return !!value; } + return typeof value === 'number' || value instanceof Number; }; /** @@ -6387,16 +7363,16 @@ SchemaNumber.prototype.checkRequired = function checkRequired(value, doc) { SchemaNumber.prototype.min = function(value, message) { if (this.minValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.minValidator; + return v.validator !== this.minValidator; }, this); } - if (null != value) { - var msg = message || errorMessages.Number.min; + if (value !== null && value !== undefined) { + var msg = message || MongooseError.messages.Number.min; msg = msg.replace(/{MIN}/, value); this.validators.push({ validator: this.minValidator = function(v) { - return v === null || v >= value; + return v == null || v >= value; }, message: msg, type: 'min', @@ -6441,16 +7417,16 @@ SchemaNumber.prototype.min = function(value, message) { SchemaNumber.prototype.max = function(value, message) { if (this.maxValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.maxValidator; + return v.validator !== this.maxValidator; }, this); } - if (null != value) { - var msg = message || errorMessages.Number.max; + if (value !== null && value !== undefined) { + var msg = message || MongooseError.messages.Number.max; msg = msg.replace(/{MAX}/, value); this.validators.push({ validator: this.maxValidator = function(v) { - return v === null || v <= value; + return v == null || v <= value; }, message: msg, type: 'max', @@ -6474,7 +7450,7 @@ SchemaNumber.prototype.cast = function(value, doc, init) { if (SchemaType._isRef(this, value, doc, init)) { // wait! we may need to cast this to a document - if (null == value) { + if (value === null || value === undefined) { return value; } @@ -6487,7 +7463,7 @@ SchemaNumber.prototype.cast = function(value, doc, init) { } // setting a populated path - if ('number' == typeof value) { + if (typeof value === 'number') { return value; } else if (Buffer.isBuffer(value) || !utils.isObject(value)) { throw new CastError('number', value, this.path); @@ -6505,19 +7481,26 @@ SchemaNumber.prototype.cast = function(value, doc, init) { } var val = value && value._id - ? value._id // documents - : value; + ? value._id // documents + : value; if (!isNaN(val)) { - if (null === val) return val; - if ('' === val) return null; + if (val === null) { + return val; + } + if (val === '') { + return null; + } if (typeof val === 'string' || typeof val === 'boolean') { val = Number(val); } - if (val instanceof Number) return val; - if ('number' == typeof val) return val; - if (val.toString && !Array.isArray(val) && - val.toString() == Number(val)) { + if (val instanceof Number) { + return val; + } + if (typeof val === 'number') { + return val; + } + if (val.toString && !Array.isArray(val) && val.toString() == Number(val)) { return new Number(val); } } @@ -6534,23 +7517,27 @@ function handleSingle(val) { } function handleArray(val) { - var self = this; + var _this = this; if (!Array.isArray(val)) { return [this.cast(val)]; } return val.map(function(m) { - return self.cast(m); + return _this.cast(m); }); } SchemaNumber.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$gt' : handleSingle, - '$gte': handleSingle, - '$lt' : handleSingle, - '$lte': handleSingle, - '$mod': handleArray - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $bitsAllClear: handleBitwiseOperator, + $bitsAnyClear: handleBitwiseOperator, + $bitsAllSet: handleBitwiseOperator, + $bitsAnySet: handleBitwiseOperator, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle, + $mod: handleArray + }); /** * Casts contents for queries. @@ -6564,13 +7551,13 @@ SchemaNumber.prototype.castForQuery = function($conditional, val) { var handler; if (arguments.length === 2) { handler = this.$conditionalHandlers[$conditional]; - if (!handler) - throw new Error("Can't use " + $conditional + " with Number."); + if (!handler) { + throw new Error('Can\'t use ' + $conditional + ' with Number.'); + } return handler.call(this, val); - } else { - val = this.cast($conditional); - return val == null ? val : val; } + val = this.cast($conditional); + return val; }; /*! @@ -6580,7 +7567,7 @@ SchemaNumber.prototype.castForQuery = function($conditional, val) { module.exports = SchemaNumber; }).call(this,require("buffer").Buffer) -},{"../error":12,"../schematype":36,"../utils":45,"./../document":5,"buffer":49}],34:[function(require,module,exports){ +},{"../error":12,"../schematype":41,"../utils":51,"./../document":5,"./operators/bitwise":37,"buffer":84}],36:[function(require,module,exports){ (function (Buffer){ /* eslint no-empty: 1 */ @@ -6600,7 +7587,7 @@ var SchemaType = require('../schematype'), * @param {String} key * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function ObjectId(key, options) { @@ -6611,14 +7598,14 @@ function ObjectId(key, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ ObjectId.schemaName = 'ObjectId'; /*! * Inherits from SchemaType. */ -ObjectId.prototype = Object.create( SchemaType.prototype ); +ObjectId.prototype = Object.create(SchemaType.prototype); ObjectId.prototype.constructor = ObjectId; /** @@ -6638,17 +7625,19 @@ ObjectId.prototype.auto = function(turnOn) { }; /** - * Check required + * Check if the given value satisfies a required validator. * - * @api private + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ ObjectId.prototype.checkRequired = function checkRequired(value, doc) { if (SchemaType._isRef(this, value, doc, true)) { - return null != value; - } else { - return value instanceof oid; + return !!value; } + return value instanceof oid; }; /** @@ -6664,7 +7653,7 @@ ObjectId.prototype.cast = function(value, doc, init) { if (SchemaType._isRef(this, value, doc, init)) { // wait! we may need to cast this to a document - if (null == value) { + if (value === null || value === undefined) { return value; } @@ -6689,16 +7678,26 @@ ObjectId.prototype.cast = function(value, doc, init) { var path = doc.$__fullPath(this.path); var owner = doc.ownerDocument ? doc.ownerDocument() : doc; var pop = owner.populated(path, true); - var ret = new pop.options.model(value); - ret.$__.wasPopulated = true; + var ret = value; + if (!doc.$__.populated || + !doc.$__.populated[path] || + !doc.$__.populated[path].options || + !doc.$__.populated[path].options.options || + !doc.$__.populated[path].options.options.lean) { + ret = new pop.options.model(value); + ret.$__.wasPopulated = true; + } + return ret; } - // If null or undefined - if (value == null) return value; + if (value === null || value === undefined) { + return value; + } - if (value instanceof oid) + if (value instanceof oid) { return value; + } if (value._id) { if (value._id instanceof oid) { @@ -6706,14 +7705,15 @@ ObjectId.prototype.cast = function(value, doc, init) { } if (value._id.toString instanceof Function) { try { - return oid.createFromHexString(value._id.toString()); - } catch (e) {} + return new oid(value._id.toString()); + } catch (e) { + } } } if (value.toString instanceof Function) { try { - return oid.createFromHexString(value.toString()); + return new oid(value.toString()); } catch (err) { throw new CastError('ObjectId', value, this.path); } @@ -6731,12 +7731,12 @@ function handleSingle(val) { } ObjectId.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$gt': handleSingle, - '$gte': handleSingle, - '$lt': handleSingle, - '$lte': handleSingle - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle + }); /** * Casts contents for queries. @@ -6750,12 +7750,12 @@ ObjectId.prototype.castForQuery = function($conditional, val) { var handler; if (arguments.length === 2) { handler = this.$conditionalHandlers[$conditional]; - if (!handler) - throw new Error("Can't use " + $conditional + " with ObjectId."); + if (!handler) { + throw new Error('Can\'t use ' + $conditional + ' with ObjectId.'); + } return handler.call(this, val); - } else { - return this.cast($conditional); } + return this.cast($conditional); }; /*! @@ -6778,144 +7778,325 @@ function resetId(v) { module.exports = ObjectId; }).call(this,require("buffer").Buffer) -},{"../schematype":36,"../types/objectid":43,"../utils":45,"./../document":5,"buffer":49}],35:[function(require,module,exports){ +},{"../schematype":41,"../types/objectid":49,"../utils":51,"./../document":5,"buffer":84}],37:[function(require,module,exports){ (function (Buffer){ - /*! - * Module dependencies. + * Module requirements. */ -var SchemaType = require('../schematype'), - CastError = SchemaType.CastError, - errorMessages = require('../error').messages, - utils = require('../utils'), - Document; +var CastError = require('../../error/cast'); -/** - * String SchemaType constructor. - * - * @param {String} key - * @param {Object} options - * @inherits SchemaType - * @api private +/*! + * ignore */ -function SchemaString(key, options) { - this.enumValues = []; - this.regExp = null; - SchemaType.call(this, key, options, 'String'); +function handleBitwiseOperator(val) { + var _this = this; + if (Array.isArray(val)) { + return val.map(function(v) { + return _castNumber(_this.path, v); + }); + } else if (Buffer.isBuffer(val)) { + return val; + } + // Assume trying to cast to number + return _castNumber(_this.path, val); } -/** - * This schema type's name, to defend against minifiers that mangle - * function names. - * - * @api private +/*! + * ignore */ -SchemaString.schemaName = 'String'; +function _castNumber(path, num) { + var v = Number(num); + if (isNaN(v)) { + throw new CastError('number', num, path); + } + return v; +} + +module.exports = handleBitwiseOperator; + +}).call(this,require("buffer").Buffer) +},{"../../error/cast":13,"buffer":84}],38:[function(require,module,exports){ /*! - * Inherits from SchemaType. + * Module requirements. */ -SchemaString.prototype = Object.create( SchemaType.prototype ); -SchemaString.prototype.constructor = SchemaString; -/** - * Adds an enum validator - * - * ####Example: - * - * var states = 'opening open closing closed'.split(' ') - * var s = new Schema({ state: { type: String, enum: states }}) - * var M = db.model('M', s) - * var m = new M({ state: 'invalid' }) - * m.save(function (err) { - * console.error(String(err)) // ValidationError: `invalid` is not a valid enum value for path `state`. - * m.state = 'open' - * m.save(callback) // success - * }) - * - * // or with custom error messages - * var enu = { - * values: 'opening open closing closed'.split(' '), - * message: 'enum validator failed for path `{PATH}` with value `{VALUE}`' - * } - * var s = new Schema({ state: { type: String, enum: enu }) - * var M = db.model('M', s) - * var m = new M({ state: 'invalid' }) - * m.save(function (err) { - * console.error(String(err)) // ValidationError: enum validator failed for path `state` with value `invalid` - * m.state = 'open' - * m.save(callback) // success - * }) - * - * @param {String|Object} [args...] enumeration values - * @return {SchemaType} this - * @see Customized Error Messages #error_messages_MongooseError-messages - * @api public +var castArraysOfNumbers = require('./helpers').castArraysOfNumbers; +var castToNumber = require('./helpers').castToNumber; + +/*! + * ignore */ -SchemaString.prototype.enum = function() { - if (this.enumValidator) { - this.validators = this.validators.filter(function(v) { - return v.validator != this.enumValidator; - }, this); - this.enumValidator = false; - } +exports.cast$geoIntersects = cast$geoIntersects; +exports.cast$near = cast$near; +exports.cast$within = cast$within; - if (undefined === arguments[0] || false === arguments[0]) { - return this; +function cast$near(val) { + var SchemaArray = require('../array'); + + if (Array.isArray(val)) { + castArraysOfNumbers(val, this); + return val; } - var values; - var errorMessage; + _castMinMaxDistance(this, val); - if (utils.isObject(arguments[0])) { - values = arguments[0].values; - errorMessage = arguments[0].message; - } else { - values = arguments; - errorMessage = errorMessages.String.enum; + if (val && val.$geometry) { + return cast$geometry(val, this); } - for (var i = 0; i < values.length; i++) { - if (undefined !== values[i]) { - this.enumValues.push(this.cast(values[i])); - } + return SchemaArray.prototype.castForQuery.call(this, val); +} + +function cast$geometry(val, self) { + switch (val.$geometry.type) { + case 'Polygon': + case 'LineString': + case 'Point': + castArraysOfNumbers(val.$geometry.coordinates, self); + break; + default: + // ignore unknowns + break; } - var vals = this.enumValues; - this.enumValidator = function(v) { - return undefined === v || ~vals.indexOf(v); - }; - this.validators.push({ - validator: this.enumValidator, - message: errorMessage, - type: 'enum', - enumValues: vals - }); + _castMinMaxDistance(this, val); - return this; -}; + return val; +} -/** - * Adds a lowercase setter. - * - * ####Example: - * - * var s = new Schema({ email: { type: String, lowercase: true }}) - * var M = db.model('M', s); - * var m = new M({ email: 'SomeEmail@example.COM' }); - * console.log(m.email) // someemail@example.com - * - * @api public - * @return {SchemaType} this +function cast$within(val) { + _castMinMaxDistance(this, val); + + if (val.$box || val.$polygon) { + var type = val.$box ? '$box' : '$polygon'; + val[type].forEach(function(arr) { + if (!Array.isArray(arr)) { + var msg = 'Invalid $within $box argument. ' + + 'Expected an array, received ' + arr; + throw new TypeError(msg); + } + arr.forEach(function(v, i) { + arr[i] = castToNumber.call(this, v); + }); + }); + } else if (val.$center || val.$centerSphere) { + type = val.$center ? '$center' : '$centerSphere'; + val[type].forEach(function(item, i) { + if (Array.isArray(item)) { + item.forEach(function(v, j) { + item[j] = castToNumber.call(this, v); + }); + } else { + val[type][i] = castToNumber.call(this, item); + } + }); + } else if (val.$geometry) { + cast$geometry(val, this); + } + + return val; +} + +function cast$geoIntersects(val) { + var geo = val.$geometry; + if (!geo) { + return; + } + + cast$geometry(val, this); + return val; +} + +function _castMinMaxDistance(self, val) { + if (val.$maxDistance) { + val.$maxDistance = castToNumber.call(self, val.$maxDistance); + } + if (val.$minDistance) { + val.$minDistance = castToNumber.call(self, val.$minDistance); + } +} + +},{"../array":27,"./helpers":39}],39:[function(require,module,exports){ +'use strict'; + +/*! + * Module requirements. + */ + +var Types = { + Number: require('../number') +}; + +/*! + * @ignore + */ + +exports.castToNumber = castToNumber; +exports.castArraysOfNumbers = castArraysOfNumbers; + +/*! + * @ignore + */ + +function castToNumber(val) { + return Types.Number.prototype.cast.call(this, val); +} + +function castArraysOfNumbers(arr, self) { + arr.forEach(function(v, i) { + if (Array.isArray(v)) { + castArraysOfNumbers(v, self); + } else { + arr[i] = castToNumber.call(self, v); + } + }); +} + +},{"../number":35}],40:[function(require,module,exports){ +(function (Buffer){ +/*! + * Module dependencies. + */ + +var SchemaType = require('../schematype'); +var CastError = SchemaType.CastError; +var MongooseError = require('../error'); +var utils = require('../utils'); +var Document; + +/** + * String SchemaType constructor. + * + * @param {String} key + * @param {Object} options + * @inherits SchemaType + * @api public + */ + +function SchemaString(key, options) { + this.enumValues = []; + this.regExp = null; + SchemaType.call(this, key, options, 'String'); +} + +/** + * This schema type's name, to defend against minifiers that mangle + * function names. + * + * @api public + */ +SchemaString.schemaName = 'String'; + +/*! + * Inherits from SchemaType. + */ +SchemaString.prototype = Object.create(SchemaType.prototype); +SchemaString.prototype.constructor = SchemaString; + +/** + * Adds an enum validator + * + * ####Example: + * + * var states = 'opening open closing closed'.split(' ') + * var s = new Schema({ state: { type: String, enum: states }}) + * var M = db.model('M', s) + * var m = new M({ state: 'invalid' }) + * m.save(function (err) { + * console.error(String(err)) // ValidationError: `invalid` is not a valid enum value for path `state`. + * m.state = 'open' + * m.save(callback) // success + * }) + * + * // or with custom error messages + * var enu = { + * values: 'opening open closing closed'.split(' '), + * message: 'enum validator failed for path `{PATH}` with value `{VALUE}`' + * } + * var s = new Schema({ state: { type: String, enum: enu }) + * var M = db.model('M', s) + * var m = new M({ state: 'invalid' }) + * m.save(function (err) { + * console.error(String(err)) // ValidationError: enum validator failed for path `state` with value `invalid` + * m.state = 'open' + * m.save(callback) // success + * }) + * + * @param {String|Object} [args...] enumeration values + * @return {SchemaType} this + * @see Customized Error Messages #error_messages_MongooseError-messages + * @api public + */ + +SchemaString.prototype.enum = function() { + if (this.enumValidator) { + this.validators = this.validators.filter(function(v) { + return v.validator !== this.enumValidator; + }, this); + this.enumValidator = false; + } + + if (arguments[0] === void 0 || arguments[0] === false) { + return this; + } + + var values; + var errorMessage; + + if (utils.isObject(arguments[0])) { + values = arguments[0].values; + errorMessage = arguments[0].message; + } else { + values = arguments; + errorMessage = MongooseError.messages.String.enum; + } + + for (var i = 0; i < values.length; i++) { + if (undefined !== values[i]) { + this.enumValues.push(this.cast(values[i])); + } + } + + var vals = this.enumValues; + this.enumValidator = function(v) { + return undefined === v || ~vals.indexOf(v); + }; + this.validators.push({ + validator: this.enumValidator, + message: errorMessage, + type: 'enum', + enumValues: vals + }); + + return this; +}; + +/** + * Adds a lowercase setter. + * + * ####Example: + * + * var s = new Schema({ email: { type: String, lowercase: true }}) + * var M = db.model('M', s); + * var m = new M({ email: 'SomeEmail@example.COM' }); + * console.log(m.email) // someemail@example.com + * + * @api public + * @return {SchemaType} this */ SchemaString.prototype.lowercase = function() { return this.set(function(v, self) { - if ('string' != typeof v) v = self.cast(v); - if (v) return v.toLowerCase(); + if (typeof v !== 'string') { + v = self.cast(v); + } + if (v) { + return v.toLowerCase(); + } return v; }); }; @@ -6936,8 +8117,12 @@ SchemaString.prototype.lowercase = function() { SchemaString.prototype.uppercase = function() { return this.set(function(v, self) { - if ('string' != typeof v) v = self.cast(v); - if (v) return v.toUpperCase(); + if (typeof v !== 'string') { + v = self.cast(v); + } + if (v) { + return v.toUpperCase(); + } return v; }); }; @@ -6960,10 +8145,17 @@ SchemaString.prototype.uppercase = function() { * @return {SchemaType} this */ -SchemaString.prototype.trim = function() { +SchemaString.prototype.trim = function(shouldTrim) { + if (arguments.length > 0 && !shouldTrim) { + return this; + } return this.set(function(v, self) { - if ('string' != typeof v) v = self.cast(v); - if (v) return v.trim(); + if (typeof v !== 'string') { + v = self.cast(v); + } + if (v) { + return v.trim(); + } return v; }); }; @@ -7002,12 +8194,12 @@ SchemaString.prototype.trim = function() { SchemaString.prototype.minlength = function(value, message) { if (this.minlengthValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.minlengthValidator; + return v.validator !== this.minlengthValidator; }, this); } - if (null != value) { - var msg = message || errorMessages.String.minlength; + if (value !== null && value !== undefined) { + var msg = message || MongooseError.messages.String.minlength; msg = msg.replace(/{MINLENGTH}/, value); this.validators.push({ validator: this.minlengthValidator = function(v) { @@ -7056,12 +8248,12 @@ SchemaString.prototype.minlength = function(value, message) { SchemaString.prototype.maxlength = function(value, message) { if (this.maxlengthValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.maxlengthValidator; + return v.validator !== this.maxlengthValidator; }, this); } - if (null != value) { - var msg = message || errorMessages.String.maxlength; + if (value !== null && value !== undefined) { + var msg = message || MongooseError.messages.String.maxlength; msg = msg.replace(/{MAXLENGTH}/, value); this.validators.push({ validator: this.maxlengthValidator = function(v) { @@ -7117,16 +8309,16 @@ SchemaString.prototype.maxlength = function(value, message) { SchemaString.prototype.match = function match(regExp, message) { // yes, we allow multiple match validators - var msg = message || errorMessages.String.match; + var msg = message || MongooseError.messages.String.match; var matchValidator = function(v) { if (!regExp) { return false; } - var ret = ((null != v && '' !== v) - ? regExp.test(v) - : true); + var ret = ((v != null && v !== '') + ? regExp.test(v) + : true); return ret; }; @@ -7140,18 +8332,19 @@ SchemaString.prototype.match = function match(regExp, message) { }; /** - * Check required + * Check if the given value satisfies a required validator. * - * @param {String|null|undefined} value - * @api private + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ SchemaString.prototype.checkRequired = function checkRequired(value, doc) { if (SchemaType._isRef(this, value, doc, true)) { - return null != value; - } else { - return (value instanceof String || typeof value == 'string') && value.length; + return !!value; } + return (value instanceof String || typeof value === 'string') && value.length; }; /** @@ -7164,7 +8357,7 @@ SchemaString.prototype.cast = function(value, doc, init) { if (SchemaType._isRef(this, value, doc, init)) { // wait! we may need to cast this to a document - if (null == value) { + if (value === null || value === undefined) { return value; } @@ -7177,7 +8370,7 @@ SchemaString.prototype.cast = function(value, doc, init) { } // setting a populated path - if ('string' == typeof value) { + if (typeof value === 'string') { return value; } else if (Buffer.isBuffer(value) || !utils.isObject(value)) { throw new CastError('string', value, this.path); @@ -7195,13 +8388,13 @@ SchemaString.prototype.cast = function(value, doc, init) { } // If null or undefined - if (value == null) { + if (value === null || value === undefined) { return value; } - if ('undefined' !== typeof value) { + if (typeof value !== 'undefined') { // handle documents being passed - if (value._id && 'string' == typeof value._id) { + if (value._id && typeof value._id === 'string') { return value._id; } @@ -7225,25 +8418,26 @@ function handleSingle(val) { } function handleArray(val) { - var self = this; + var _this = this; if (!Array.isArray(val)) { return [this.castForQuery(val)]; } return val.map(function(m) { - return self.castForQuery(m); + return _this.castForQuery(m); }); } SchemaString.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$all': handleArray, - '$gt' : handleSingle, - '$gte': handleSingle, - '$lt' : handleSingle, - '$lte': handleSingle, - '$options': handleSingle, - '$regex': handleSingle - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $all: handleArray, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle, + $options: handleSingle, + $regex: handleSingle, + $not: handleSingle + }); /** * Casts contents for queries. @@ -7257,14 +8451,16 @@ SchemaString.prototype.castForQuery = function($conditional, val) { var handler; if (arguments.length === 2) { handler = this.$conditionalHandlers[$conditional]; - if (!handler) - throw new Error("Can't use " + $conditional + " with String."); + if (!handler) { + throw new Error('Can\'t use ' + $conditional + ' with String.'); + } return handler.call(this, val); - } else { - val = $conditional; - if (val instanceof RegExp) return val; - return this.cast(val); } + val = $conditional; + if (Object.prototype.toString.call(val) === '[object RegExp]') { + return val; + } + return this.cast(val); }; /*! @@ -7274,17 +8470,16 @@ SchemaString.prototype.castForQuery = function($conditional, val) { module.exports = SchemaString; }).call(this,require("buffer").Buffer) -},{"../error":12,"../schematype":36,"../utils":45,"./../document":5,"buffer":49}],36:[function(require,module,exports){ +},{"../error":12,"../schematype":41,"../utils":51,"./../document":5,"buffer":84}],41:[function(require,module,exports){ (function (Buffer){ /*! * Module dependencies. */ var utils = require('./utils'); -var error = require('./error'); -var errorMessages = error.messages; -var CastError = error.CastError; -var ValidatorError = error.ValidatorError; +var MongooseError = require('./error'); +var CastError = MongooseError.CastError; +var ValidatorError = MongooseError.ValidatorError; /** * SchemaType constructor @@ -7306,13 +8501,15 @@ function SchemaType(path, options, instance) { this.selected; for (var i in options) { - if (this[i] && 'function' == typeof this[i]) { + if (this[i] && typeof this[i] === 'function') { // { unique: true, index: true } - if ('index' == i && this._index) continue; + if (i === 'index' && this._index) { + continue; + } var opts = Array.isArray(options[i]) - ? options[i] - : [options[i]]; + ? options[i] + : [options[i]]; this[i].apply(this, opts); } @@ -7363,11 +8560,13 @@ function SchemaType(path, options, instance) { */ SchemaType.prototype.default = function(val) { - if (1 === arguments.length) { - this.defaultValue = typeof val === 'function' - ? val - : this.cast(val); - return this; + if (arguments.length === 1) { + if (val === void 0) { + this.defaultValue = void 0; + return void 0; + } + this.defaultValue = val; + return this.defaultValue; } else if (arguments.length > 1) { this.defaultValue = utils.args(arguments); } @@ -7421,10 +8620,17 @@ SchemaType.prototype.index = function(options) { */ SchemaType.prototype.unique = function(bool) { - if (null == this._index || 'boolean' == typeof this._index) { + if (this._index === false) { + if (!bool) { + return; + } + throw new Error('Path "' + this.path + '" may not have `index` set to ' + + 'false and `unique` set to true'); + } + if (this._index == null || this._index === true) { this._index = {}; - } else if ('string' == typeof this._index) { - this._index = { type: this._index }; + } else if (typeof this._index === 'string') { + this._index = {type: this._index}; } this._index.unique = bool; @@ -7438,16 +8644,17 @@ SchemaType.prototype.unique = function(bool) { * * var s = new Schema({name : {type: String, text : true }) * Schema.path('name').index({text : true}); - * @param bool + * @param {Boolean} bool * @return {SchemaType} this * @api public */ SchemaType.prototype.text = function(bool) { - if (null == this._index || 'boolean' == typeof this._index) { + if (this._index === null || this._index === undefined || + typeof this._index === 'boolean') { this._index = {}; - } else if ('string' == typeof this._index) { - this._index = { type: this._index }; + } else if (typeof this._index === 'string') { + this._index = {type: this._index}; } this._index.text = bool; @@ -7468,10 +8675,11 @@ SchemaType.prototype.text = function(bool) { */ SchemaType.prototype.sparse = function(bool) { - if (null == this._index || 'boolean' == typeof this._index) { + if (this._index === null || this._index === undefined || + typeof this._index === 'boolean') { this._index = {}; - } else if ('string' == typeof this._index) { - this._index = { type: this._index }; + } else if (typeof this._index === 'string') { + this._index = {type: this._index}; } this._index.sparse = bool; @@ -7484,7 +8692,7 @@ SchemaType.prototype.sparse = function(bool) { * ####Example: * * function capitalize (val) { - * if ('string' != typeof val) val = ''; + * if (typeof val !== 'string') val = ''; * return val.charAt(0).toUpperCase() + val.substring(1); * } * @@ -7552,8 +8760,9 @@ SchemaType.prototype.sparse = function(bool) { */ SchemaType.prototype.set = function(fn) { - if ('function' != typeof fn) + if (typeof fn !== 'function') { throw new TypeError('A setter must be a function.'); + } this.setters.push(fn); return this; }; @@ -7621,8 +8830,9 @@ SchemaType.prototype.set = function(fn) { */ SchemaType.prototype.get = function(fn) { - if ('function' != typeof fn) + if (typeof fn !== 'function') { throw new TypeError('A getter must be a function.'); + } this.getters.push(fn); return this; }; @@ -7662,7 +8872,7 @@ SchemaType.prototype.get = function(fn) { * * ####Error message templates: * - * From the examples above, you may have noticed that error messages support baseic templating. There are a few other template keywords besides `{PATH}` and `{VALUE}` too. To find out more, details are available [here](#error_messages_MongooseError-messages) + * From the examples above, you may have noticed that error messages support basic templating. There are a few other template keywords besides `{PATH}` and `{VALUE}` too. To find out more, details are available [here](#error_messages_MongooseError.messages) * * ####Asynchronous validation: * @@ -7710,7 +8920,7 @@ SchemaType.prototype.get = function(fn) { */ SchemaType.prototype.validate = function(obj, message, type) { - if ('function' == typeof obj || obj && 'RegExp' === utils.getFunctionName(obj.constructor)) { + if (typeof obj === 'function' || obj && utils.getFunctionName(obj.constructor) === 'RegExp') { var properties; if (message instanceof Object && !type) { properties = utils.clone(message); @@ -7720,9 +8930,13 @@ SchemaType.prototype.validate = function(obj, message, type) { properties.validator = obj; properties.type = properties.type || 'user defined'; } else { - if (!message) message = errorMessages.general.default; - if (!type) type = 'user defined'; - properties = { message: message, type: type, validator: obj }; + if (!message) { + message = MongooseError.messages.general.default; + } + if (!type) { + type = 'user defined'; + } + properties = {message: message, type: type, validator: obj}; } this.validators.push(properties); return this; @@ -7734,10 +8948,10 @@ SchemaType.prototype.validate = function(obj, message, type) { for (i = 0, length = arguments.length; i < length; i++) { arg = arguments[i]; - if (!(arg && 'Object' === utils.getFunctionName(arg.constructor))) { + if (!(arg && utils.getFunctionName(arg.constructor) === 'Object')) { var msg = 'Invalid validator. Received (' + typeof arg + ') ' - + arg - + '. See http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate'; + + arg + + '. See http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate'; throw new Error(msg); } @@ -7748,8 +8962,8 @@ SchemaType.prototype.validate = function(obj, message, type) { }; /** - * Adds a required validator to this schematype. The required validator is added - * to the front of the validators array using `unshift()`. + * Adds a required validator to this SchemaType. The validator gets added + * to the front of this SchemaType's validators array using `unshift()`. * * ####Example: * @@ -7767,44 +8981,60 @@ SchemaType.prototype.validate = function(obj, message, type) { * * Schema.path('name').required(true, 'grrr :( '); * + * // or make a path conditionally required based on a function + * var isOver18 = function() { return this.age >= 18; }; + * Schema.path('voterRegistrationId').required(isOver18); + * + * The required validator uses the SchemaType's `checkRequired` function to + * determine whether a given value satisfies the required validator. By default, + * a value satisfies the required validator if `val != null` (that is, if + * the value is not null nor undefined). However, most built-in mongoose schema + * types override the default `checkRequired` function: * * @param {Boolean} required enable/disable the validator * @param {String} [message] optional custom error message * @return {SchemaType} this * @see Customized Error Messages #error_messages_MongooseError-messages + * @see SchemaArray#checkRequired #schema_array_SchemaArray.checkRequired + * @see SchemaBoolean#checkRequired #schema_boolean_SchemaBoolean-checkRequired + * @see SchemaBuffer#checkRequired #schema_buffer_SchemaBuffer.schemaName + * @see SchemaNumber#checkRequired #schema_number_SchemaNumber-min + * @see SchemaObjectId#checkRequired #schema_objectid_ObjectId-auto + * @see SchemaString#checkRequired #schema_string_SchemaString-checkRequired * @api public */ SchemaType.prototype.required = function(required, message) { - if (false === required) { + if (required === false) { this.validators = this.validators.filter(function(v) { - return v.validator != this.requiredValidator; + return v.validator !== this.requiredValidator; }, this); this.isRequired = false; return this; } - var self = this; + var _this = this; this.isRequired = true; this.requiredValidator = function(v) { // in here, `this` refers to the validating document. // no validation when this path wasn't selected in the query. - if ('isSelected' in this && - !this.isSelected(self.path) && - !this.isModified(self.path)) return true; + if ('isSelected' in this && !this.isSelected(_this.path) && !this.isModified(_this.path)) { + return true; + } - return (('function' === typeof required) && !required.apply(this)) || - self.checkRequired(v, this); + return ((typeof required === 'function') && !required.apply(this)) || + _this.checkRequired(v, this); }; + this.originalRequiredValue = required; - if ('string' == typeof required) { + if (typeof required === 'string') { message = required; required = undefined; } - var msg = message || errorMessages.general.required; + var msg = message || MongooseError.messages.general.required; this.validators.unshift({ validator: this.requiredValidator, message: msg, @@ -7823,15 +9053,18 @@ SchemaType.prototype.required = function(required, message) { */ SchemaType.prototype.getDefault = function(scope, init) { - var ret = 'function' === typeof this.defaultValue - ? this.defaultValue.call(scope) - : this.defaultValue; + var ret = typeof this.defaultValue === 'function' + ? this.defaultValue.call(scope) + : this.defaultValue; - if (null !== ret && undefined !== ret) { - return this.cast(ret, scope, init); - } else { - return ret; + if (ret !== null && ret !== undefined) { + var casted = this.cast(ret, scope, init); + if (casted && casted.$isSingleNested) { + casted.$parent = scope; + } + return casted; } + return ret; }; /** @@ -7843,7 +9076,7 @@ SchemaType.prototype.getDefault = function(scope, init) { * @api private */ -SchemaType.prototype.applySetters = function(value, scope, init, priorVal) { +SchemaType.prototype.applySetters = function(value, scope, init, priorVal, options) { var v = value, setters = this.setters, len = setters.length, @@ -7861,10 +9094,12 @@ SchemaType.prototype.applySetters = function(value, scope, init, priorVal) { v = newVal; } - if (null === v || undefined === v) return v; + if (v === null || v === undefined) { + return v; + } // do not cast until all setters are applied #665 - v = this.cast(v, scope, init, priorVal); + v = this.cast(v, scope, init, priorVal, options); return v; }; @@ -7929,19 +9164,23 @@ SchemaType.prototype.doValidate = function(value, fn, scope) { path = this.path, count = this.validators.length; - if (!count) return fn(null); + if (!count) { + return fn(null); + } var validate = function(ok, validatorProperties) { - if (err) return; - if (ok === undefined || ok) { - --count || fn(null); + if (err) { + return; + } + if (ok === undefined || ok) { + --count || fn(null); } else { err = new ValidatorError(validatorProperties); fn(err); } }; - var self = this; + var _this = this; this.validators.forEach(function(v) { if (err) { return; @@ -7955,18 +9194,24 @@ SchemaType.prototype.doValidate = function(value, fn, scope) { if (validator instanceof RegExp) { validate(validator.test(value), validatorProperties); - } else if ('function' === typeof validator) { - if (value === undefined && !self.isRequired) { + } else if (typeof validator === 'function') { + if (value === undefined && !_this.isRequired) { validate(true, validatorProperties); return; } - if (2 === validator.length) { - validator.call(scope, value, function(ok, customMsg) { + if (validator.length === 2) { + var returnVal = validator.call(scope, value, function(ok, customMsg) { + if (typeof returnVal === 'boolean') { + return; + } if (customMsg) { validatorProperties.message = customMsg; } validate(ok, validatorProperties); }); + if (typeof returnVal === 'boolean') { + validate(returnVal, validatorProperties); + } } else { validate(validator.call(scope, value), validatorProperties); } @@ -7992,17 +9237,21 @@ SchemaType.prototype.doValidateSync = function(value, scope) { path = this.path, count = this.validators.length; - if (!count) return null; + if (!count) { + return null; + } var validate = function(ok, validatorProperties) { - if (err) return; + if (err) { + return; + } if (ok !== undefined && !ok) { err = new ValidatorError(validatorProperties); } }; - var self = this; - if (value === undefined && !self.isRequired) { + var _this = this; + if (value === undefined && !_this.isRequired) { return null; } @@ -8018,9 +9267,9 @@ SchemaType.prototype.doValidateSync = function(value, scope) { if (validator instanceof RegExp) { validate(validator.test(value), validatorProperties); - } else if ('function' === typeof validator) { + } else if (typeof validator === 'function') { // if not async validators - if (2 !== validator.length) { + if (validator.length !== 2) { validate(validator.call(scope, value), validatorProperties); } } @@ -8054,11 +9303,13 @@ SchemaType._isRef = function(self, value, doc, init) { } if (ref) { - if (null == value) return true; + if (value == null) { + return true; + } if (!Buffer.isBuffer(value) && // buffers are objects too - 'Binary' != value._bsontype // raw binary value from the db + value._bsontype !== 'Binary' // raw binary value from the db && utils.isObject(value) // might have deselected _id in population query - ) { + ) { return true; } } @@ -8093,11 +9344,11 @@ function handleArray(val) { */ SchemaType.prototype.$conditionalHandlers = { - '$all': handleArray, - '$eq': handleSingle, - '$in' : handleArray, - '$ne' : handleSingle, - '$nin': handleArray + $all: handleArray, + $eq: handleSingle, + $in: handleArray, + $ne: handleSingle, + $nin: handleArray }; /** @@ -8116,10 +9367,20 @@ SchemaType.prototype.castForQuery = function($conditional, val) { throw new Error('Can\'t use ' + $conditional); } return handler.call(this, val); - } else { - val = $conditional; - return this.cast(val); } + val = $conditional; + return this.cast(val); +}; + +/** + * Default check for if this path satisfies the `required` validator. + * + * @param {any} val + * @api private + */ + +SchemaType.prototype.checkRequired = function(val) { + return val != null; }; /*! @@ -8133,7 +9394,98 @@ exports.CastError = CastError; exports.ValidatorError = ValidatorError; }).call(this,require("buffer").Buffer) -},{"./error":12,"./utils":45,"buffer":49}],37:[function(require,module,exports){ +},{"./error":12,"./utils":51,"buffer":84}],42:[function(require,module,exports){ +(function (Buffer){ +'use strict'; + +/*! + * Module dependencies. + */ + +var ObjectId = require('../types/objectid'); +var utils = require('../utils'); + +exports.flatten = flatten; +exports.modifiedPaths = modifiedPaths; + +/*! + * ignore + */ + +function flatten(update, path, options) { + var keys; + if (update && utils.isMongooseObject(update) && !Buffer.isBuffer(update)) { + keys = Object.keys(update.toObject({ transform: false })); + } else { + keys = Object.keys(update || {}); + } + + var numKeys = keys.length; + var result = {}; + path = path ? path + '.' : ''; + + for (var i = 0; i < numKeys; ++i) { + var key = keys[i]; + var val = update[key]; + result[path + key] = val; + if (shouldFlatten(val)) { + if (options && options.skipArrays && Array.isArray(val)) { + continue; + } + var flat = flatten(val, path + key); + for (var k in flat) { + result[k] = flat[k]; + } + if (Array.isArray(val)) { + result[path + key] = val; + } + } + } + + return result; +} + +/*! + * ignore + */ + +function modifiedPaths(update, path, result) { + var keys = Object.keys(update || {}); + var numKeys = keys.length; + result = result || {}; + path = path ? path + '.' : ''; + + for (var i = 0; i < numKeys; ++i) { + var key = keys[i]; + var val = update[key]; + + result[path + key] = true; + if (utils.isMongooseObject(val) && !Buffer.isBuffer(val)) { + val = val.toObject({ transform: false }); + } + if (shouldFlatten(val)) { + modifiedPaths(val, path + key, result); + } + } + + return result; +} + +/*! + * ignore + */ + +function shouldFlatten(val) { + return val && + typeof val === 'object' && + !(val instanceof Date) && + !(val instanceof ObjectId) && + (!Array.isArray(val) || val.length > 0) && + !(val instanceof Buffer); +} + +}).call(this,require("buffer").Buffer) +},{"../types/objectid":49,"../utils":51,"buffer":84}],43:[function(require,module,exports){ /*! * Module dependencies. @@ -8238,10 +9590,10 @@ StateMachine.prototype.clear = function clear(state) { */ StateMachine.prototype.some = function some() { - var self = this; + var _this = this; var what = arguments.length ? arguments : this.stateNames; return Array.prototype.some.call(what, function(state) { - return Object.keys(self.states[state]).length; + return Object.keys(_this.states[state]).length; }); }; @@ -8262,10 +9614,10 @@ StateMachine.prototype._iter = function _iter(iterMethod) { if (!states.length) states = this.stateNames; - var self = this; + var _this = this; var paths = states.reduce(function(paths, state) { - return paths.concat(Object.keys(self.states[state])); + return paths.concat(Object.keys(_this.states[state])); }, []); return paths[iterMethod](function(path, i, paths) { @@ -8313,7 +9665,7 @@ StateMachine.prototype.map = function map() { return this.map.apply(this, arguments); }; -},{"./utils":45}],38:[function(require,module,exports){ +},{"./utils":51}],44:[function(require,module,exports){ (function (Buffer){ /*! * Module dependencies. @@ -8343,19 +9695,17 @@ var isMongooseObject = utils.isMongooseObject; function MongooseArray(values, path, doc) { var arr = [].concat(values); - utils.decorate( arr, MongooseArray.mixin ); - arr.isMongooseArray = true; - - var _options = { enumerable: false, configurable: true, writable: true }; - var keys = Object.keys(MongooseArray.mixin). - concat(['isMongooseArray', 'validators', '_path']); - for (var i = 0; i < keys.length; ++i) { - Object.defineProperty(arr, keys[i], _options); + var keysMA = Object.keys(MongooseArray.mixin); + var numKeys = keysMA.length; + for (var i = 0; i < numKeys; ++i) { + arr[keysMA[i]] = MongooseArray.mixin[keysMA[i]]; } - arr._atomics = {}; - arr.validators = []; arr._path = path; + arr.isMongooseArray = true; + arr.validators = []; + arr._atomics = {}; + arr._schema = void 0; // Because doc comes from the context of another function, doc === global // can happen if there was a null somewhere up the chain (see #3020) @@ -8370,6 +9720,12 @@ function MongooseArray(values, path, doc) { } MongooseArray.mixin = { + /*! + * ignore + */ + toBSON: function() { + return this.toObject({ transform: false }); + }, /** * Stores a queue of atomic operations to perform @@ -8401,23 +9757,14 @@ MongooseArray.mixin = { */ _cast: function(value) { - var owner = this._owner; var populated = false; var Model; if (this._parent) { - // if a populated array, we must cast to the same model - // instance as specified in the original query. - if (!owner) { - owner = this._owner = this._parent.ownerDocument - ? this._parent.ownerDocument() - : this._parent; - } - - populated = owner.populated(this._path, true); + populated = this._parent.populated(this._path, true); } - if (populated && null != value) { + if (populated && value !== null && value !== undefined) { // cast to the populated Models schema Model = populated.options.model; @@ -8425,13 +9772,13 @@ MongooseArray.mixin = { // non-objects are to be interpreted as _id if (Buffer.isBuffer(value) || value instanceof ObjectId || !utils.isObject(value)) { - value = { _id: value }; + value = {_id: value}; } // gh-2399 // we should cast model only when it's not a discriminator var isDisc = value.schema && value.schema.discriminatorMapping && - value.schema.discriminatorMapping.key !== undefined; + value.schema.discriminatorMapping.key !== undefined; if (!isDisc) { value = new Model(value); } @@ -8461,7 +9808,7 @@ MongooseArray.mixin = { dirtyPath = this._path; if (arguments.length) { - if (null != embeddedPath) { + if (embeddedPath != null) { // an embedded doc bubbled up the change dirtyPath = dirtyPath + '.' + this.indexOf(elem) + '.' + embeddedPath; } else { @@ -8487,20 +9834,20 @@ MongooseArray.mixin = { */ _registerAtomic: function(op, val) { - if ('$set' == op) { + if (op === '$set') { // $set takes precedence over all other ops. // mark entire array modified. - this._atomics = { $set: val }; + this._atomics = {$set: val}; return this; } var atomics = this._atomics; // reset pop/shift after save - if ('$pop' == op && !('$pop' in atomics)) { - var self = this; + if (op === '$pop' && !('$pop' in atomics)) { + var _this = this; this._parent.once('save', function() { - self._popped = self._shifted = null; + _this._popped = _this._shifted = null; }); } @@ -8510,7 +9857,7 @@ MongooseArray.mixin = { Object.keys(atomics).length && !(op in atomics)) { // a different op was previously registered. // save the entire thing. - this._atomics = { $set: this }; + this._atomics = {$set: this}; return this; } @@ -8524,10 +9871,10 @@ MongooseArray.mixin = { if (val[0] instanceof EmbeddedDocument) { selector = pullOp['$or'] || (pullOp['$or'] = []); Array.prototype.push.apply(selector, val.map(function(v) { - return v.toObject({ virtuals: false }); + return v.toObject({transform: false}); })); } else { - selector = pullOp['_id'] || (pullOp['_id'] = {'$in' : [] }); + selector = pullOp['_id'] || (pullOp['_id'] = {$in: []}); selector['$in'] = selector['$in'].concat(val); } } else { @@ -8553,8 +9900,8 @@ MongooseArray.mixin = { var keys = Object.keys(this._atomics); var i = keys.length; - if (0 === i) { - ret[0] = ['$set', this.toObject({ depopulate: 1, transform: false })]; + if (i === 0) { + ret[0] = ['$set', this.toObject({depopulate: 1, transform: false})]; return ret; } @@ -8566,15 +9913,15 @@ MongooseArray.mixin = { // need to convert their elements as if they were MongooseArrays // to handle populated arrays versus DocumentArrays properly. if (isMongooseObject(val)) { - val = val.toObject({ depopulate: 1, transform: false }); + val = val.toObject({depopulate: 1, transform: false}); } else if (Array.isArray(val)) { - val = this.toObject.call(val, { depopulate: 1, transform: false }); + val = this.toObject.call(val, {depopulate: 1, transform: false}); } else if (val.valueOf) { val = val.valueOf(); } - if ('$addToSet' == op) { - val = { $each: val }; + if (op === '$addToSet') { + val = {$each: val}; } ret.push([op, val]); @@ -8593,7 +9940,7 @@ MongooseArray.mixin = { */ hasAtomics: function hasAtomics() { - if (!(this._atomics && 'Object' === this._atomics.constructor.name)) { + if (!(this._atomics && this._atomics.constructor.name === 'Object')) { return 0; } @@ -8623,7 +9970,8 @@ MongooseArray.mixin = { push: function() { var values = [].map.call(arguments, this._mapCast, this); - values = this._schema.applySetters(values, this._parent); + values = this._schema.applySetters(values, this._parent, undefined, + undefined, {skipDocumentArrayCast: true}); var ret = [].push.apply(this, values); // $pushAll might be fibbed (could be $push). But it makes it easier to @@ -8694,7 +10042,9 @@ MongooseArray.mixin = { this._markModified(); // only allow popping once - if (this._popped) return; + if (this._popped) { + return; + } this._popped = true; return [].pop.call(this); @@ -8758,7 +10108,9 @@ MongooseArray.mixin = { this._markModified(); // only allow shifting once - if (this._shifted) return; + if (this._shifted) { + return; + } this._shifted = true; return [].shift.call(this); @@ -8812,6 +10164,8 @@ MongooseArray.mixin = { * doc.subdocs.push({ _id: 4815162342 }) * doc.subdocs.pull(4815162342); // works * + * The first pull call will result in a atomic operation on the database, if pull is called repeatedly without saving the document, a $set operation is used on the complete array instead, overwriting possible changes that happened on the database in the meantime. + * * @param {any} [args...] * @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull * @api public @@ -8827,8 +10181,11 @@ MongooseArray.mixin = { while (i--) { mem = cur[i]; - if (mem instanceof EmbeddedDocument) { - if (values.some(function(v) { return v.equals(mem); } )) { + if (mem instanceof Document) { + var some = values.some(function(v) { + return mem.equals(v); + }); + if (some) { [].splice.call(cur, i, 1); } } else if (~cur.indexOf.call(values, mem)) { @@ -8867,8 +10224,8 @@ MongooseArray.mixin = { vals = []; for (i = 0; i < arguments.length; ++i) { vals[i] = i < 2 - ? arguments[i] - : this._cast(arguments[i], arguments[0] + (i - 2)); + ? arguments[i] + : this._cast(arguments[i], arguments[0] + (i - 2)); } ret = [].splice.apply(this, vals); this._registerAtomic('$set', this); @@ -8939,19 +10296,26 @@ MongooseArray.mixin = { var values = [].map.call(arguments, this._mapCast, this); values = this._schema.applySetters(values, this._parent); var added = []; - var type = values[0] instanceof EmbeddedDocument ? 'doc' : - values[0] instanceof Date ? 'date' : - ''; + var type = ''; + if (values[0] instanceof EmbeddedDocument) { + type = 'doc'; + } else if (values[0] instanceof Date) { + type = 'date'; + } values.forEach(function(v) { var found; switch (type) { case 'doc': - found = this.some(function(doc) { return doc.equals(v); }); + found = this.some(function(doc) { + return doc.equals(v); + }); break; case 'date': var val = +v; - found = this.some(function(d) { return +d === val; }); + found = this.some(function(d) { + return +d === val; + }); break; default: found = ~this.indexOf(v); @@ -8998,9 +10362,9 @@ MongooseArray.mixin = { set: function set(i, val) { var value = this._cast(val, i); value = this._schema.caster instanceof EmbeddedDocument ? - value : - this._schema.caster.applySetters(val, this._parent) - ; + value : + this._schema.caster.applySetters(val, this._parent) + ; this[i] = value; this._markModified(i); return this; @@ -9020,8 +10384,8 @@ MongooseArray.mixin = { if (options && options.depopulate) { return this.map(function(doc) { return doc instanceof Document - ? doc.toObject(options) - : doc; + ? doc.toObject(options) + : doc; }); } @@ -9051,10 +10415,13 @@ MongooseArray.mixin = { */ indexOf: function indexOf(obj) { - if (obj instanceof ObjectId) obj = obj.toString(); + if (obj instanceof ObjectId) { + obj = obj.toString(); + } for (var i = 0, len = this.length; i < len; ++i) { - if (obj == this[i]) + if (obj == this[i]) { return i; + } } return -1; } @@ -9079,7 +10446,7 @@ MongooseArray.mixin.remove = MongooseArray.mixin.pull; module.exports = exports = MongooseArray; }).call(this,require("buffer").Buffer) -},{"../document":5,"../utils":45,"./embedded":41,"./objectid":43,"buffer":49}],39:[function(require,module,exports){ +},{"../document":5,"../utils":51,"./embedded":47,"./objectid":49,"buffer":84}],45:[function(require,module,exports){ (function (Buffer){ /*! * Module dependencies. @@ -9105,7 +10472,7 @@ function MongooseBuffer(value, encode, offset) { var length = arguments.length; var val; - if (0 === length || null === arguments[0] || undefined === arguments[0]) { + if (length === 0 || arguments[0] === null || arguments[0] === undefined) { val = 0; } else { val = value; @@ -9124,17 +10491,17 @@ function MongooseBuffer(value, encode, offset) { } var buf = new Buffer(val, encoding, offset); - utils.decorate( buf, MongooseBuffer.mixin ); + utils.decorate(buf, MongooseBuffer.mixin); buf.isMongooseBuffer = true; // make sure these internal props don't show up in Object.keys() Object.defineProperties(buf, { - validators: { value: [] }, - _path: { value: path }, - _parent: { value: doc } + validators: {value: []}, + _path: {value: path}, + _parent: {value: doc} }); - if (doc && "string" === typeof path) { + if (doc && typeof path === 'string') { Object.defineProperty(buf, '_schema', { value: doc.schema.path(path) }); @@ -9148,7 +10515,7 @@ function MongooseBuffer(value, encode, offset) { * Inherit from Buffer. */ -//MongooseBuffer.prototype = new Buffer(0); +// MongooseBuffer.prototype = new Buffer(0); MongooseBuffer.mixin = { @@ -9214,7 +10581,7 @@ MongooseBuffer.mixin = { * * `Buffer#copy` does not mark `target` as modified so you must copy from a `MongooseBuffer` for it to work as expected. This is a work around since `copy` modifies the target, not this. * - * @return {MongooseBuffer} + * @return {Number} The number of bytes copied. * @param {Buffer} target * @method copy * @receiver MongooseBuffer @@ -9237,21 +10604,23 @@ MongooseBuffer.mixin = { ( // node < 0.5 -'writeUInt8 writeUInt16 writeUInt32 writeInt8 writeInt16 writeInt32 ' + -'writeFloat writeDouble fill ' + -'utf8Write binaryWrite asciiWrite set ' + + 'writeUInt8 writeUInt16 writeUInt32 writeInt8 writeInt16 writeInt32 ' + + 'writeFloat writeDouble fill ' + + 'utf8Write binaryWrite asciiWrite set ' + // node >= 0.5 -'writeUInt16LE writeUInt16BE writeUInt32LE writeUInt32BE ' + -'writeInt16LE writeInt16BE writeInt32LE writeInt32BE ' + -'writeFloatLE writeFloatBE writeDoubleLE writeDoubleBE' + 'writeUInt16LE writeUInt16BE writeUInt32LE writeUInt32BE ' + + 'writeInt16LE writeInt16BE writeInt32LE writeInt32BE ' + + 'writeFloatLE writeFloatBE writeDoubleLE writeDoubleBE' ).split(' ').forEach(function(method) { - if (!Buffer.prototype[method]) return; - MongooseBuffer.mixin[method] = new Function( - 'var ret = Buffer.prototype.' + method + '.apply(this, arguments);' + - 'this._markModified();' + - 'return ret;' - ); + if (!Buffer.prototype[method]) { + return; + } + MongooseBuffer.mixin[method] = function() { + var ret = Buffer.prototype[method].apply(this, arguments); + this._markModified(); + return ret; + }; }); /** @@ -9278,9 +10647,9 @@ MongooseBuffer.mixin = { */ MongooseBuffer.mixin.toObject = function(options) { - var subtype = 'number' == typeof options - ? options - : (this._subtype || 0); + var subtype = typeof options === 'number' + ? options + : (this._subtype || 0); return new Binary(this, subtype); }; @@ -9303,7 +10672,9 @@ MongooseBuffer.mixin.equals = function(other) { } for (var i = 0; i < this.length; ++i) { - if (this[i] !== other[i]) return false; + if (this[i] !== other[i]) { + return false; + } } return true; @@ -9332,11 +10703,11 @@ MongooseBuffer.mixin.equals = function(other) { */ MongooseBuffer.mixin.subtype = function(subtype) { - if ('number' != typeof subtype) { + if (typeof subtype !== 'number') { throw new TypeError('Invalid subtype. Expected a number'); } - if (this._subtype != subtype) { + if (this._subtype !== subtype) { this._markModified(); } @@ -9352,7 +10723,7 @@ MongooseBuffer.Binary = Binary; module.exports = MongooseBuffer; }).call(this,require("buffer").Buffer) -},{"../drivers":11,"../utils":45,"buffer":49}],40:[function(require,module,exports){ +},{"../drivers":11,"../utils":51,"buffer":84}],46:[function(require,module,exports){ (function (Buffer){ /*! * Module dependencies. @@ -9362,7 +10733,6 @@ var MongooseArray = require('./array'), ObjectId = require('./objectid'), ObjectIdSchema = require('../schema/objectid'), utils = require('../utils'), - util = require('util'), Document = require('../document'); /** @@ -9379,16 +10749,36 @@ var MongooseArray = require('./array'), function MongooseDocumentArray(values, path, doc) { var arr = [].concat(values); + arr._path = path; + + var props = { + isMongooseArray: true, + isMongooseDocumentArray: true, + validators: [], + _atomics: {}, + _schema: void 0, + _handlers: void 0 + }; // Values always have to be passed to the constructor to initialize, since // otherwise MongooseArray#push will mark the array as modified to the parent. - utils.decorate( arr, MongooseDocumentArray.mixin ); - arr.isMongooseArray = true; - arr.isMongooseDocumentArray = true; + var keysMA = Object.keys(MongooseArray.mixin); + var numKeys = keysMA.length; + for (var j = 0; j < numKeys; ++j) { + arr[keysMA[j]] = MongooseArray.mixin[keysMA[j]]; + } - arr._atomics = {}; - arr.validators = []; - arr._path = path; + var keysMDA = Object.keys(MongooseDocumentArray.mixin); + numKeys = keysMDA.length; + for (var i = 0; i < numKeys; ++i) { + arr[keysMDA[i]] = MongooseDocumentArray.mixin[keysMDA[i]]; + } + + var keysP = Object.keys(props); + numKeys = keysP.length; + for (var k = 0; k < numKeys; ++k) { + arr[keysP[k]] = props[keysP[k]]; + } // Because doc comes from the context of another function, doc === global // can happen if there was a null somewhere up the chain (see #3020 && #3034) @@ -9412,165 +10802,179 @@ function MongooseDocumentArray(values, path, doc) { /*! * Inherits from MongooseArray */ -MongooseDocumentArray.mixin = Object.create( MongooseArray.mixin ); +// MongooseDocumentArray.mixin = Object.create( MongooseArray.mixin ); +MongooseDocumentArray.mixin = { + /*! + * ignore + */ + toBSON: function() { + return this.toObject({ transform: false }); + }, -/** - * Overrides MongooseArray#cast - * - * @method _cast - * @api private - * @receiver MongooseDocumentArray - */ + /** + * Overrides MongooseArray#cast + * + * @method _cast + * @api private + * @receiver MongooseDocumentArray + */ -MongooseDocumentArray.mixin._cast = function(value, index) { - if (value instanceof this._schema.casterConstructor) { - if (!(value.__parent && value.__parentArray)) { - // value may have been created using array.create() - value.__parent = this._parent; - value.__parentArray = this; + _cast: function(value, index) { + if (value instanceof this._schema.casterConstructor) { + if (!(value.__parent && value.__parentArray)) { + // value may have been created using array.create() + value.__parent = this._parent; + value.__parentArray = this; + } + value.__index = index; + return value; } - value.__index = index; - return value; - } - - // handle cast('string') or cast(ObjectId) etc. - // only objects are permitted so we can safely assume that - // non-objects are to be interpreted as _id - if (Buffer.isBuffer(value) || - value instanceof ObjectId || !utils.isObject(value)) { - value = { _id: value }; - } - return new this._schema.casterConstructor(value, this, undefined, undefined, index); -}; -/** - * Searches array items for the first document with a matching _id. - * - * ####Example: - * - * var embeddedDoc = m.array.id(some_id); - * - * @return {EmbeddedDocument|null} the subdocument or null if not found. - * @param {ObjectId|String|Number|Buffer} id - * @TODO cast to the _id based on schema for proper comparison - * @method id - * @api public - * @receiver MongooseDocumentArray - */ + if (value === undefined || value === null) { + return null; + } -MongooseDocumentArray.mixin.id = function(id) { - var casted, - sid, - _id; + // handle cast('string') or cast(ObjectId) etc. + // only objects are permitted so we can safely assume that + // non-objects are to be interpreted as _id + if (Buffer.isBuffer(value) || + value instanceof ObjectId || !utils.isObject(value)) { + value = {_id: value}; + } + return new this._schema.casterConstructor(value, this, undefined, undefined, index); + }, - try { - var casted_ = ObjectIdSchema.prototype.cast.call({}, id); - if (casted_) casted = String(casted_); - } catch (e) { - casted = null; - } + /** + * Searches array items for the first document with a matching _id. + * + * ####Example: + * + * var embeddedDoc = m.array.id(some_id); + * + * @return {EmbeddedDocument|null} the subdocument or null if not found. + * @param {ObjectId|String|Number|Buffer} id + * @TODO cast to the _id based on schema for proper comparison + * @method id + * @api public + * @receiver MongooseDocumentArray + */ - for (var i = 0, l = this.length; i < l; i++) { - _id = this[i].get('_id'); + id: function(id) { + var casted, + sid, + _id; - if (_id === null || typeof _id === 'undefined') { - continue; - } else if (_id instanceof Document) { - sid || (sid = String(id)); - if (sid == _id._id) return this[i]; - } else if (!(_id instanceof ObjectId)) { - if (utils.deepEqual(id, _id)) return this[i]; - } else if (casted == _id) { - return this[i]; + try { + var casted_ = ObjectIdSchema.prototype.cast.call({}, id); + if (casted_) { + casted = String(casted_); + } + } catch (e) { + casted = null; } - } - return null; -}; + for (var i = 0, l = this.length; i < l; i++) { + _id = this[i].get('_id'); -/** - * Returns a native js Array of plain js objects - * - * ####NOTE: - * - * _Each sub-document is converted to a plain object by calling its `#toObject` method._ - * - * @param {Object} [options] optional options to pass to each documents `toObject` method call during conversion - * @return {Array} - * @method toObject - * @api public - * @receiver MongooseDocumentArray - */ + if (_id === null || typeof _id === 'undefined') { + continue; + } else if (_id instanceof Document) { + sid || (sid = String(id)); + if (sid == _id._id) { + return this[i]; + } + } else if (!(id instanceof ObjectId) && !(_id instanceof ObjectId)) { + if (utils.deepEqual(id, _id)) { + return this[i]; + } + } else if (casted == _id) { + return this[i]; + } + } -MongooseDocumentArray.mixin.toObject = function(options) { - return this.map(function(doc) { - return doc && doc.toObject(options) || null; - }); -}; + return null; + }, -/** - * Helper for console.log - * - * @method inspect - * @api public - * @receiver MongooseDocumentArray - */ + /** + * Returns a native js Array of plain js objects + * + * ####NOTE: + * + * _Each sub-document is converted to a plain object by calling its `#toObject` method._ + * + * @param {Object} [options] optional options to pass to each documents `toObject` method call during conversion + * @return {Array} + * @method toObject + * @api public + * @receiver MongooseDocumentArray + */ -MongooseDocumentArray.mixin.inspect = function() { - return '[' + Array.prototype.map.call(this, function(doc) { - if (doc) { - return doc.inspect - ? doc.inspect() - : util.inspect(doc); - } - return 'null'; - }).join('\n') + ']'; -}; + toObject: function(options) { + return this.map(function(doc) { + return doc && doc.toObject(options) || null; + }); + }, -/** - * Creates a subdocument casted to this schema. - * - * This is the same subdocument constructor used for casting. - * - * @param {Object} obj the value to cast to this arrays SubDocument schema - * @method create - * @api public - * @receiver MongooseDocumentArray - */ + /** + * Helper for console.log + * + * @method inspect + * @api public + * @receiver MongooseDocumentArray + */ -MongooseDocumentArray.mixin.create = function(obj) { - return new this._schema.casterConstructor(obj); -}; + inspect: function() { + return Array.prototype.slice.call(this); + }, -/** - * Creates a fn that notifies all child docs of `event`. - * - * @param {String} event - * @return {Function} - * @method notify - * @api private - * @receiver MongooseDocumentArray - */ + /** + * Creates a subdocument casted to this schema. + * + * This is the same subdocument constructor used for casting. + * + * @param {Object} obj the value to cast to this arrays SubDocument schema + * @method create + * @api public + * @receiver MongooseDocumentArray + */ -MongooseDocumentArray.mixin.notify = function notify(event) { - var self = this; - return function notify(val) { - var i = self.length; - while (i--) { - if (!self[i]) continue; - switch (event) { - // only swap for save event for now, we may change this to all event types later - case 'save': - val = self[i]; - break; - default: - // NO-OP - break; - } - self[i].emit(event, val); - } - }; -}; + create: function(obj) { + return new this._schema.casterConstructor(obj); + }, + + /** + * Creates a fn that notifies all child docs of `event`. + * + * @param {String} event + * @return {Function} + * @method notify + * @api private + * @receiver MongooseDocumentArray + */ + + notify: function notify(event) { + var _this = this; + return function notify(val) { + var i = _this.length; + while (i--) { + if (!_this[i]) { + continue; + } + switch (event) { + // only swap for save event for now, we may change this to all event types later + case 'save': + val = _this[i]; + break; + default: + // NO-OP + break; + } + _this[i].emit(event, val); + } + }; + } + +}; /*! * Module exports. @@ -9579,7 +10983,7 @@ MongooseDocumentArray.mixin.notify = function notify(event) { module.exports = MongooseDocumentArray; }).call(this,require("buffer").Buffer) -},{"../document":5,"../schema/objectid":34,"../utils":45,"./array":38,"./objectid":43,"buffer":49,"util":57}],41:[function(require,module,exports){ +},{"../document":5,"../schema/objectid":36,"../utils":51,"./array":44,"./objectid":49,"buffer":84}],47:[function(require,module,exports){ /* eslint no-func-assign: 1 */ /*! @@ -9587,7 +10991,6 @@ module.exports = MongooseDocumentArray; */ var Document = require('../document_provider')(); -var inspect = require('util').inspect; var PromiseProvider = require('../promise_provider'); /** @@ -9612,18 +11015,22 @@ function EmbeddedDocument(obj, parentArr, skipId, fields, index) { Document.call(this, obj, fields, skipId); - var self = this; + var _this = this; this.on('isNew', function(val) { - self.isNew = val; + _this.isNew = val; }); } /*! * Inherit from Document */ -EmbeddedDocument.prototype = Object.create( Document.prototype ); +EmbeddedDocument.prototype = Object.create(Document.prototype); EmbeddedDocument.prototype.constructor = EmbeddedDocument; +EmbeddedDocument.prototype.toBSON = function() { + return this.toObject({ transform: false }); +}; + /** * Marks the embedded doc modified. * @@ -9639,9 +11046,11 @@ EmbeddedDocument.prototype.constructor = EmbeddedDocument; */ EmbeddedDocument.prototype.markModified = function(path) { - if (!this.__parentArray) return; - this.$__.activePaths.modify(path); + if (!this.__parentArray) { + return; + } + if (this.isNew) { // Mark the WHOLE parent array as modified // if this is a new document (i.e., we are initializing @@ -9652,6 +11061,16 @@ EmbeddedDocument.prototype.markModified = function(path) { } }; +/*! + * ignore + */ + +EmbeddedDocument.prototype.populate = function() { + throw new Error('Mongoose does not support calling populate() on nested ' + + 'docs. Instead of `doc.arr[0].populate("path")`, use ' + + '`doc.populate("arr.0.path")`'); +}; + /** * Used as a stub for [hooks.js](https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3) * @@ -9672,56 +11091,65 @@ EmbeddedDocument.prototype.save = function(fn) { }); }; +/*! + * Registers remove event listeners for triggering + * on subdocuments. + * + * @param {EmbeddedDocument} sub + * @api private + */ + +function registerRemoveListener(sub) { + var owner = sub.ownerDocument(); + + function emitRemove() { + owner.removeListener('save', emitRemove); + owner.removeListener('remove', emitRemove); + sub.emit('remove', sub); + owner = sub = null; + } + + owner.on('save', emitRemove); + owner.on('remove', emitRemove); +} + /** * Removes the subdocument from its parent array. * + * @param {Object} [options] * @param {Function} [fn] * @api public */ -EmbeddedDocument.prototype.remove = function(fn) { - if (!this.__parentArray) return this; +EmbeddedDocument.prototype.remove = function(options, fn) { + if ( typeof options === 'function' && !fn ) { + fn = options; + options = undefined; + } + if (!this.__parentArray || (options && options.noop)) { + fn && fn(null); + return this; + } var _id; if (!this.willRemove) { _id = this._doc._id; if (!_id) { throw new Error('For your own good, Mongoose does not know ' + - 'how to remove an EmbeddedDocument that has no _id'); + 'how to remove an EmbeddedDocument that has no _id'); } - this.__parentArray.pull({ _id: _id }); + this.__parentArray.pull({_id: _id}); this.willRemove = true; registerRemoveListener(this); } - if (fn) + if (fn) { fn(null); + } return this; }; -/*! - * Registers remove event listeners for triggering - * on subdocuments. - * - * @param {EmbeddedDocument} sub - * @api private - */ - -function registerRemoveListener(sub) { - var owner = sub.ownerDocument(); - - owner.on('save', emitRemove); - owner.on('remove', emitRemove); - - function emitRemove() { - owner.removeListener('save', emitRemove); - owner.removeListener('remove', emitRemove); - sub.emit('remove', sub); - owner = sub = emitRemove = null; - } -} - /** * Override #update method of parent documents. * @api private @@ -9738,7 +11166,7 @@ EmbeddedDocument.prototype.update = function() { */ EmbeddedDocument.prototype.inspect = function() { - return inspect(this.toObject()); + return this.toObject({ transform: false, retainKeyOrder: true }); }; /** @@ -9751,9 +11179,12 @@ EmbeddedDocument.prototype.inspect = function() { */ EmbeddedDocument.prototype.invalidate = function(path, err, val, first) { + Document.prototype.invalidate.call(this, path, err, val); if (!this.__parent) { - var msg = 'Unable to invalidate a subdocument that has not been added to an array.'; - throw new Error(msg); + if (err.name === 'ValidatorError') { + return true; + } + throw err; } var index = this.__index; @@ -9803,10 +11234,9 @@ EmbeddedDocument.prototype.$markValid = function(path) { EmbeddedDocument.prototype.$isValid = function(path) { var index = this.__index; - if (typeof index !== 'undefined') { - + if (typeof index !== 'undefined' && this.__parent) { return !this.__parent.$__.validationError || - !this.__parent.$__.validationError.errors[path]; + !this.__parent.$__.validationError.errors[this.$__fullPath(path)]; } return true; @@ -9824,13 +11254,16 @@ EmbeddedDocument.prototype.ownerDocument = function() { } var parent = this.__parent; - if (!parent) return this; + if (!parent) { + return this; + } - while (parent.__parent) { - parent = parent.__parent; + while (parent.__parent || parent.$parent) { + parent = parent.__parent || parent.$parent; } - return this.$__.ownerDocument = parent; + this.$__.ownerDocument = parent; + return this.$__.ownerDocument; }; /** @@ -9845,13 +11278,19 @@ EmbeddedDocument.prototype.ownerDocument = function() { EmbeddedDocument.prototype.$__fullPath = function(path) { if (!this.$__.fullPath) { - var parent = this; - if (!parent.__parent) return path; + var parent = this; // eslint-disable-line consistent-this + if (!parent.__parent) { + return path; + } var paths = []; - while (parent.__parent) { - paths.unshift(parent.__parentArray._path); - parent = parent.__parent; + while (parent.__parent || parent.$parent) { + if (parent.__parent) { + paths.unshift(parent.__parentArray._path); + } else { + paths.unshift(parent.$basePath); + } + parent = parent.__parent || parent.$parent; } this.$__.fullPath = paths.join('.'); @@ -9863,8 +11302,8 @@ EmbeddedDocument.prototype.$__fullPath = function(path) { } return path - ? this.$__.fullPath + '.' + path - : this.$__.fullPath; + ? this.$__.fullPath + '.' + path + : this.$__.fullPath; }; /** @@ -9893,7 +11332,7 @@ EmbeddedDocument.prototype.parentArray = function() { module.exports = EmbeddedDocument; -},{"../document_provider":6,"../promise_provider":23,"util":57}],42:[function(require,module,exports){ +},{"../document_provider":6,"../promise_provider":25}],48:[function(require,module,exports){ /*! * Module exports. @@ -9910,7 +11349,7 @@ exports.ObjectId = require('./objectid'); exports.Subdocument = require('./subdocument'); -},{"./array":38,"./buffer":39,"./documentarray":40,"./embedded":41,"./objectid":43,"./subdocument":44}],43:[function(require,module,exports){ +},{"./array":44,"./buffer":45,"./documentarray":46,"./embedded":47,"./objectid":49,"./subdocument":50}],49:[function(require,module,exports){ /** * ObjectId type constructor * @@ -9925,7 +11364,7 @@ var ObjectId = require('../drivers').ObjectId; module.exports = ObjectId; -},{"../drivers":11}],44:[function(require,module,exports){ +},{"../drivers":11}],50:[function(require,module,exports){ var Document = require('../document'); var PromiseProvider = require('../promise_provider'); @@ -9938,13 +11377,17 @@ module.exports = Subdocument; * @api private */ -function Subdocument() { - Document.apply(this, arguments); +function Subdocument(value, fields) { this.$isSingleNested = true; + Document.call(this, value, fields); } Subdocument.prototype = Object.create(Document.prototype); +Subdocument.prototype.toBSON = function() { + return this.toObject({ transform: false }); +}, + /** * Used as a stub for [hooks.js](https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3) * @@ -9972,7 +11415,11 @@ Subdocument.prototype.$isValid = function(path) { }; Subdocument.prototype.markModified = function(path) { + Document.prototype.markModified.call(this, path); if (this.$parent) { + if (this.$parent.isDirectModified(this.$basePath)) { + return; + } this.$parent.markModified([this.$basePath, path].join('.')); } }; @@ -9984,12 +11431,94 @@ Subdocument.prototype.$markValid = function(path) { }; Subdocument.prototype.invalidate = function(path, err, val) { + Document.prototype.invalidate.call(this, path, err, val); if (this.$parent) { this.$parent.invalidate([this.$basePath, path].join('.'), err, val); + } else if (err.kind === 'cast' || err.name === 'CastError') { + throw err; + } +}; + +/** + * Returns the top level document of this sub-document. + * + * @return {Document} + */ + +Subdocument.prototype.ownerDocument = function() { + if (this.$__.ownerDocument) { + return this.$__.ownerDocument; + } + + var parent = this.$parent; + if (!parent) { + return this; + } + + while (parent.$parent || parent.__parent) { + parent = parent.$parent || parent.__parent; + } + this.$__.ownerDocument = parent; + return this.$__.ownerDocument; +}; + +/** + * Null-out this subdoc + * + * @param {Object} [options] + * @param {Function} [callback] optional callback for compatibility with Document.prototype.remove + */ + +Subdocument.prototype.remove = function(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + // If removing entire doc, no need to remove subdoc + if (!options || !options.noop) { + this.$parent.set(this.$basePath, null); + registerRemoveListener(this); } + + if (typeof callback === 'function') { + callback(null); + } +}; + +/*! + * ignore + */ + +Subdocument.prototype.populate = function() { + throw new Error('Mongoose does not support calling populate() on nested ' + + 'docs. Instead of `doc.nested.populate("path")`, use ' + + '`doc.populate("nested.path")`'); }; -},{"../document":5,"../promise_provider":23}],45:[function(require,module,exports){ +/*! + * Registers remove event listeners for triggering + * on subdocuments. + * + * @param {EmbeddedDocument} sub + * @api private + */ + +function registerRemoveListener(sub) { + var owner = sub.ownerDocument(); + + function emitRemove() { + owner.removeListener('save', emitRemove); + owner.removeListener('remove', emitRemove); + sub.emit('remove', sub); + owner = sub = null; + } + + owner.on('save', emitRemove); + owner.on('remove', emitRemove); +} + +},{"../document":5,"../promise_provider":25}],51:[function(require,module,exports){ (function (process,Buffer){ /*! * Module dependencies. @@ -10014,9 +11543,15 @@ var Document; exports.toCollectionName = function(name, options) { options = options || {}; - if ('system.profile' === name) return name; - if ('system.indexes' === name) return name; - if (options.pluralization === false) return name; + if (name === 'system.profile') { + return name; + } + if (name === 'system.indexes') { + return name; + } + if (options.pluralization === false) { + return name; + } return pluralize(name.toLowerCase()); }; @@ -10106,7 +11641,9 @@ function pluralize(str) { found = rules.filter(function(rule) { return str.match(rule[0]); }); - if (found[0]) return str.replace(found[0][0], found[0][1]); + if (found[0]) { + return str.replace(found[0][0], found[0][1]); + } } return str; } @@ -10123,29 +11660,36 @@ function pluralize(str) { */ exports.deepEqual = function deepEqual(a, b) { - if (a === b) return true; + if (a === b) { + return true; + } - if (a instanceof Date && b instanceof Date) + if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); + } if (a instanceof ObjectId && b instanceof ObjectId) { return a.toString() === b.toString(); } if (a instanceof RegExp && b instanceof RegExp) { - return a.source == b.source && - a.ignoreCase == b.ignoreCase && - a.multiline == b.multiline && - a.global == b.global; + return a.source === b.source && + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.global === b.global; } - if (typeof a !== 'object' && typeof b !== 'object') + if (typeof a !== 'object' && typeof b !== 'object') { return a == b; + } - if (a === null || b === null || a === undefined || b === undefined) + if (a === null || b === null || a === undefined || b === undefined) { return false; + } - if (a.prototype !== b.prototype) return false; + if (a.prototype !== b.prototype) { + return false; + } // Handle MongooseNumbers if (a instanceof Number && b instanceof Number) { @@ -10156,37 +11700,46 @@ exports.deepEqual = function deepEqual(a, b) { return exports.buffer.areEqual(a, b); } - if (isMongooseObject(a)) a = a.toObject(); - if (isMongooseObject(b)) b = b.toObject(); + if (isMongooseObject(a)) { + a = a.toObject(); + } + if (isMongooseObject(b)) { + b = b.toObject(); + } try { var ka = Object.keys(a), kb = Object.keys(b), key, i; - } catch (e) {//happens when one is a string literal and the other isn't + } catch (e) { + // happens when one is a string literal and the other isn't return false; } // having the same number of owned properties (keys incorporates // hasOwnProperty) - if (ka.length != kb.length) + if (ka.length !== kb.length) { return false; + } - //the same set of keys (although not necessarily the same order), + // the same set of keys (although not necessarily the same order), ka.sort(); kb.sort(); - //~~~cheap key test + // ~~~cheap key test for (i = ka.length - 1; i >= 0; i--) { - if (ka[i] != kb[i]) + if (ka[i] !== kb[i]) { return false; + } } - //equivalent values for every corresponding key, and - //~~~possibly expensive deep test + // equivalent values for every corresponding key, and + // ~~~possibly expensive deep test for (i = ka.length - 1; i >= 0; i--) { key = ka[i]; - if (!deepEqual(a[key], b[key])) return false; + if (!deepEqual(a[key], b[key])) { + return false; + } } return true; @@ -10206,18 +11759,19 @@ exports.deepEqual = function deepEqual(a, b) { */ exports.clone = function clone(obj, options) { - if (obj === undefined || obj === null) + if (obj === undefined || obj === null) { return obj; + } - if (Array.isArray(obj)) + if (Array.isArray(obj)) { return cloneArray(obj, options); + } if (isMongooseObject(obj)) { - if (options && options.json && 'function' === typeof obj.toJSON) { + if (options && options.json && typeof obj.toJSON === 'function') { return obj.toJSON(options); - } else { - return obj.toObject(options); } + return obj.toObject(options); } if (obj.constructor) { @@ -10234,16 +11788,18 @@ exports.clone = function clone(obj, options) { } } - if (obj instanceof ObjectId) + if (obj instanceof ObjectId) { return new ObjectId(obj.id); + } if (!obj.constructor && exports.isObject(obj)) { // object created with Object.create(null) return cloneObject(obj, options); } - if (obj.valueOf) + if (obj.valueOf) { return obj.valueOf(); + } }; var clone = exports.clone; @@ -10265,7 +11821,7 @@ function cloneObject(obj, options) { for (k in obj) { val = clone(obj[k], options); - if (!minimize || ('undefined' !== typeof val)) { + if (!minimize || (typeof val !== 'undefined')) { hasKeys || (hasKeys = true); ret[k] = val; } @@ -10280,22 +11836,25 @@ function cloneObject(obj, options) { k = keys[i]; val = clone(obj[k], options); - if (!minimize || ('undefined' !== typeof val)) { - if (!hasKeys) hasKeys = true; + if (!minimize || (typeof val !== 'undefined')) { + if (!hasKeys) { + hasKeys = true; + } ret[k] = val; } } } return minimize - ? hasKeys && ret - : ret; + ? hasKeys && ret + : ret; } function cloneArray(arr, options) { var ret = []; - for (var i = 0, l = arr.length; i < l; i++) + for (var i = 0, l = arr.length; i < l; i++) { ret.push(clone(arr[i], options)); + } return ret; } @@ -10350,7 +11909,7 @@ exports.merge = function merge(to, from) { while (i--) { key = keys[i]; - if ('undefined' === typeof to[key]) { + if (typeof to[key] === 'undefined') { to[key] = from[key]; } else if (exports.isObject(from[key])) { merge(to[key], from[key]); @@ -10373,6 +11932,7 @@ var toString = Object.prototype.toString; */ exports.toObject = function toObject(obj) { + Document || (Document = require('./document')); var ret; if (exports.isNullOrUndefined(obj)) { @@ -10419,7 +11979,7 @@ exports.isObject = function(arg) { if (Buffer.isBuffer(arg)) { return true; } - return '[object Object]' == toString.call(arg); + return toString.call(arg) === '[object Object]'; }; /*! @@ -10441,7 +12001,9 @@ exports.args = sliced; */ exports.tick = function tick(callback) { - if ('function' !== typeof callback) return; + if (typeof callback !== 'function') { + return; + } return function() { try { callback.apply(this, arguments); @@ -10470,8 +12032,8 @@ exports.isMongooseObject = function(v) { MongooseBuffer || (MongooseBuffer = require('./types').Buffer); return v instanceof Document || - (v && v.isMongooseArray) || - (v && v.isMongooseBuffer); + (v && v.isMongooseArray) || + (v && v.isMongooseBuffer); }; var isMongooseObject = exports.isMongooseObject; @@ -10483,11 +12045,15 @@ var isMongooseObject = exports.isMongooseObject; */ exports.expires = function expires(object) { - if (!(object && 'Object' == object.constructor.name)) return; - if (!('expires' in object)) return; + if (!(object && object.constructor.name === 'Object')) { + return; + } + if (!('expires' in object)) { + return; + } var when; - if ('string' != typeof object.expires) { + if (typeof object.expires !== 'string') { when = object.expires; } else { when = Math.round(ms(object.expires) / 1000); @@ -10528,7 +12094,7 @@ exports.populate = function populate(path, select, model, match, options, subPop // an array, string, or object literal). // might have passed an object specifying all arguments - if (1 === arguments.length) { + if (arguments.length === 1) { if (path instanceof PopulateOptions) { return [path]; } @@ -10547,13 +12113,13 @@ exports.populate = function populate(path, select, model, match, options, subPop subPopulate = path.populate; path = path.path; } - } else if ('string' !== typeof model && 'function' !== typeof model) { + } else if (typeof model !== 'string' && typeof model !== 'function') { options = match; match = model; model = undefined; } - if ('string' != typeof path) { + if (typeof path !== 'string') { throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`'); } @@ -10563,6 +12129,7 @@ exports.populate = function populate(path, select, model, match, options, subPop var ret = []; var paths = path.split(' '); + options = exports.clone(options, { retainKeyOrder: true }); for (var i = 0; i < paths.length; ++i) { ret.push(new PopulateOptions(paths[i], select, match, options, model, subPopulate)); } @@ -10639,7 +12206,7 @@ exports.object.hasOwnProperty = function(obj, prop) { */ exports.isNullOrUndefined = function(val) { - return null == val; + return val === null || val === undefined; }; /*! @@ -10722,11 +12289,19 @@ exports.array.unique = function(arr) { exports.buffer = {}; exports.buffer.areEqual = function(a, b) { - if (!Buffer.isBuffer(a)) return false; - if (!Buffer.isBuffer(b)) return false; - if (a.length !== b.length) return false; + if (!Buffer.isBuffer(a)) { + return false; + } + if (!Buffer.isBuffer(b)) { + return false; + } + if (a.length !== b.length) { + return false; + } for (var i = 0, len = a.length; i < len; ++i) { - if (a[i] !== b[i]) return false; + if (a[i] !== b[i]) { + return false; + } } return true; }; @@ -10748,28 +12323,32 @@ exports.decorate = function(destination, source) { * merges to with a copy of from * * @param {Object} to - * @param {Object} from + * @param {Object} fromObj * @api private */ -exports.mergeClone = function(to, from) { - var keys = Object.keys(from), +exports.mergeClone = function(to, fromObj) { + var keys = Object.keys(fromObj), i = keys.length, key; while (i--) { key = keys[i]; - if ('undefined' === typeof to[key]) { + if (typeof to[key] === 'undefined') { // make sure to retain key order here because of a bug handling the $each // operator in mongodb 2.4.4 - to[key] = exports.clone(from[key], { retainKeyOrder : 1}); + to[key] = exports.clone(fromObj[key], {retainKeyOrder: 1}); } else { - if (exports.isObject(from[key])) { - exports.mergeClone(to[key], from[key]); + if (exports.isObject(fromObj[key])) { + var obj = fromObj[key]; + if (isMongooseObject(fromObj[key]) && !fromObj[key].isMongooseBuffer) { + obj = obj.toObject({ transform: false }); + } + exports.mergeClone(to[key], obj); } else { // make sure to retain key order here because of a bug handling the // $each operator in mongodb 2.4.4 - to[key] = exports.clone(from[key], { retainKeyOrder : 1}); + to[key] = exports.clone(fromObj[key], {retainKeyOrder: 1}); } } } @@ -10789,8 +12368,8 @@ exports.each = function(arr, fn) { } }; -}).call(this,require("FWaASH"),require("buffer").Buffer) -},{"./document":5,"./types":42,"./types/objectid":43,"FWaASH":55,"buffer":49,"mpath":78,"ms":91,"regexp-clone":92,"sliced":93}],46:[function(require,module,exports){ +}).call(this,require("g5I+bs"),require("buffer").Buffer) +},{"./document":5,"./types":48,"./types/objectid":49,"buffer":84,"g5I+bs":133,"mpath":119,"ms":132,"regexp-clone":134,"sliced":135}],52:[function(require,module,exports){ /** * VirtualType constructor @@ -10895,4680 +12474,5553 @@ VirtualType.prototype.applySetters = function(value, scope) { module.exports = VirtualType; -},{}],47:[function(require,module,exports){ -(function (process){ -/*! - * async - * https://github.com/caolan/async - * - * Copyright 2010-2014 Caolan McMahon - * Released under the MIT license - */ -/*jshint onevar: false, indent:4 */ -/*global setImmediate: false, setTimeout: false, console: false */ -(function () { - - var async = {}; +},{}],53:[function(require,module,exports){ +// http://wiki.commonjs.org/wiki/Unit_Testing/1.0 +// +// THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8! +// +// Originally from narwhal.js (http://narwhaljs.org) +// Copyright (c) 2009 Thomas Robinson <280north.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the 'Software'), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // global on the server, window in the browser - var root, previous_async; +// when used in node, this will actually load the util module we depend on +// versus loading the builtin util module as happens otherwise +// this is a bug in node module loading as far as I am concerned +var util = require('util/'); - root = this; - if (root != null) { - previous_async = root.async; - } +var pSlice = Array.prototype.slice; +var hasOwn = Object.prototype.hasOwnProperty; - async.noConflict = function () { - root.async = previous_async; - return async; - }; - - function only_once(fn) { - var called = false; - return function() { - if (called) throw new Error("Callback was already called."); - called = true; - fn.apply(root, arguments); - } - } - - //// cross-browser compatiblity functions //// +// 1. The assert module provides functions that throw +// AssertionError's when particular conditions are not met. The +// assert module must conform to the following interface. - var _toString = Object.prototype.toString; +var assert = module.exports = ok; - var _isArray = Array.isArray || function (obj) { - return _toString.call(obj) === '[object Array]'; - }; +// 2. The AssertionError is defined in assert. +// new assert.AssertionError({ message: message, +// actual: actual, +// expected: expected }) - var _each = function (arr, iterator) { - if (arr.forEach) { - return arr.forEach(iterator); - } - for (var i = 0; i < arr.length; i += 1) { - iterator(arr[i], i, arr); - } - }; +assert.AssertionError = function AssertionError(options) { + this.name = 'AssertionError'; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + if (options.message) { + this.message = options.message; + this.generatedMessage = false; + } else { + this.message = getMessage(this); + this.generatedMessage = true; + } + var stackStartFunction = options.stackStartFunction || fail; - var _map = function (arr, iterator) { - if (arr.map) { - return arr.map(iterator); - } - var results = []; - _each(arr, function (x, i, a) { - results.push(iterator(x, i, a)); - }); - return results; - }; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, stackStartFunction); + } + else { + // non v8 browsers so we can have a stacktrace + var err = new Error(); + if (err.stack) { + var out = err.stack; - var _reduce = function (arr, iterator, memo) { - if (arr.reduce) { - return arr.reduce(iterator, memo); - } - _each(arr, function (x, i, a) { - memo = iterator(memo, x, i, a); - }); - return memo; - }; + // try to strip useless frames + var fn_name = stackStartFunction.name; + var idx = out.indexOf('\n' + fn_name); + if (idx >= 0) { + // once we have located the function frame + // we need to strip out everything before it (and its line) + var next_line = out.indexOf('\n', idx + 1); + out = out.substring(next_line + 1); + } - var _keys = function (obj) { - if (Object.keys) { - return Object.keys(obj); - } - var keys = []; - for (var k in obj) { - if (obj.hasOwnProperty(k)) { - keys.push(k); - } - } - return keys; - }; + this.stack = out; + } + } +}; - //// exported async module functions //// +// assert.AssertionError instanceof Error +util.inherits(assert.AssertionError, Error); - //// nextTick implementation with browser-compatible fallback //// - if (typeof process === 'undefined' || !(process.nextTick)) { - if (typeof setImmediate === 'function') { - async.nextTick = function (fn) { - // not a direct alias for IE10 compatibility - setImmediate(fn); - }; - async.setImmediate = async.nextTick; - } - else { - async.nextTick = function (fn) { - setTimeout(fn, 0); - }; - async.setImmediate = async.nextTick; - } - } - else { - async.nextTick = process.nextTick; - if (typeof setImmediate !== 'undefined') { - async.setImmediate = function (fn) { - // not a direct alias for IE10 compatibility - setImmediate(fn); - }; - } - else { - async.setImmediate = async.nextTick; - } - } +function replacer(key, value) { + if (util.isUndefined(value)) { + return '' + value; + } + if (util.isNumber(value) && (isNaN(value) || !isFinite(value))) { + return value.toString(); + } + if (util.isFunction(value) || util.isRegExp(value)) { + return value.toString(); + } + return value; +} - async.each = function (arr, iterator, callback) { - callback = callback || function () {}; - if (!arr.length) { - return callback(); - } - var completed = 0; - _each(arr, function (x) { - iterator(x, only_once(done) ); - }); - function done(err) { - if (err) { - callback(err); - callback = function () {}; - } - else { - completed += 1; - if (completed >= arr.length) { - callback(); - } - } - } - }; - async.forEach = async.each; +function truncate(s, n) { + if (util.isString(s)) { + return s.length < n ? s : s.slice(0, n); + } else { + return s; + } +} - async.eachSeries = function (arr, iterator, callback) { - callback = callback || function () {}; - if (!arr.length) { - return callback(); - } - var completed = 0; - var iterate = function () { - iterator(arr[completed], function (err) { - if (err) { - callback(err); - callback = function () {}; - } - else { - completed += 1; - if (completed >= arr.length) { - callback(); - } - else { - iterate(); - } - } - }); - }; - iterate(); - }; - async.forEachSeries = async.eachSeries; +function getMessage(self) { + return truncate(JSON.stringify(self.actual, replacer), 128) + ' ' + + self.operator + ' ' + + truncate(JSON.stringify(self.expected, replacer), 128); +} - async.eachLimit = function (arr, limit, iterator, callback) { - var fn = _eachLimit(limit); - fn.apply(null, [arr, iterator, callback]); - }; - async.forEachLimit = async.eachLimit; +// At present only the three keys mentioned above are used and +// understood by the spec. Implementations or sub modules can pass +// other keys to the AssertionError's constructor - they will be +// ignored. - var _eachLimit = function (limit) { +// 3. All of the following functions must throw an AssertionError +// when a corresponding condition is not met, with a message that +// may be undefined if not provided. All assertion methods provide +// both the actual and expected values to the assertion error for +// display purposes. - return function (arr, iterator, callback) { - callback = callback || function () {}; - if (!arr.length || limit <= 0) { - return callback(); - } - var completed = 0; - var started = 0; - var running = 0; +function fail(actual, expected, message, operator, stackStartFunction) { + throw new assert.AssertionError({ + message: message, + actual: actual, + expected: expected, + operator: operator, + stackStartFunction: stackStartFunction + }); +} - (function replenish () { - if (completed >= arr.length) { - return callback(); - } +// EXTENSION! allows for well behaved errors defined elsewhere. +assert.fail = fail; - while (running < limit && started < arr.length) { - started += 1; - running += 1; - iterator(arr[started - 1], function (err) { - if (err) { - callback(err); - callback = function () {}; - } - else { - completed += 1; - running -= 1; - if (completed >= arr.length) { - callback(); - } - else { - replenish(); - } - } - }); - } - })(); - }; - }; +// 4. Pure assertion tests whether a value is truthy, as determined +// by !!guard. +// assert.ok(guard, message_opt); +// This statement is equivalent to assert.equal(true, !!guard, +// message_opt);. To test strictly for the value true, use +// assert.strictEqual(true, guard, message_opt);. +function ok(value, message) { + if (!value) fail(value, true, message, '==', assert.ok); +} +assert.ok = ok; - var doParallel = function (fn) { - return function () { - var args = Array.prototype.slice.call(arguments); - return fn.apply(null, [async.each].concat(args)); - }; - }; - var doParallelLimit = function(limit, fn) { - return function () { - var args = Array.prototype.slice.call(arguments); - return fn.apply(null, [_eachLimit(limit)].concat(args)); - }; - }; - var doSeries = function (fn) { - return function () { - var args = Array.prototype.slice.call(arguments); - return fn.apply(null, [async.eachSeries].concat(args)); - }; - }; +// 5. The equality assertion tests shallow, coercive equality with +// ==. +// assert.equal(actual, expected, message_opt); +assert.equal = function equal(actual, expected, message) { + if (actual != expected) fail(actual, expected, message, '==', assert.equal); +}; - var _asyncMap = function (eachfn, arr, iterator, callback) { - arr = _map(arr, function (x, i) { - return {index: i, value: x}; - }); - if (!callback) { - eachfn(arr, function (x, callback) { - iterator(x.value, function (err) { - callback(err); - }); - }); - } else { - var results = []; - eachfn(arr, function (x, callback) { - iterator(x.value, function (err, v) { - results[x.index] = v; - callback(err); - }); - }, function (err) { - callback(err, results); - }); - } - }; - async.map = doParallel(_asyncMap); - async.mapSeries = doSeries(_asyncMap); - async.mapLimit = function (arr, limit, iterator, callback) { - return _mapLimit(limit)(arr, iterator, callback); - }; +// 6. The non-equality assertion tests for whether two objects are not equal +// with != assert.notEqual(actual, expected, message_opt); - var _mapLimit = function(limit) { - return doParallelLimit(limit, _asyncMap); - }; +assert.notEqual = function notEqual(actual, expected, message) { + if (actual == expected) { + fail(actual, expected, message, '!=', assert.notEqual); + } +}; - // reduce only has a series version, as doing reduce in parallel won't - // work in many situations. - async.reduce = function (arr, memo, iterator, callback) { - async.eachSeries(arr, function (x, callback) { - iterator(memo, x, function (err, v) { - memo = v; - callback(err); - }); - }, function (err) { - callback(err, memo); - }); - }; - // inject alias - async.inject = async.reduce; - // foldl alias - async.foldl = async.reduce; - - async.reduceRight = function (arr, memo, iterator, callback) { - var reversed = _map(arr, function (x) { - return x; - }).reverse(); - async.reduce(reversed, memo, iterator, callback); - }; - // foldr alias - async.foldr = async.reduceRight; +// 7. The equivalence assertion tests a deep equality relation. +// assert.deepEqual(actual, expected, message_opt); - var _filter = function (eachfn, arr, iterator, callback) { - var results = []; - arr = _map(arr, function (x, i) { - return {index: i, value: x}; - }); - eachfn(arr, function (x, callback) { - iterator(x.value, function (v) { - if (v) { - results.push(x); - } - callback(); - }); - }, function (err) { - callback(_map(results.sort(function (a, b) { - return a.index - b.index; - }), function (x) { - return x.value; - })); - }); - }; - async.filter = doParallel(_filter); - async.filterSeries = doSeries(_filter); - // select alias - async.select = async.filter; - async.selectSeries = async.filterSeries; - - var _reject = function (eachfn, arr, iterator, callback) { - var results = []; - arr = _map(arr, function (x, i) { - return {index: i, value: x}; - }); - eachfn(arr, function (x, callback) { - iterator(x.value, function (v) { - if (!v) { - results.push(x); - } - callback(); - }); - }, function (err) { - callback(_map(results.sort(function (a, b) { - return a.index - b.index; - }), function (x) { - return x.value; - })); - }); - }; - async.reject = doParallel(_reject); - async.rejectSeries = doSeries(_reject); - - var _detect = function (eachfn, arr, iterator, main_callback) { - eachfn(arr, function (x, callback) { - iterator(x, function (result) { - if (result) { - main_callback(x); - main_callback = function () {}; - } - else { - callback(); - } - }); - }, function (err) { - main_callback(); - }); - }; - async.detect = doParallel(_detect); - async.detectSeries = doSeries(_detect); - - async.some = function (arr, iterator, main_callback) { - async.each(arr, function (x, callback) { - iterator(x, function (v) { - if (v) { - main_callback(true); - main_callback = function () {}; - } - callback(); - }); - }, function (err) { - main_callback(false); - }); - }; - // any alias - async.any = async.some; - - async.every = function (arr, iterator, main_callback) { - async.each(arr, function (x, callback) { - iterator(x, function (v) { - if (!v) { - main_callback(false); - main_callback = function () {}; - } - callback(); - }); - }, function (err) { - main_callback(true); - }); - }; - // all alias - async.all = async.every; - - async.sortBy = function (arr, iterator, callback) { - async.map(arr, function (x, callback) { - iterator(x, function (err, criteria) { - if (err) { - callback(err); - } - else { - callback(null, {value: x, criteria: criteria}); - } - }); - }, function (err, results) { - if (err) { - return callback(err); - } - else { - var fn = function (left, right) { - var a = left.criteria, b = right.criteria; - return a < b ? -1 : a > b ? 1 : 0; - }; - callback(null, _map(results.sort(fn), function (x) { - return x.value; - })); - } - }); - }; +assert.deepEqual = function deepEqual(actual, expected, message) { + if (!_deepEqual(actual, expected)) { + fail(actual, expected, message, 'deepEqual', assert.deepEqual); + } +}; - async.auto = function (tasks, callback) { - callback = callback || function () {}; - var keys = _keys(tasks); - var remainingTasks = keys.length - if (!remainingTasks) { - return callback(); - } +function _deepEqual(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; - var results = {}; + } else if (util.isBuffer(actual) && util.isBuffer(expected)) { + if (actual.length != expected.length) return false; - var listeners = []; - var addListener = function (fn) { - listeners.unshift(fn); - }; - var removeListener = function (fn) { - for (var i = 0; i < listeners.length; i += 1) { - if (listeners[i] === fn) { - listeners.splice(i, 1); - return; - } - } - }; - var taskComplete = function () { - remainingTasks-- - _each(listeners.slice(0), function (fn) { - fn(); - }); - }; + for (var i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) return false; + } - addListener(function () { - if (!remainingTasks) { - var theCallback = callback; - // prevent final callback from calling itself if it errors - callback = function () {}; + return true; - theCallback(null, results); - } - }); + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (util.isDate(actual) && util.isDate(expected)) { + return actual.getTime() === expected.getTime(); - _each(keys, function (k) { - var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]]; - var taskCallback = function (err) { - var args = Array.prototype.slice.call(arguments, 1); - if (args.length <= 1) { - args = args[0]; - } - if (err) { - var safeResults = {}; - _each(_keys(results), function(rkey) { - safeResults[rkey] = results[rkey]; - }); - safeResults[k] = args; - callback(err, safeResults); - // stop subsequent errors hitting callback multiple times - callback = function () {}; - } - else { - results[k] = args; - async.setImmediate(taskComplete); - } - }; - var requires = task.slice(0, Math.abs(task.length - 1)) || []; - var ready = function () { - return _reduce(requires, function (a, x) { - return (a && results.hasOwnProperty(x)); - }, true) && !results.hasOwnProperty(k); - }; - if (ready()) { - task[task.length - 1](taskCallback, results); - } - else { - var listener = function () { - if (ready()) { - removeListener(listener); - task[task.length - 1](taskCallback, results); - } - }; - addListener(listener); - } - }); - }; + // 7.3 If the expected value is a RegExp object, the actual value is + // equivalent if it is also a RegExp object with the same source and + // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). + } else if (util.isRegExp(actual) && util.isRegExp(expected)) { + return actual.source === expected.source && + actual.global === expected.global && + actual.multiline === expected.multiline && + actual.lastIndex === expected.lastIndex && + actual.ignoreCase === expected.ignoreCase; - async.retry = function(times, task, callback) { - var DEFAULT_TIMES = 5; - var attempts = []; - // Use defaults if times not passed - if (typeof times === 'function') { - callback = task; - task = times; - times = DEFAULT_TIMES; - } - // Make sure times is a number - times = parseInt(times, 10) || DEFAULT_TIMES; - var wrappedTask = function(wrappedCallback, wrappedResults) { - var retryAttempt = function(task, finalAttempt) { - return function(seriesCallback) { - task(function(err, result){ - seriesCallback(!err || finalAttempt, {err: err, result: result}); - }, wrappedResults); - }; - }; - while (times) { - attempts.push(retryAttempt(task, !(times-=1))); - } - async.series(attempts, function(done, data){ - data = data[data.length - 1]; - (wrappedCallback || callback)(data.err, data.result); - }); - } - // If a callback is passed, run this as a controll flow - return callback ? wrappedTask() : wrappedTask - }; + // 7.4. Other pairs that do not both pass typeof value == 'object', + // equivalence is determined by ==. + } else if (!util.isObject(actual) && !util.isObject(expected)) { + return actual == expected; - async.waterfall = function (tasks, callback) { - callback = callback || function () {}; - if (!_isArray(tasks)) { - var err = new Error('First argument to waterfall must be an array of functions'); - return callback(err); - } - if (!tasks.length) { - return callback(); - } - var wrapIterator = function (iterator) { - return function (err) { - if (err) { - callback.apply(null, arguments); - callback = function () {}; - } - else { - var args = Array.prototype.slice.call(arguments, 1); - var next = iterator.next(); - if (next) { - args.push(wrapIterator(next)); - } - else { - args.push(callback); - } - async.setImmediate(function () { - iterator.apply(null, args); - }); - } - }; - }; - wrapIterator(async.iterator(tasks))(); - }; + // 7.5 For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } +} - var _parallel = function(eachfn, tasks, callback) { - callback = callback || function () {}; - if (_isArray(tasks)) { - eachfn.map(tasks, function (fn, callback) { - if (fn) { - fn(function (err) { - var args = Array.prototype.slice.call(arguments, 1); - if (args.length <= 1) { - args = args[0]; - } - callback.call(null, err, args); - }); - } - }, callback); - } - else { - var results = {}; - eachfn.each(_keys(tasks), function (k, callback) { - tasks[k](function (err) { - var args = Array.prototype.slice.call(arguments, 1); - if (args.length <= 1) { - args = args[0]; - } - results[k] = args; - callback(err); - }); - }, function (err) { - callback(err, results); - }); - } - }; +function isArguments(object) { + return Object.prototype.toString.call(object) == '[object Arguments]'; +} - async.parallel = function (tasks, callback) { - _parallel({ map: async.map, each: async.each }, tasks, callback); - }; +function objEquiv(a, b) { + if (util.isNullOrUndefined(a) || util.isNullOrUndefined(b)) + return false; + // an identical 'prototype' property. + if (a.prototype !== b.prototype) return false; + //~~~I've managed to break Object.keys through screwy arguments passing. + // Converting to array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = pSlice.call(a); + b = pSlice.call(b); + return _deepEqual(a, b); + } + try { + var ka = objectKeys(a), + kb = objectKeys(b), + key, i; + } catch (e) {//happens when one is a string literal and the other isn't + return false; + } + // having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) + return false; + //the same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + //~~~cheap key test + for (i = ka.length - 1; i >= 0; i--) { + if (ka[i] != kb[i]) + return false; + } + //equivalent values for every corresponding key, and + //~~~possibly expensive deep test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!_deepEqual(a[key], b[key])) return false; + } + return true; +} - async.parallelLimit = function(tasks, limit, callback) { - _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); - }; +// 8. The non-equivalence assertion tests for any deep inequality. +// assert.notDeepEqual(actual, expected, message_opt); - async.series = function (tasks, callback) { - callback = callback || function () {}; - if (_isArray(tasks)) { - async.mapSeries(tasks, function (fn, callback) { - if (fn) { - fn(function (err) { - var args = Array.prototype.slice.call(arguments, 1); - if (args.length <= 1) { - args = args[0]; - } - callback.call(null, err, args); - }); - } - }, callback); - } - else { - var results = {}; - async.eachSeries(_keys(tasks), function (k, callback) { - tasks[k](function (err) { - var args = Array.prototype.slice.call(arguments, 1); - if (args.length <= 1) { - args = args[0]; - } - results[k] = args; - callback(err); - }); - }, function (err) { - callback(err, results); - }); - } - }; +assert.notDeepEqual = function notDeepEqual(actual, expected, message) { + if (_deepEqual(actual, expected)) { + fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); + } +}; - async.iterator = function (tasks) { - var makeCallback = function (index) { - var fn = function () { - if (tasks.length) { - tasks[index].apply(null, arguments); - } - return fn.next(); - }; - fn.next = function () { - return (index < tasks.length - 1) ? makeCallback(index + 1): null; - }; - return fn; - }; - return makeCallback(0); - }; +// 9. The strict equality assertion tests strict equality, as determined by ===. +// assert.strictEqual(actual, expected, message_opt); - async.apply = function (fn) { - var args = Array.prototype.slice.call(arguments, 1); - return function () { - return fn.apply( - null, args.concat(Array.prototype.slice.call(arguments)) - ); - }; - }; +assert.strictEqual = function strictEqual(actual, expected, message) { + if (actual !== expected) { + fail(actual, expected, message, '===', assert.strictEqual); + } +}; - var _concat = function (eachfn, arr, fn, callback) { - var r = []; - eachfn(arr, function (x, cb) { - fn(x, function (err, y) { - r = r.concat(y || []); - cb(err); - }); - }, function (err) { - callback(err, r); - }); - }; - async.concat = doParallel(_concat); - async.concatSeries = doSeries(_concat); - - async.whilst = function (test, iterator, callback) { - if (test()) { - iterator(function (err) { - if (err) { - return callback(err); - } - async.whilst(test, iterator, callback); - }); - } - else { - callback(); - } - }; +// 10. The strict non-equality assertion tests for strict inequality, as +// determined by !==. assert.notStrictEqual(actual, expected, message_opt); - async.doWhilst = function (iterator, test, callback) { - iterator(function (err) { - if (err) { - return callback(err); - } - var args = Array.prototype.slice.call(arguments, 1); - if (test.apply(null, args)) { - async.doWhilst(iterator, test, callback); - } - else { - callback(); - } - }); - }; +assert.notStrictEqual = function notStrictEqual(actual, expected, message) { + if (actual === expected) { + fail(actual, expected, message, '!==', assert.notStrictEqual); + } +}; - async.until = function (test, iterator, callback) { - if (!test()) { - iterator(function (err) { - if (err) { - return callback(err); - } - async.until(test, iterator, callback); - }); - } - else { - callback(); - } - }; +function expectedException(actual, expected) { + if (!actual || !expected) { + return false; + } - async.doUntil = function (iterator, test, callback) { - iterator(function (err) { - if (err) { - return callback(err); - } - var args = Array.prototype.slice.call(arguments, 1); - if (!test.apply(null, args)) { - async.doUntil(iterator, test, callback); - } - else { - callback(); - } - }); - }; + if (Object.prototype.toString.call(expected) == '[object RegExp]') { + return expected.test(actual); + } else if (actual instanceof expected) { + return true; + } else if (expected.call({}, actual) === true) { + return true; + } - async.queue = function (worker, concurrency) { - if (concurrency === undefined) { - concurrency = 1; - } - function _insert(q, data, pos, callback) { - if (!q.started){ - q.started = true; - } - if (!_isArray(data)) { - data = [data]; - } - if(data.length == 0) { - // call drain immediately if there are no tasks - return async.setImmediate(function() { - if (q.drain) { - q.drain(); - } - }); - } - _each(data, function(task) { - var item = { - data: task, - callback: typeof callback === 'function' ? callback : null - }; + return false; +} - if (pos) { - q.tasks.unshift(item); - } else { - q.tasks.push(item); - } +function _throws(shouldThrow, block, expected, message) { + var actual; - if (q.saturated && q.tasks.length === q.concurrency) { - q.saturated(); - } - async.setImmediate(q.process); - }); - } + if (util.isString(expected)) { + message = expected; + expected = null; + } - var workers = 0; - var q = { - tasks: [], - concurrency: concurrency, - saturated: null, - empty: null, - drain: null, - started: false, - paused: false, - push: function (data, callback) { - _insert(q, data, false, callback); - }, - kill: function () { - q.drain = null; - q.tasks = []; - }, - unshift: function (data, callback) { - _insert(q, data, true, callback); - }, - process: function () { - if (!q.paused && workers < q.concurrency && q.tasks.length) { - var task = q.tasks.shift(); - if (q.empty && q.tasks.length === 0) { - q.empty(); - } - workers += 1; - var next = function () { - workers -= 1; - if (task.callback) { - task.callback.apply(task, arguments); - } - if (q.drain && q.tasks.length + workers === 0) { - q.drain(); - } - q.process(); - }; - var cb = only_once(next); - worker(task.data, cb); - } - }, - length: function () { - return q.tasks.length; - }, - running: function () { - return workers; - }, - idle: function() { - return q.tasks.length + workers === 0; - }, - pause: function () { - if (q.paused === true) { return; } - q.paused = true; - q.process(); - }, - resume: function () { - if (q.paused === false) { return; } - q.paused = false; - q.process(); - } - }; - return q; - }; - - async.priorityQueue = function (worker, concurrency) { - - function _compareTasks(a, b){ - return a.priority - b.priority; - }; - - function _binarySearch(sequence, item, compare) { - var beg = -1, - end = sequence.length - 1; - while (beg < end) { - var mid = beg + ((end - beg + 1) >>> 1); - if (compare(item, sequence[mid]) >= 0) { - beg = mid; - } else { - end = mid - 1; - } - } - return beg; - } - - function _insert(q, data, priority, callback) { - if (!q.started){ - q.started = true; - } - if (!_isArray(data)) { - data = [data]; - } - if(data.length == 0) { - // call drain immediately if there are no tasks - return async.setImmediate(function() { - if (q.drain) { - q.drain(); - } - }); - } - _each(data, function(task) { - var item = { - data: task, - priority: priority, - callback: typeof callback === 'function' ? callback : null - }; - - q.tasks.splice(_binarySearch(q.tasks, item, _compareTasks) + 1, 0, item); + try { + block(); + } catch (e) { + actual = e; + } - if (q.saturated && q.tasks.length === q.concurrency) { - q.saturated(); - } - async.setImmediate(q.process); - }); - } - - // Start with a normal queue - var q = async.queue(worker, concurrency); - - // Override push to accept second parameter representing priority - q.push = function (data, priority, callback) { - _insert(q, data, priority, callback); - }; - - // Remove unshift function - delete q.unshift; + message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + + (message ? ' ' + message : '.'); - return q; - }; + if (shouldThrow && !actual) { + fail(actual, expected, 'Missing expected exception' + message); + } - async.cargo = function (worker, payload) { - var working = false, - tasks = []; - - var cargo = { - tasks: tasks, - payload: payload, - saturated: null, - empty: null, - drain: null, - drained: true, - push: function (data, callback) { - if (!_isArray(data)) { - data = [data]; - } - _each(data, function(task) { - tasks.push({ - data: task, - callback: typeof callback === 'function' ? callback : null - }); - cargo.drained = false; - if (cargo.saturated && tasks.length === payload) { - cargo.saturated(); - } - }); - async.setImmediate(cargo.process); - }, - process: function process() { - if (working) return; - if (tasks.length === 0) { - if(cargo.drain && !cargo.drained) cargo.drain(); - cargo.drained = true; - return; - } + if (!shouldThrow && expectedException(actual, expected)) { + fail(actual, expected, 'Got unwanted exception' + message); + } - var ts = typeof payload === 'number' - ? tasks.splice(0, payload) - : tasks.splice(0, tasks.length); + if ((shouldThrow && actual && expected && + !expectedException(actual, expected)) || (!shouldThrow && actual)) { + throw actual; + } +} - var ds = _map(ts, function (task) { - return task.data; - }); +// 11. Expected to throw an error: +// assert.throws(block, Error_opt, message_opt); - if(cargo.empty) cargo.empty(); - working = true; - worker(ds, function () { - working = false; +assert.throws = function(block, /*optional*/error, /*optional*/message) { + _throws.apply(this, [true].concat(pSlice.call(arguments))); +}; - var args = arguments; - _each(ts, function (data) { - if (data.callback) { - data.callback.apply(null, args); - } - }); +// EXTENSION! This is annoying to write outside this module. +assert.doesNotThrow = function(block, /*optional*/message) { + _throws.apply(this, [false].concat(pSlice.call(arguments))); +}; - process(); - }); - }, - length: function () { - return tasks.length; - }, - running: function () { - return working; - } - }; - return cargo; - }; +assert.ifError = function(err) { if (err) {throw err;}}; - var _console_fn = function (name) { - return function (fn) { - var args = Array.prototype.slice.call(arguments, 1); - fn.apply(null, args.concat([function (err) { - var args = Array.prototype.slice.call(arguments, 1); - if (typeof console !== 'undefined') { - if (err) { - if (console.error) { - console.error(err); - } - } - else if (console[name]) { - _each(args, function (x) { - console[name](x); - }); - } - } - }])); - }; - }; - async.log = _console_fn('log'); - async.dir = _console_fn('dir'); - /*async.info = _console_fn('info'); - async.warn = _console_fn('warn'); - async.error = _console_fn('error');*/ - - async.memoize = function (fn, hasher) { - var memo = {}; - var queues = {}; - hasher = hasher || function (x) { - return x; - }; - var memoized = function () { - var args = Array.prototype.slice.call(arguments); - var callback = args.pop(); - var key = hasher.apply(null, args); - if (key in memo) { - async.nextTick(function () { - callback.apply(null, memo[key]); - }); - } - else if (key in queues) { - queues[key].push(callback); - } - else { - queues[key] = [callback]; - fn.apply(null, args.concat([function () { - memo[key] = arguments; - var q = queues[key]; - delete queues[key]; - for (var i = 0, l = q.length; i < l; i++) { - q[i].apply(null, arguments); - } - }])); - } - }; - memoized.memo = memo; - memoized.unmemoized = fn; - return memoized; - }; +var objectKeys = Object.keys || function (obj) { + var keys = []; + for (var key in obj) { + if (hasOwn.call(obj, key)) keys.push(key); + } + return keys; +}; - async.unmemoize = function (fn) { - return function () { - return (fn.unmemoized || fn).apply(null, arguments); - }; - }; +},{"util/":138}],54:[function(require,module,exports){ +'use strict'; - async.times = function (count, iterator, callback) { - var counter = []; - for (var i = 0; i < count; i++) { - counter.push(i); - } - return async.map(counter, iterator, callback); - }; +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = eachLimit; - async.timesSeries = function (count, iterator, callback) { - var counter = []; - for (var i = 0; i < count; i++) { - counter.push(i); - } - return async.mapSeries(counter, iterator, callback); - }; +var _eachOf = require('./eachOf'); - async.seq = function (/* functions... */) { - var fns = arguments; - return function () { - var that = this; - var args = Array.prototype.slice.call(arguments); - var callback = args.pop(); - async.reduce(fns, args, function (newargs, fn, cb) { - fn.apply(that, newargs.concat([function () { - var err = arguments[0]; - var nextargs = Array.prototype.slice.call(arguments, 1); - cb(err, nextargs); - }])) - }, - function (err, results) { - callback.apply(that, [err].concat(results)); - }); - }; - }; +var _eachOf2 = _interopRequireDefault(_eachOf); - async.compose = function (/* functions... */) { - return async.seq.apply(null, Array.prototype.reverse.call(arguments)); - }; +var _withoutIndex = require('./internal/withoutIndex'); - var _applyEach = function (eachfn, fns /*args...*/) { - var go = function () { - var that = this; - var args = Array.prototype.slice.call(arguments); - var callback = args.pop(); - return eachfn(fns, function (fn, cb) { - fn.apply(that, args.concat([cb])); - }, - callback); - }; - if (arguments.length > 2) { - var args = Array.prototype.slice.call(arguments, 2); - return go.apply(this, args); - } - else { - return go; - } - }; - async.applyEach = doParallel(_applyEach); - async.applyEachSeries = doSeries(_applyEach); +var _withoutIndex2 = _interopRequireDefault(_withoutIndex); - async.forever = function (fn, callback) { - function next(err) { - if (err) { - if (callback) { - return callback(err); - } - throw err; - } - fn(next); - } - next(); - }; +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - // Node.js - if (typeof module !== 'undefined' && module.exports) { - module.exports = async; - } - // AMD / RequireJS - else if (typeof define !== 'undefined' && define.amd) { - define([], function () { - return async; - }); - } - // included directly via + $("#links, #content").on("click", "a", function (e) { + if (_gaq && this.hash) { + _gaq.push(['_trackEvent', 'anchor', 'click', this.hash]) + } + }) +})() \ No newline at end of file diff --git a/docs/api.jade b/docs/api.jade index 1db2474ab23..34db9a8b6f1 100644 --- a/docs/api.jade +++ b/docs/api.jade @@ -4,6 +4,7 @@ html(lang='en') meta(charset="utf-8") meta(name="viewport", content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no") title Mongoose API v#{package.version} + include ./includes/favicon.jade link( rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css") link(href='//fonts.googleapis.com/css?family=Anonymous+Pro:400,700|Droid+Sans+Mono|Open+Sans:400,700|Linden+Hill|Quattrocento:400,700|News+Cycle:400,700|Antic+Slab|Cabin+Condensed:400,700', rel='stylesheet', type='text/css') @@ -22,10 +23,11 @@ html(lang='en') .doclinks each item in docs div.file(class=item.hasPublic ? '' : 'private') - a.section(href='#' + item.cleantitle) - | #{item.title} - |   - i.fa.fa-caret-right.toggle-item + span.toggle-item + a.section(href='#' + item.cleantitle) + | #{item.title} + |   + i.fa.fa-caret-right ul each method in item.methods if method.ctx && !method.ignore @@ -108,7 +110,7 @@ html(lang='en') if i+1 < tag.types.length | , | > - span= tag.description + span!= tag.description if method.return .returns h4 Returns: @@ -272,19 +274,20 @@ html(lang='en') hr(class= property.isPrivate ? 'private' : '') script. document.body.className = 'load'; - include includes/googleanalytics + include includes/keen script(src="/docs/js/zepto.min.js") script(src="/docs/js/cookies.min.js") script. $(".toggle-item").click(function() { - if ($(this).hasClass('fa-caret-right')) { - $(this).removeClass('fa-caret-right'); - $(this).addClass('fa-caret-down'); - $(this).parent().addClass('displayed'); + var $parent = $(this).parent(), + $icon = $(this).find("i"); + + if($parent.hasClass('displayed')){ + $parent.removeClass('displayed'); + $icon.addClass("fa-caret-right").removeClass("fa-caret-down"); } else { - $(this).addClass('fa-caret-right'); - $(this).removeClass('fa-caret-down'); - $(this).parent().removeClass('displayed'); + $parent.addClass('displayed'); + $icon.removeClass("fa-caret-right").addClass("fa-caret-down"); } }); $(".module").on("click", ".showcode", function (e) { diff --git a/docs/compatibility.jade b/docs/compatibility.jade index e8f0f775e4e..b4dd447f5cf 100644 --- a/docs/compatibility.jade +++ b/docs/compatibility.jade @@ -16,3 +16,5 @@ block content * MongoDB Server 2.4.x: mongoose `~3.8`, `4.x` * MongoDB Server 2.6.x: mongoose `~3.8.8`, `4.x` * MongoDB Server 3.0.x: mongoose `~3.8.22`, `4.x` + * MongoDB Server 3.2.x: mongoose `>=4.3.0` + * MongoDB Server 3.4.x: mongoose `>=4.7.3` diff --git a/docs/connections.jade b/docs/connections.jade index ee1984488ee..54cfe5c26ad 100644 --- a/docs/connections.jade +++ b/docs/connections.jade @@ -3,15 +3,18 @@ extends layout block content h2 Connections :markdown - We may connect to MongoDB by utilizing the `mongoose.connect()` method. + You can connect to MongoDB with the `mongoose.connect()` method. :js mongoose.connect('mongodb://localhost/myapp'); :markdown - This is the minimum needed to connect the `myapp` database running locally on the default port (27017). If the local connection fails then try using 127.0.0.1 instead of localhost. Sometimes issues may arise when the local hostname has been changed. - - We may also specify several more parameters in the `uri` depending on your environment: + This is the minimum needed to connect the `myapp` database running locally + on the default port (27017). If the local connection fails then try using + 127.0.0.1 instead of localhost. Sometimes issues may arise when the local + hostname has been changed. + + You can also specify several more parameters in the `uri`: :js mongoose.connect('mongodb://username:password@host:port/database?options...'); @@ -19,61 +22,149 @@ block content :markdown See the [mongodb connection string spec](http://docs.mongodb.org/manual/reference/connection-string/) for more detail. + h3#buffering Operation Buffering + :markdown + Mongoose lets you start using your models immediately, without waiting for + mongoose to establish a connection to MongoDB. + + :js + mongoose.connect('mongodb://localhost/myapp'); + var MyModel = mongoose.model('Test', new Schema({ name: String })); + // Works + MyModel.findOne(function(error, result) { /* ... */ }); + + :markdown + That's because mongoose buffers model function calls internally. This + buffering is convenient, but also a common source of confusion. Mongoose + will *not* throw any errors by default if you use a model without + connecting. + + :js + var MyModel = mongoose.model('Test', new Schema({ name: String })); + // Will just hang until mongoose successfully connects + MyModel.findOne(function(error, result) { /* ... */ }); + + setTimeout(function() { + mongoose.connect('mongodb://localhost/myapp'); + }, 60000); + + :markdown + To disable buffering, turn off the [`bufferCommands` option on your schema](./guide.html#bufferCommands). + If you have `bufferCommands` on and your connection is hanging, try turning + `bufferCommands` off to see if you haven't opened a connection properly. + h3#options Options :markdown - The `connect` method also accepts an `options` object which will be passed on to the underlying driver. All options included here take precedence over options passed in the connection string. + The `connect` method also accepts an `options` object which will be passed on to the underlying MongoDB driver. :js mongoose.connect(uri, options); :markdown - The following option keys are available: + A full list of options can be found on the [MongoDB Node.js driver docs for `connect()`](http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html#connect). + Mongoose passes options to the driver without modification, modulo three exceptions that are explained below. + + * `useMongoClient` - This is a mongoose-specific option (not passed to the MongoDB driver) that opts in to mongoose 4.11's new connection logic. If you are writing a new application, you **should** set this to `true`. + * `user`/`pass` - The username and password for authentication. These options are mongoose-specific, they are equivalent to the MongoDB driver's `auth.user` and `auth.password` options. + * `autoIndex` - By default, mongoose will automatically build indexes defined in your schema when it connects. This is great for development, but not ideal for large production deployments, because index builds can cause performance degradation. If you set `autoIndex` to false, mongoose will not automatically build indexes for **any** model associated with this connection. + + Below are some of the options that are important for tuning mongoose. + + * `autoReconnect` - The underlying MongoDB driver will automatically try to reconnect when it loses connection to MongoDB. Unless you are an extremely advanced user that wants to manage their own connection pool, do **not** set this option to `false`. + * `reconnectTries` - If you're connected to a single server or mongos proxy (as opposed to a replica set), the MongoDB driver will try to reconnect every `reconnectInterval` milliseconds for `reconnectTries` times, and give up afterward. When the driver gives up, the mongoose connection emits a `reconnectFailed` event. This option does nothing for replica set connections. + * `reconnectInterval` - See `reconnectTries` + * `promiseLibrary` - sets the [underlying driver's promise library](http://mongodb.github.io/node-mongodb-native/2.1/api/MongoClient.html) + * `poolSize` - The maximum number of sockets the MongoDB driver will keep open for this connection. By default, `poolSize` is 5. Keep in mind that, as of MongoDB 3.4, MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. + * `bufferMaxEntries` - The MongoDB driver also has its own buffering mechanism that kicks in when the driver is disconnected. Set this option to 0 and set `bufferCommands` to `false` on your schemas if you want your database operations to fail immediately when the driver is not connected, as opposed to waiting for reconnection. - db - passed to the connection db instance - server - passed to the connection server instance(s) - replset - passed to the connection ReplSet instance - user - username for authentication (if not specified in uri) - pass - password for authentication (if not specified in uri) - auth - options for authentication - mongos - passed to the [underlying driver's mongos options](http://mongodb.github.io/node-mongodb-native/2.0/api/Mongos.html) Example: :js var options = { - db: { native_parser: true }, - server: { poolSize: 5 }, - replset: { rs_name: 'myReplicaSetName' }, - user: 'myUserName', - pass: 'myPassword' - } + useMongoClient: true, + autoIndex: false, // Don't build indexes + reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect + reconnectInterval: 500, // Reconnect every 500ms + poolSize: 10, // Maintain up to 10 socket connections + // If not connected, return errors immediately rather than waiting for reconnect + bufferMaxEntries: 0 + }; mongoose.connect(uri, options); + + h3#callback Callback :markdown - **Note:** - The server option `auto_reconnect` is defaulted to true which _can_ be overridden. - The db option `forceServerObjectId` is set to false which _cannot_ be overridden. + The `connect()` function also accepts a callback parameter and returns a [promise](./promises.html). + :js + mongoose.connect(uri, options, function(error) { + // Check error in initial connection. There is no 2nd param to the callback. + }); - See the [driver](https://github.com/mongodb/node-mongodb-native) for more information about available options. + // Or using promises + mongoose.connect(uri, options).then( + () => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ }, + err => { /** handle initial connection error */ } + ); + + h4#connection-string-options Connection String Options + :markdown + You can also specify options in your connection string as [parameters in the query string](https://en.wikipedia.org/wiki/Query_string) portion of the URI. + :js + mongoose.connect('mongodb://localhost:27017/test?connectTimeoutMS=1000', { useMongoClient: true }); + // The above is equivalent to: + mongoose.connect('mongodb://localhost:27017/test', { + useMongoClient: true, + connectTimeoutMS: 1000 + }); + :markdown + The disadvantage of putting options in the query string is that query + string options are harder to read. The advantage is that you only need a + single configuration option, the URI, rather than separate options for + `socketTimeoutMS`, `connectTimeoutMS`, etc. Best practice is to put options + that likely differ between development and production, like `replicaSet` + or `ssl`, in the connection string, and options that should remain constant, + like `connectTimeoutMS` or `poolSize`, in the options object. + + The MongoDB docs have a full list of [supported connection string options](https://docs.mongodb.com/manual/reference/connection-string/) h4#keepAlive A note about keepAlive .important :markdown - For long running applictions it is often prudent to enable `keepAlive`. Without it, after some period of time you may start to see `"connection closed"` errors for what seems like no reason. If so, after [reading this](http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html), you may decide to enable `keepAlive`: + For long running applications, it is often prudent to enable `keepAlive` + with a number of milliseconds. Without it, after some period of time + you may start to see `"connection closed"` errors for what seems like + no reason. If so, after + [reading this](http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html), + you may decide to enable `keepAlive`: :js - options.server.socketOptions = options.replset.socketOptions = { keepAlive: 1 }; + options.server.socketOptions = options.replset.socketOptions = { keepAlive: 120 }; mongoose.connect(uri, options); - h3#replicaset_connections ReplicaSet Connections + h3#replicaset_connections Replica Set Connections :markdown - The same method is used to connect to a replica set but instead of passing a single `uri` we pass a comma delimited list of `uri`s. + To connect to a replica set you pass a comma delimited list of hosts to + connect to rather than a single host. :js mongoose.connect('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]' [, options]); + :markdown + To connect to a single node replica set, specify the `replicaSet` option. + + :js + mongoose.connect('mongodb://host1:port1/?replicaSet=rsName'); + h3#mongos_connections Multi-mongos support :markdown - High availability over multiple `mongos` instances is also supported. Pass a connection string for your `mongos` instances and set the `mongos` option to true: + High availability over multiple `mongos` instances is also supported. + Pass a connection string for your `mongos` instances. If you are not using + the `useMongoClient` option, you must also set the `mongos` option: :js mongoose.connect('mongodb://mongosA:27501,mongosB:27501', { mongos: true }, cb); + :markdown + With `useMongoClient`, you do not need to set the `mongos` option. You also + do **not** need to use `mongos` or `useMongoClient` in mongoose 5.x. + :js + mongoose.connect('mongodb://mongosA:27501,mongosB:27501', { useMongoClient: true }, cb); h3#multiple_connections Multiple connections :markdown @@ -83,11 +174,13 @@ block content var conn = mongoose.createConnection('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]', options); :markdown - This [connection](./api.html#connection_Connection) object can then be used for creating and retrieving [models](./api.html#model_Model) that are scoped only to this specific connection. + This [connection](./api.html#connection_Connection) object is then used to + create and retrieve [models](./api.html#model_Model). Models are + **always** scoped to a single connection. h3#connection_pools Connection pools :markdown - Each `connection`, whether created with `mongoose.connect` or `mongoose.createConnection` are all backed by an internal configurable connection pool defaulting to a size of 5. Adjust the pool size using your connection options: + Each `connection`, whether created with `mongoose.connect` or `mongoose.createConnection` are all backed by an internal configurable connection pool defaulting to a maximum size of 5. Adjust the pool size using your connection options: :js // single server @@ -101,6 +194,82 @@ block content var uri = 'mongodb://localhost/test?poolSize=4'; mongoose.createConnection(uri); + h3#use-mongo-client The `useMongoClient` Option + :markdown + Mongoose's default connection logic is deprecated as of 4.11.0. Please opt + in to the new connection logic using the `useMongoClient` option, but + make sure you test your connections first if you're upgrading an existing + codebase! + :js + // Using `mongoose.connect`... + var promise = mongoose.connect('mongodb://localhost/myapp', { + useMongoClient: true, + /* other options */ + }); + // Or `createConnection` + var promise = mongoose.createConnection('mongodb://localhost/myapp', { + useMongoClient: true, + /* other options */ + }); + promise.then(function(db) { + /* Use `db`, for instance `db.model()` + }); + // Or, if you already have a connection + connection.openUri('mongodb://localhost/myapp', { /* options */ }); + :markdown + The parameters to `openUri()` are passed transparently to the + [underlying MongoDB driver's `MongoClient.connect()` function](http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html#connect). + Please see the [driver documentation for this function](http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html#connect) for options. + The same is true for `connect()` and `createConnection()` if `useMongoClient` is `true`. + :markdown + You may see the following deprecation warning with `useMongoClient`: + + ``` + the server/replset/mongos options are deprecated, all their options are supported at the top level of the options object + ``` + + In older version of the MongoDB driver you had to specify distinct options for server connections, replica set connections, and mongos connections: + :js + mongoose.connect(myUri, { + server: { + socketOptions: { + socketTimeoutMS: 0, + keepAlive: true + }, + reconnectTries: 30 + }, + replset: { + socketOptions: { + socketTimeoutMS: 0, + keepAlive: true + }, + reconnectTries: 30 + }, + mongos: { + socketOptions: { + socketTimeoutMS: 0, + keepAlive: true + }, + reconnectTries: 30 + } + }); + :markdown + With `useMongoClient` you can instead declare these options at the top level, without all that extra nesting. [Here's the list of all supported options](http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html). + :js + // Equivalent to the above code + mongoose.connect(myUri, { + socketTimeoutMS: 0, + keepAlive: true, + reconnectTries: 30 + }); + + :markdown + This deprecation is because the [MongoDB driver](https://www.npmjs.com/package/mongodb) + has deprecated an API that is critical to mongoose's connection logic to + support MongoDB 3.6, see [this github issue](https://github.com/Automattic/mongoose/issues/5304) + and [this blog post](http://thecodebarbarian.com/mongoose-4.11-use-mongo-client.html) + for more details. + h3#next Next Up :markdown Now that we've covered `connections`, let's take a look at how we can break pieces of our functionality out into reusable and shareable [plugins](/docs/plugins.html). diff --git a/docs/contributing.html b/docs/contributing.html index 3e20d34c244..26e45d6954f 100644 --- a/docs/contributing.html +++ b/docs/contributing.html @@ -1,9 +1,26 @@ -Mongoose Contributing v3.3.1Fork me on GitHub

Contributing

Please read all about contributing here.

\ No newline at end of file +KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/documents.html b/docs/documents.html index e0a06b86036..520898e9d05 100644 --- a/docs/documents.html +++ b/docs/documents.html @@ -1,20 +1,59 @@ -Mongoose Documents v3.3.1Fork me on GitHub

Documents

Mongoose documents represent a one-to-one mapping to documents as stored in MongoDB. Each document is an instance of its Model.

Retrieving

There are many ways to retrieve documents from MongoDB. We won't cover that in this section. See the chapter on querying for detail.

Updating

There are a number of ways to update documents. We'll first look at a traditional approach using findById:

Tank.findById(id, function (err, tank) {
+Mongoose Documents v4.11.9Fork me on GitHub

Documents

Mongoose documents represent a one-to-one mapping to documents as stored in MongoDB. Each document is an instance of its Model.

Retrieving

There are many ways to retrieve documents from MongoDB. We won't cover that in this section. See the chapter on querying for detail.

Updating

There are a number of ways to update documents. We'll first look at a +traditional approach using findById:

Tank.findById(id, function (err, tank) {
   if (err) return handleError(err);
   
   tank.size = 'large';
-  tank.save(function (err) {
+  tank.save(function (err, updatedTank) {
     if (err) return handleError(err);
-    res.send(tank);
+    res.send(updatedTank);
   });
-});

This approach involves first retreiving the document from Mongo, then issuing an update command (triggered by calling save). However, if we don't need the document returned in our application and merely want to update a property in the database directly, Model#update is right for us:

Tank.update({ _id: id }, { $set: { size: 'large' }}, callback);

If we do need the document returned in our application there is another, often better, option:

Tank.findByIdAndUpdate(id, { $set: { size: 'large' }}, function (err, tank) {
+});

You can also use .set() +to modify documents. Under the hood, tank.size = 'large'; becomes tank.set({ size: 'large' }).

Tank.findById(id, function (err, tank) {
+  if (err) return handleError(err);
+  
+  tank.set({ size: 'large' });
+  tank.save(function (err, updatedTank) {
+    if (err) return handleError(err);
+    res.send(updatedTank);
+  });
+});

This approach involves first retrieving the document from Mongo, then +issuing an update command (triggered by calling save). However, if we +don't need the document returned in our application and merely want to +update a property in the database directly, +Model#update is right for us:

Tank.update({ _id: id }, { $set: { size: 'large' }}, callback);

If we do need the document returned in our application there is another, often better, option:

Tank.findByIdAndUpdate(id, { $set: { size: 'large' }}, { new: true }, function (err, tank) {
   if (err) return handleError(err);
   res.send(tank);
-});

The findAndUpdate/Remove static methods all make a change to at most one document, and return it with just one call to the database. There are several variations on the findAndModify theme. Read the API docs for more detail.

Validating

Documents are validated before they are saved. Read the api docs or the validation chapter for detail.

Next Up

Now that we've covered Documents, let's take a look at Sub-documents.

\ No newline at end of file +

_Note that findAndUpdate/Remove do not execute any hooks or validation before making the change in the database. You can use the runValidators option to access a limited subset of document validation. However, if you need hooks and full document validation, first query for the document and then save() it._

Validating

Documents are validated before they are saved. Read the api docs or the validation chapter for detail.

Overwriting

You can overwrite an entire document using .set(). This is handy if you +want to change what document is being saved in middleware.

Tank.findById(id, function (err, tank) {
+  if (err) return handleError(err);
+  // Now `otherTank` is a copy of `tank`
+  otherTank.set(tank);
+});
+

Next Up

Now that we've covered Documents, let's take a look at Sub-documents.

\ No newline at end of file diff --git a/docs/documents.jade b/docs/documents.jade index b16531c8722..17cb210e26d 100644 --- a/docs/documents.jade +++ b/docs/documents.jade @@ -9,35 +9,66 @@ block content There are many ways to retrieve documents from MongoDB. We won't cover that in this section. See the chapter on [querying](./queries.html) for detail. h3 Updating :markdown - There are a number of ways to update documents. We'll first look at a traditional approach using [findById](./api.html#model_Model.findById): + There are a number of ways to update documents. We'll first look at a + traditional approach using [findById](./api.html#model_Model.findById): :js Tank.findById(id, function (err, tank) { if (err) return handleError(err); tank.size = 'large'; - tank.save(function (err) { + tank.save(function (err, updatedTank) { if (err) return handleError(err); - res.send(tank); + res.send(updatedTank); }); }); :markdown - This approach involves first retreiving the document from Mongo, then issuing an update command (triggered by calling `save`). However, if we don't need the document returned in our application and merely want to update a property in the database directly, [Model#update](./api.html#model_Model.update) is right for us: + You can also use [`.set()`](./api.html#document_Document-set) + to modify documents. Under the hood, `tank.size = 'large';` becomes `tank.set({ size: 'large' })`. + :js + Tank.findById(id, function (err, tank) { + if (err) return handleError(err); + + tank.set({ size: 'large' }); + tank.save(function (err, updatedTank) { + if (err) return handleError(err); + res.send(updatedTank); + }); + }); + :markdown + This approach involves first retrieving the document from Mongo, then + issuing an update command (triggered by calling `save`). However, if we + don't need the document returned in our application and merely want to + update a property in the database directly, + [Model#update](./api.html#model_Model.update) is right for us: :js Tank.update({ _id: id }, { $set: { size: 'large' }}, callback); :markdown If we do need the document returned in our application there is another, often [better](./api.html#model_Model.findByIdAndUpdate), option: :js - Tank.findByIdAndUpdate(id, { $set: { size: 'large' }}, function (err, tank) { + Tank.findByIdAndUpdate(id, { $set: { size: 'large' }}, { new: true }, function (err, tank) { if (err) return handleError(err); res.send(tank); }); :markdown - The `findAndUpdate/Remove` static methods all make a change to at most one document, and return it with just one call to the database. There [are](./api.html#model_Model.findByIdAndRemove) [several](./api.html#model_Model.findOneAndUpdate) [variations](./api.html#model_Model.findOneAndRemove) on the [findAndModify](http://www.mongodb.org/display/DOCS/findAndModify+Command) theme. Read the [API](./api.html) docs for more detail. _Note that `findAndUpdate/Remove` do not execute any hooks or validation before making the change in the database. If you need hooks and validation, first query for the document and then save it._ + The `findAndUpdate/Remove` static methods all make a change to at most one document, and return it with just one call to the database. There [are](./api.html#model_Model.findByIdAndRemove) [several](./api.html#model_Model.findOneAndUpdate) [variations](./api.html#model_Model.findOneAndRemove) on the [findAndModify](http://www.mongodb.org/display/DOCS/findAndModify+Command) theme. Read the [API](./api.html) docs for more detail. + + _Note that `findAndUpdate/Remove` do *not* execute any hooks or validation before making the change in the database. You can use the [`runValidators` option](/docs/validation.html#update-validators) to access a limited subset of document validation. However, if you need hooks and full document validation, first query for the document and then `save()` it._ h3 Validating :markdown Documents are validated before they are saved. Read the [api](./api.html#document_Document-validate) docs or the [validation](./validation.html) chapter for detail. + h3 Overwriting + :markdown + You can overwrite an entire document using `.set()`. This is handy if you + want to change what document is being saved in [middleware](./docs/middleware.html). + :js + Tank.findById(id, function (err, tank) { + if (err) return handleError(err); + // Now `otherTank` is a copy of `tank` + otherTank.set(tank); + }); + h3#next Next Up :markdown Now that we've covered `Documents`, let's take a look at [Sub-documents](/docs/subdocs.html). diff --git a/docs/faq.html b/docs/faq.html index e8432a0665e..35c16c6c762 100644 --- a/docs/faq.html +++ b/docs/faq.html @@ -1,31 +1,145 @@ -Mongoose FAQ v3.3.1Fork me on GitHub

FAQ

Q. Why don't my changes to arrays get saved when I update an element directly?

doc.array[3] = 'changed';
-doc.save();

A. Mongoose doesn't create getters/setters for array indexes; without them mongoose never gets notified of the change and so doesn't know to persist the new value. The work-around is to first mark the path of the array modified before saving.

doc.markModified('array');
-doc.save();

Q. Why doesn't mongoose allow me to directly assign schemas to paths?

var userSchema = new Schema({ name: String });
-new Schema({ user: userSchema })

A. Schemas have a one-to-one mapping with documents. Documents have save and remove methods along with their own pre and post hooks which would lead to code like the following:

doc.user.save();  // ?
-doc.user.remove();// ?
-doc.save()

We've felt that this api would be more confusing than helpful. The counter argument is that arrays of sub-documents already have this functionality, but at best this too leads to confusion (calling save on a sub-document is a no-op and exists only to support pre save hooks). In the future this is likely to be revisited.


Q. How can I enable debugging?

+Fork me on GitHub

FAQ

Q. Why don't my changes to arrays get saved when I update an element +directly?

doc.array[3] = 'changed';
+doc.save();

A. Mongoose doesn't create getters/setters for array indexes; without +them mongoose never gets notified of the change and so doesn't know to +persist the new value. The work-around is to use +MongooseArray#set available +in Mongoose >= 3.2.0.

// 3.2.0
+doc.array.set(3, 'changed');
+doc.save();
 
-

A. Set the debug option to true:

mongoose.set('debug', true)

Q. My save() callback never executes. What am I doing wrong?

+// if running a version less than 3.2.0, you must mark the array modified before saving. +doc.array[3] = 'changed'; +doc.markModified('array'); +doc.save(); +

Q. I declared a schema property as unique but I can still save duplicates. What gives?

-

A. All collection actions (insert, remove, queries, etc) are queued until the connection opens. It is likely that an error occurred while attempting to connect. Try adding an error handler to your connection.

// if connecting on the default mongoose connection
+

A. Mongoose doesn't handle unique on it's own, { name: { type: String, unique: true } } +just a shorthand for creating a MongoDB unique index on name. +For example, if MongoDB doesn't already have a unique index on name, the below code will not error despite the fact that unique is true.

var schema = new mongoose.Schema({
+  name: { type: String, unique: true }
+});
+var Model = db.model('Test', schema);
+
+Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) {
+  console.log(err); // No error, unless index was already built
+});

However, if you wait for the index to build using the Model.on('index') event, attempts to save duplicates will correctly error.

var schema = new mongoose.Schema({
+  name: { type: String, unique: true }
+});
+var Model = db.model('Test', schema);
+
+Model.on('index', function(err) { // <-- Wait for model's indexes to finish
+  assert.ifError(err);
+  Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) {
+    console.log(err);
+  });
+});

MongoDB persists indexes, so you only need to rebuild indexes if you're starting +with a fresh database or you ran db.dropDatabase(). In a production environment, +you should [create your indexes using the MongoDB shell])(https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/) +rather than relying on mongoose to do it for you. The unique option for schemas is +convenient for development and documentation, but mongoose is not an index management solution.


Q. When I have a nested property in a schema, mongoose adds empty objects by default. Why?

var schema = new mongoose.Schema({
+  nested: {
+    prop: String
+  }
+});
+var Model = db.model('Test', schema);
+
+// The below prints `{ _id: /* ... */, nested: {} }`, mongoose assigns
+// `nested` to an empty object `{}` by default.
+console.log(new Model());

A. This is a performance optimization. These empty objects are not saved +to the database, nor are they in the result toObject(), nor do they show +up in JSON.stringify() output unless you turn off the minimize option.

+ +

The reason for this behavior is that Mongoose's change detection +and getters/setters are based on Object.defineProperty(). +In order to support change detection on nested properties without incurring +the overhead of running Object.defineProperty() every time a document is created, +mongoose defines properties on the Model prototype when the model is compiled. +Because mongoose needs to define getters and setters for nested.prop, nested +must always be defined as an object on a mongoose document, even if nested +is undefined on the underlying POJO.


Q. Why don't in-place modifications to date objects +(e.g. date.setMonth(1);) get saved?

doc.createdAt.setDate(2011, 5, 1);
+doc.save(); // createdAt changes won't get saved!

A. Mongoose currently doesn't watch for in-place updates to date +objects. If you have need for this feature, feel free to discuss on +this GitHub issue. +There are several workarounds:

doc.createdAt.setDate(2011, 5, 1);
+doc.markModified('createdAt');
+doc.save(); // Works
doc.createdAt = new Date(2011, 5, 1).setHours(4);
+doc.save(); // Works
+

Q. I'm populating a nested property under an array like the below code:

+ +

+ new Schema({ + arr: [{ + child: { ref: 'OtherModel', type: Schema.Types.ObjectId } + }] + }); +

+ +

.populate({ path: 'arr.child', options: { sort: 'name' } }) won't sort +by arr.child.name?

A. See this GitHub issue. It's a known issue but one that's exceptionally difficult to fix.


Q. All function calls on my models hang, what am I doing wrong?

+ +

A. By default, mongoose will buffer your function calls until it can +connect to MongoDB. Read the buffering section of the connection docs +for more information.


Q. How can I enable debugging?

+ +

A. Set the debug option to true:

mongoose.set('debug', true)

All executed collection methods will log output of their arguments to your console.


Q. My save() callback never executes. What am I doing wrong?

+ +

A. All collection actions (insert, remove, queries, etc.) are queued until the connection opens. It is likely that an error occurred while attempting to connect. Try adding an error handler to your connection.

// if connecting on the default mongoose connection
 mongoose.connect(..);
 mongoose.connection.on('error', handleError);
 
 // if connecting on a separate connection
 var conn = mongoose.createConnection(..);
-conn.on('error', handleError);
+conn.on('error', handleError);

Q. Should I create/destroy a new connection for each database operation?

+ +

A. No. Open your connection when your application starts up and leave it open until the application shuts down.


Q. Why do I get "OverwriteModelError: Cannot overwrite .. model once +compiled" when I use nodemon / a testing framework?

+ +

A. mongoose.model('ModelName', schema) requires 'ModelName' to be +unique, so you can access the model by using mongoose.model('ModelName'). +If you put mongoose.model('ModelName', schema); in a +mocha beforeEach() hook, this code will +attempt to create a new model named 'ModelName' before every test, +and so you will get an error. Make sure you only create a new model with +a given name once. If you need to create multiple models with the +same name, create a new connection and bind the model to the connection.

var mongoose = require('mongoose');
+var connection = mongoose.createConnection(..);
+
+// use mongoose.Schema
+var kittySchema = mongoose.Schema({ name: String });
+
+// use connection.model
+var Kitten = connection.model('Kitten', kittySchema);
 

Something to add?

-

If you'd like to contribute to this page, please visit it on github and use the Edit button to send a pull request.


\ No newline at end of file +KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/faq.jade b/docs/faq.jade index 1300fc9bf2d..d3c879c2c87 100644 --- a/docs/faq.jade +++ b/docs/faq.jade @@ -10,12 +10,17 @@ block append style block content h2 FAQ :markdown - **Q**. Why don't my changes to arrays get saved when I update an element directly? + **Q**. Why don't my changes to arrays get saved when I update an element + directly? :js doc.array[3] = 'changed'; doc.save(); :markdown - **A**. Mongoose doesn't create getters/setters for array indexes; without them mongoose never gets notified of the change and so doesn't know to persist the new value. The work-around is to use [MongooseArray#set](./api.html#types_array_MongooseArray-set) available in **Mongoose >= 3.2.0**. + **A**. Mongoose doesn't create getters/setters for array indexes; without + them mongoose never gets notified of the change and so doesn't know to + persist the new value. The work-around is to use + [MongooseArray#set](./api.html#types_array_MongooseArray.set) available + in **Mongoose >= 3.2.0**. :js // 3.2.0 doc.array.set(3, 'changed'); @@ -25,20 +30,194 @@ block content doc.array[3] = 'changed'; doc.markModified('array'); doc.save(); - hr#assign_schemas_to_paths + + hr#unique-doesnt-work + :markdown + **Q**. I declared a schema property as `unique` but I can still save duplicates. What gives? + + **A**. Mongoose doesn't handle `unique` on its own: `{ name: { type: String, unique: true } }` + is just a shorthand for creating a [MongoDB unique index on `name`](https://docs.mongodb.com/manual/core/index-unique/). + For example, if MongoDB doesn't already have a unique index on `name`, the below code will not error despite the fact that `unique` is true. + :js + var schema = new mongoose.Schema({ + name: { type: String, unique: true } + }); + var Model = db.model('Test', schema); + + Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) { + console.log(err); // No error, unless index was already built + }); :markdown - **Q**. Why doesn't mongoose allow me to directly assign schemas to paths? + However, if you wait for the index to build using the `Model.on('index')` event, attempts to save duplicates will correctly error. :js - var userSchema = new Schema({ name: String }); - new Schema({ user: userSchema }) + var schema = new mongoose.Schema({ + name: { type: String, unique: true } + }); + var Model = db.model('Test', schema); + + Model.on('index', function(err) { // <-- Wait for model's indexes to finish + assert.ifError(err); + Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) { + console.log(err); + }); + }); + + // Promise based alternative. `init()` returns a promise that resolves + // when the indexes have finished building successfully. The `init()` + // function is idempotent, so don't worry about triggering an index rebuild. + Model.init().then(function() { + assert.ifError(err); + Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) { + console.log(err); + }); + }); + :markdown + MongoDB persists indexes, so you only need to rebuild indexes if you're starting + with a fresh database or you ran `db.dropDatabase()`. In a production environment, + you should [create your indexes using the MongoDB shell])(https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/) + rather than relying on mongoose to do it for you. The `unique` option for schemas is + convenient for development and documentation, but mongoose is *not* an index management solution. + + hr#nested-properties :markdown - **A**. Schemas have a one-to-one mapping with documents. Documents have `save` and `remove` methods along with their own `pre` and `post` hooks which would lead to code like the following: + **Q**. When I have a nested property in a schema, mongoose adds empty objects by default. Why? :js - doc.user.save(); // ? - doc.user.remove();// ? - doc.save() + var schema = new mongoose.Schema({ + nested: { + prop: String + } + }); + var Model = db.model('Test', schema); + + // The below prints `{ _id: /* ... */, nested: {} }`, mongoose assigns + // `nested` to an empty object `{}` by default. + console.log(new Model()); :markdown - We've felt that this api would be more confusing than helpful. The counter argument is that arrays of sub-documents already have this functionality, but at best this too leads to confusion (calling `save` on a sub-document is a no-op and exists only to support `pre` save hooks). In the future this is likely to be revisited. + **A**. This is a performance optimization. These empty objects are not saved + to the database, nor are they in the result `toObject()`, nor do they show + up in `JSON.stringify()` output unless you turn off the [`minimize` option](./guide.html#minimize). + + The reason for this behavior is that Mongoose's change detection + and getters/setters are based on [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). + In order to support change detection on nested properties without incurring + the overhead of running `Object.defineProperty()` every time a document is created, + mongoose defines properties on the `Model` prototype when the model is compiled. + Because mongoose needs to define getters and setters for `nested.prop`, `nested` + must always be defined as an object on a mongoose document, even if `nested` + is undefined on the underlying [POJO](./guide.html#minimize). + + hr#arrow-functions + :markdown + **Q**. I'm using an arrow function for a [virtual](./guide.html#virtuals), getter/setter, or [method](./guide.html#methods) and the value of `this` is wrong. + + **A**. Arrow functions [handle the `this` keyword much differently than conventional functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_binding_of_this). + Mongoose getters/setters depend on `this` to give you access to the document that you're writing to, but this functionality does not work with arrow functions. Do **not** use arrow functions for mongoose getters/setters unless do not intend to access the document in the getter/setter. + :js + // Do **NOT** use arrow functions as shown below unless you're certain + // that's what you want. If you're reading this FAQ, odds are you should + // just be using a conventional function. + var schema = new mongoose.Schema({ + propWithGetter: { + type: String, + get: v => { + // Will **not** be the doc, do **not** use arrow functions for getters/setters + console.log(this); + return v; + } + } + }); + + // `this` will **not** be the doc, do **not** use arrow functions for methods + schema.method.arrowMethod = () => this; + schema.virtual('virtualWithArrow').get(() => { + // `this` will **not** be the doc, do **not** use arrow functions for virtuals + console.log(this); + }); + + hr#type-key + :markdown + **Q**. I have an embedded property named `type` like this: + :js + const holdingSchema = new Schema({ + // You might expect `asset` to be an object that has 2 properties, + // but unfortunately `type` is special in mongoose so mongoose + // interprets this schema to mean that `asset` is a string + asset: { + type: String, + ticker: String + } + }); + :markdown + But mongoose gives me a CastError telling me that it can't cast an object + to a string when I try to save a `Holding` with an `asset` object. Why + is this? + :js + Holding.create({ asset: { type: 'stock', ticker: 'MDB' } }).catch(error => { + // Cast to String failed for value "{ type: 'stock', ticker: 'MDB' }" at path "asset" + console.error(error); + }); + :markdown + **A**. The `type` property is special in mongoose, so when you say + `type: String`, mongoose interprets it as a type declaration. In the + above schema, mongoose thinks `asset` is a string, not an object. Do + this instead: + :js + const holdingSchema = new Schema({ + // This is how you tell mongoose you mean `asset` is an object with + // a string property `type`, as opposed to telling mongoose that `asset` + // is a string. + asset: { + type: { type: String }, + ticker: String + } + }); + + hr#date_changes + :markdown + **Q**. Why don't in-place modifications to date objects + (e.g. `date.setMonth(1);`) get saved? + :js + doc.createdAt.setDate(2011, 5, 1); + doc.save(); // createdAt changes won't get saved! + :markdown + **A**. Mongoose currently doesn't watch for in-place updates to date + objects. If you have need for this feature, feel free to discuss on + [this GitHub issue](https://github.com/Automattic/mongoose/issues/3738). + There are several workarounds: + :js + doc.createdAt.setDate(2011, 5, 1); + doc.markModified('createdAt'); + doc.save(); // Works + :js + doc.createdAt = new Date(2011, 5, 1).setHours(4); + doc.save(); // Works + + hr#populate_sort_order + :markdown + **Q**. I'm populating a nested property under an array like the below code: + + ``` + new Schema({ + arr: [{ + child: { ref: 'OtherModel', type: Schema.Types.ObjectId } + }] + }); + ``` + + `.populate({ path: 'arr.child', options: { sort: 'name' } })` won't sort + by `arr.child.name`? + + :markdown + **A**. See [this GitHub issue](https://github.com/Automattic/mongoose/issues/2202). It's a known issue but one that's exceptionally difficult to fix. + + hr#model_functions_hanging + :markdown + **Q**. All function calls on my models hang, what am I doing wrong? + + **A**. By default, mongoose will buffer your function calls until it can + connect to MongoDB. Read the [buffering section of the connection docs](./connections.html#buffering) + for more information. + hr#enable_debugging :markdown **Q**. How can I enable debugging? @@ -52,7 +231,7 @@ block content :markdown **Q**. My `save()` callback never executes. What am I doing wrong? - **A**. All `collection` actions (insert, remove, queries, etc) are queued until the `connection` opens. It is likely that an error occurred while attempting to connect. Try adding an error handler to your connection. + **A**. All `collection` actions (insert, remove, queries, etc.) are queued until the `connection` opens. It is likely that an error occurred while attempting to connect. Try adding an error handler to your connection. :js // if connecting on the default mongoose connection mongoose.connect(..); @@ -66,6 +245,28 @@ block content **Q**. Should I create/destroy a new connection for each database operation? **A**. No. Open your connection when your application starts up and leave it open until the application shuts down. + hr#overwrite-model-error + :markdown + **Q**. Why do I get "OverwriteModelError: Cannot overwrite .. model once + compiled" when I use nodemon / a testing framework? + + **A**. `mongoose.model('ModelName', schema)` requires 'ModelName' to be + unique, so you can access the model by using `mongoose.model('ModelName')`. + If you put `mongoose.model('ModelName', schema);` in a + [mocha `beforeEach()` hook](https://mochajs.org/#hooks), this code will + attempt to create a new model named 'ModelName' before **every** test, + and so you will get an error. Make sure you only create a new model with + a given name **once**. If you need to create multiple models with the + same name, create a new connection and bind the model to the connection. + :js + var mongoose = require('mongoose'); + var connection = mongoose.createConnection(..); + + // use mongoose.Schema + var kittySchema = mongoose.Schema({ name: String }); + + // use connection.model + var Kitten = connection.model('Kitten', kittySchema); hr :markdown @@ -73,4 +274,3 @@ block content If you'd like to contribute to this page, please [visit it](https://github.com/Automattic/mongoose/tree/master/docs/faq.jade) on github and use the [Edit](https://github.com/blog/844-forking-with-the-edit-button) button to send a pull request. br - diff --git a/docs/guide.html b/docs/guide.html index 64c442adae9..02d5975ea96 100644 --- a/docs/guide.html +++ b/docs/guide.html @@ -1,4 +1,8 @@ -Mongoose Schemas v3.3.1Fork me on GitHub

Schemas

If you haven't yet done so, please take a minute to read the quickstart to get an idea of how Mongoose works.

If you are migrating from 2.x to 3.x please take a moment to read the migration guide.

This page covers Schema definition, plugins, instance methods, statics, indexes, virtuals and options. Let's start with Schema definition.

Defining your schema

Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.

var blogSchema = new Schema({
+Mongoose Schemas v4.11.9Fork me on GitHub

Schemas

If you haven't yet done so, please take a minute to read the quickstart to get an idea of how Mongoose works. +If you are migrating from 3.x to 4.x please take a moment to read the migration guide.

Defining your schema

Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.

var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var blogSchema = new Schema({
   title:  String,
   author: String,
   body:   String,
@@ -11,50 +15,119 @@
   }
 });

If you want to add additional keys later, use the Schema#add method.

Each key in our blogSchema defines a property in our documents which will be cast to its associated SchemaType. For example, we've defined a title which will be cast to the String SchemaType and date which will be cast to a Date SchemaType. -Keys may also be assigned nested objects containing further key/type definitions (e.g. the `meta` property above).

The permitted SchemaTypes are

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array
Read more about them here.

Schemas not only define the structure of your document and casting of properties, they also define document instance methods, static Model methods, compound indexes and document lifecycle hooks called middleware.

Pluggable

Schemas are pluggable which allows us to package up reusable features into plugins that can be shared with the community or just between your projects.

Instance methods

Models are just fancy constructor functions. As such they can have prototype methods inherited by their instances. In the case of Mongoose, instances are documents.

Defining an instance method is easy.

var animalSchema = new Schema({ name: String, type: String });
+Keys may also be assigned nested objects containing further key/type definitions (e.g. the `meta` property above).

The permitted SchemaTypes are

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array
Read more about them here.

Schemas not only define the structure of your document and casting of properties, they also define document instance methods, static Model methods, compound indexes and document lifecycle hooks called middleware.

Creating a model

To use our schema definition, we need to convert our blogSchema into a Model we can work with. +To do so, we pass it into mongoose.model(modelName, schema):

var Blog = mongoose.model('Blog', blogSchema);
+// ready to go!
+

Instance methods

Instances of Models are documents. Documents have many of their own built-in instance methods. We may also define our own custom document instance methods too.

// define a schema
+var animalSchema = new Schema({ name: String, type: String });
 
-animalSchema.methods.findSimilarTypes = function (cb) {
+// assign a function to the "methods" object of our animalSchema
+animalSchema.methods.findSimilarTypes = function(cb) {
   return this.model('Animal').find({ type: this.type }, cb);
-}

Now all of our animal instances have a findSimilarTypes method available to it.

var Animal = mongoose.model('Animal', animalSchema);
+};

Now all of our animal instances have a findSimilarTypes method available to it.

var Animal = mongoose.model('Animal', animalSchema);
 var dog = new Animal({ type: 'dog' });
 
-dog.findSimilarTypes(function (err, dogs) {
+dog.findSimilarTypes(function(err, dogs) {
   console.log(dogs); // woof
-});

Statics

-Adding static constructor methods to Models is simple as well. Continuing with our animalSchema:

animalSchema.statics.findByName = function (name, cb) {
-  this.find({ name: new RegExp(name, 'i') }, cb);
-}
+});
+

Overwriting a default mongoose document method may lead to unpredictable results. See this for more details.

Do not declare methods using ES6 arrow functions (=>). Arrow functions explicitly prevent binding this, so your method will not have access to the document and the above examples will not work.

Statics

Adding static methods to a Model is simple as well. Continuing with our animalSchema:

// assign a function to the "statics" object of our animalSchema
+animalSchema.statics.findByName = function(name, cb) {
+  return this.find({ name: new RegExp(name, 'i') }, cb);
+};
+
+var Animal = mongoose.model('Animal', animalSchema);
+Animal.findByName('fido', function(err, animals) {
+  console.log(animals);
+});
+

Do not declare statics using ES6 arrow functions (=>). Arrow functions explicitly prevent binding this, so the above examples will not work because of the value of this.

Query Helpers

You can also add query helper functions, which are like instance methods +but for mongoose queries. Query helper methods let you extend mongoose's +chainable query builder API.

animalSchema.query.byName = function(name) {
+  return this.find({ name: new RegExp(name, 'i') });
+};
 
 var Animal = mongoose.model('Animal', animalSchema);
-Animal.findByName('fido', function (err, animals) {
+Animal.find().byName('fido').exec(function(err, animals) {
   console.log(animals);
-});

Indexes

Indexes can be defined at the path level or the schema level. Defining indexes at the schema level is necessary when defining compound indexes.

animalSchema.index({ name: 1, type: -1 });
-

When your application starts up, Mongoose automatically calls ensureIndex for each defined index. It is recommended this behavior be disabled in production by setting the autoIndex option of your schema to false.

animalSchema.set('autoIndex', false);
+});
+

Indexes

MongoDB supports secondary indexes. +With mongoose, we define these indexes within our Schema at the path level or the schema level. +Defining indexes at the schema level is necessary when creating +compound indexes.

var animalSchema = new Schema({
+  name: String,
+  type: String,
+  tags: { type: [String], index: true } // field level
+});
+
+animalSchema.index({ name: 1, type: -1 }); // schema level
+

When your application starts up, Mongoose automatically calls ensureIndex for each defined index in your schema. +Mongoose will call ensureIndex for each index sequentially, and emit an 'index' event on the model when all the ensureIndex calls succeeded or when there was an error. +While nice for development, it is recommended this behavior be disabled in production since index creation can cause a significant performance impact. Disable the behavior by setting the autoIndex option of your schema to false, or globally on the connection by setting the option config.autoIndex to false.

mongoose.connect('mongodb://user:pass@localhost:port/database', { config: { autoIndex: false } });
+// or  
+mongoose.createConnection('mongodb://user:pass@localhost:port/database', { config: { autoIndex: false } });
+// or
+animalSchema.set('autoIndex', false);
 // or
 new Schema({..}, { autoIndex: false });
-

See also the Model#ensureIndexes method for more details.

Virtuals

Virtual attributes are attributes that are convenient to have around but that do not get persisted to MongoDB.

var personSchema = new Schema({
+

Mongoose will emit an index event on the model when indexes are done +building or an error occurred.

// Will cause an error because mongodb has an _id index by default that
+// is not sparse
+animalSchema.index({ _id: 1 }, { sparse: true });
+var Animal = mongoose.model('Animal', animalSchema);
+
+Animal.on('index', function(error) {
+  // "_id index cannot be sparse"
+  console.log(error.message);
+});
+

See also the Model#ensureIndexes method.

Virtuals

Virtuals are document properties that you can get and set but that do not get persisted to MongoDB. The getters are useful for formatting or combining fields, while setters are useful for de-composing a single value into multiple values for storage.

// define a schema
+var personSchema = new Schema({
   name: {
     first: String,
     last: String
   }
 });
 
+// compile our model
 var Person = mongoose.model('Person', personSchema);
 
-var bad = new Person({
-    name: { first: 'Walter', last: 'White' }
-});

Suppose we want to log the full name of bad. We could do this manually like so:

console.log(bad.name.first + ' ' + bad.name.last); // Walter White

Or we could add a virtual attribute getter to our personSchema so we don't need to write out this string concatenation mess each time:

personSchema.virtual('name.full').get(function () {
+// create a document
+var axl = new Person({
+  name: { first: 'Axl', last: 'Rose' }
+});
+

Suppose you want to print out the person's full name. You could do it yourself:

console.log(axl.name.first + ' ' + axl.name.last); // Axl Rose
+

But concatenating the first and last name every time can get cumbersome. And what if you want to do some extra processing on the name, like removing diacritics?. A virtual property getter lets you define a fullName property that won't get persisted to MongoDB.

personSchema.virtual('fullName').get(function () {
   return this.name.first + ' ' + this.name.last;
-});

Now, when we access our virtual full name property, our getter function will be invoked and the value returned:

console.log('%s is insane', bad.name.full); // Walter White is insane

It would also be nice to be able to set this.name.first and this.name.last by setting this.name.full. For example, if we wanted to change bad's name.first and name.last to 'Breaking' and 'Bad' respectively, it'd be nice to just:

bad.name.full = 'Breaking Bad';

Mongoose let's you do this as well through its virtual attribute setters:

personSchema.virtual('name.full').set(function (name) {
-  var split = name.split(' ');
-  this.name.first = split[0];
-  this.name.last = split[1];
 });
+

Now, mongoose will call your getter function every time you access the fullName property:

console.log(axl.fullName); // Axl Rose
+

If you use toJSON() or toObject() (or use JSON.stringify() on a mongoose document) mongoose will not include virtuals by default. +Pass { virtuals: true } to either toObject() or toJSON().

-... +

You can also add a custom setter to your virtual that will let you set both first name and last name via the fullName virtual.

personSchema.virtual('fullName').
+  get(function() { return this.name.first + ' ' + this.name.last; }).
+  set(function(v) {
+    this.name.first = v.substr(0, v.indexOf(' '));
+    this.name.last = v.substr(v.indexOf(' ') + 1);
+  });
+  
+axl.fullName = 'William Rose'; // Now `axl.name.first` is "William"
+

Virtual property setters are applied before other validation. So the example above would still work even if the first and last name fields were required.

-mad.name.full = 'Breaking Bad'; -console.log(mad.name.first); // Breaking

If you need attributes that you can get and set but that are not themselves persisted to MongoDB, virtual attributes is the Mongoose feature for you.

Options

Schemas have a few configurable options which can be passed to the constructor or set directly:

new Schema({..}, options);
+

Only non-virtual properties work as part of queries and for field selection. Since virtuals are not stored in MongoDB, you can't query with them.

Aliases

Aliases are a particular type of virtual where the getter and setter seamlessly get and set another property. This is handy for saving network bandwidth, so you can convert a short property name stored in the database into a longer name for code readability.

var personSchema = new Schema({
+  n: {
+    type: String,
+    // Now accessing `name` will get you the value of `n`, and setting `n` will set the value of `name`
+    alias: 'name'
+  }
+});
+
+// Setting `name` will propagate to `n`
+var person = new Person({ name: 'Val' });
+console.log(person); // { n: 'Val' }
+console.log(person.toObject({ virtuals: true })); // { n: 'Val', name: 'Val' }
+console.log(person.name); // "Val"
+
+person.name = 'Not Val';
+console.log(person); // { n: 'Not Val' }
+

Options

Schemas have a few configurable options which can be passed to the constructor or set directly:

new Schema({..}, options);
 
 // or
 
@@ -62,35 +135,82 @@
 schema.set(option, value);
 

Valid options:

-

option: autoIndex

At application startup, Mongoose sends an ensureIndex command for each index declared in your Schema. As of Mongoose v3, indexes are created in the background by default. If you wish to disable the auto-creation feature and manually handle when indexes are created, set your Schemas autoIndex option to false and use the ensureIndexes method on your model.

var schema = new Schema({..}, { autoIndex: false });
-var Clock = db.model('Clock', schema);
+

option: autoIndex

At application startup, Mongoose sends an ensureIndex command for each index declared in your Schema. As of Mongoose v3, indexes are created in the background by default. If you wish to disable the auto-creation feature and manually handle when indexes are created, set your Schemas autoIndex option to false and use the ensureIndexes method on your model.

var schema = new Schema({..}, { autoIndex: false });
+var Clock = mongoose.model('Clock', schema);
 Clock.ensureIndexes(callback);
+

option: bufferCommands

By default, mongoose buffers commands when the connection goes down until +the driver manages to reconnect. To disable buffering, set bufferCommands +to false.

var schema = new Schema({..}, { bufferCommands: false });
 

option: capped

Mongoose supports MongoDBs capped collections. To specify the underlying MongoDB collection be capped, set the capped option to the maximum size of the collection in bytes.

new Schema({..}, { capped: 1024 });

The capped option may also be set to an object if you want to pass additional options like max or autoIndexId. In this case you must explicitly pass the size option which is required.

new Schema({..}, { capped: { size: 1024, max: 1000, autoIndexId: true } });
-

option: collection

Mongoose by default produces a collection name by passing the model name to the utils.toCollectionName method. This method pluralizes the name. Set this option if you need a different name for your collection.

var dataSchema = new Schema({..}, { collection: 'data' });
-

option: id

Mongoose assigns each of your schemas an id virtual getter by default which returns the documents _id field cast to a string, or in the case of ObjectIds, its hexString. If you don't want an id getter added to your schema, you may disable it passing this option at schema construction time.

// default behavior
+

option: collection

Mongoose by default produces a collection name by passing the model name to the utils.toCollectionName method. This method pluralizes the name. Set this option if you need a different name for your collection.

var dataSchema = new Schema({..}, { collection: 'data' });
+

option: emitIndexErrors

By default, mongoose will build any indexes you specify in your schema for +you, and emit an 'index' event on the model when the index build either +succeeds or errors out.

MyModel.on('index', function(error) {
+  /* If error is truthy, index build failed */
+});

However, this makes it tricky to catch when your index build fails. The +emitIndexErrors option makes seeing when your index build fails simpler. +If this option is on, mongoose will additionally emit an 'error' event +on the model when an index build fails.

MyModel.schema.options.emitIndexErrors; // true
+MyModel.on('error', function(error) {
+  // gets an error whenever index build fails
+});

Node.js' built-in +event emitter throws an exception if an error event is emitted and there are no listeners, +so its easy to configure your application to fail fast when an index build fails.

option: id

Mongoose assigns each of your schemas an id virtual getter by default which returns the documents _id field cast to a string, or in the case of ObjectIds, its hexString. If you don't want an id getter added to your schema, you may disable it passing this option at schema construction time.

// default behavior
 var schema = new Schema({ name: String });
-var Page = db.model('Page', schema);
+var Page = mongoose.model('Page', schema);
 var p = new Page({ name: 'mongodb.org' });
 console.log(p.id); // '50341373e894ad16347efe01'
 
 // disabled id
 var schema = new Schema({ name: String }, { id: false });
-var Page = db.model('Page', schema);
+var Page = mongoose.model('Page', schema);
 var p = new Page({ name: 'mongodb.org' });
 console.log(p.id); // undefined
-

option: _id

Mongoose assigns each of your schemas an _id field by default if one is not passed into the Schema constructor. The type assiged is an ObjectId to coincide with MongoDBs default behavior. If you don't want an _id added to your schema at all, you may disable it using this option.

+

option: _id

Mongoose assigns each of your schemas an _id field by default if one +is not passed into the Schema constructor. +The type assigned is an ObjectId +to coincide with MongoDB's default behavior. If you don't want an _id +added to your schema at all, you may disable it using this option.

-

Pass this option during schema construction to prevent documents from getting an auto _id created.

// default behavior
+

You can only use this option on sub-documents. Mongoose can't +save a document without knowing its id, so you will get an error if +you try to save a document without an _id.

// default behavior
 var schema = new Schema({ name: String });
-var Page = db.model('Page', schema);
+var Page = mongoose.model('Page', schema);
 var p = new Page({ name: 'mongodb.org' });
 console.log(p); // { _id: '50341373e894ad16347efe01', name: 'mongodb.org' }
 
 // disabled _id
-var schema = new Schema({ name: String }, { _id: false });
-var Page = db.model('Page', schema);
-var p = new Page({ name: 'mongodb.org' });
-console.log(p); // { name: 'mongodb.org' }
+var childSchema = new Schema({ name: String }, { _id: false });
+var parentSchema = new Schema({ children: [childSchema] });
+
+var Model = mongoose.model('Model', parentSchema);
+
+Model.create({ children: [{ name: 'Luke' }] }, function(error, doc) {
+  // doc.children[0]._id will be undefined
+});
+

option: minimize

Mongoose will, by default, "minimize" schemas by removing empty objects.

var schema = new Schema({ name: String, inventory: {} });
+var Character = mongoose.model('Character', schema);
+
+// will store `inventory` field if it is not empty
+var frodo = new Character({ name: 'Frodo', inventory: { ringOfPower: 1 }});
+Character.findOne({ name: 'Frodo' }, function(err, character) {
+  console.log(character); // { name: 'Frodo', inventory: { ringOfPower: 1 }}
+});
+
+// will not store `inventory` field if it is empty
+var sam = new Character({ name: 'Sam', inventory: {}});
+Character.findOne({ name: 'Sam' }, function(err, character) {
+  console.log(character); // { name: 'Sam' }
+});
+

This behavior can be overridden by setting minimize option to false. It will then store empty objects.

var schema = new Schema({ name: String, inventory: {} }, { minimize: false });
+var Character = mongoose.model('Character', schema);
+
+// will store `inventory` if empty
+var sam = new Character({ name: 'Sam', inventory: {}});
+Character.findOne({ name: 'Sam' }, function(err, character) {
+  console.log(character); // { name: 'Sam', inventory: {}}
+});
 

option: read

Allows setting query#read options at the schema level, providing us a way to apply default ReadPreferences to all queries derived from a model.

var schema = new Schema({..}, { read: 'primary' });            // also aliased as 'p'
 var schema = new Schema({..}, { read: 'primaryPreferred' });   // aliased as 'pp'
 var schema = new Schema({..}, { read: 'secondary' });          // aliased as 's'
@@ -100,23 +220,26 @@
 
 

The read option also allows us to specify tag sets. These tell the driver from which members of the replica-set it should attempt to read. Read more about tag sets here and here.

-

NOTE: if you specify the read pref 'nearest', you must also pass the strategy option when connecting or your reads will not behave predictably:

// pings the replset members periodically to track network latency
-// now `nearest` works as intended
+

NOTE: you may also specify the driver read pref strategy option when connecting:

// pings the replset members periodically to track network latency
 var options = { replset: { strategy: 'ping' }};
 mongoose.connect(uri, options);
 
-var schema = new Schema({..}, { read: ['n', { disk: 'ssd' }] });
+var schema = new Schema({..}, { read: ['nearest', { disk: 'ssd' }] });
 mongoose.model('JellyBean', schema);
-

option: safe

This option is passed to MongoDB with all operations and let's us specify if errors should be returned to our callbacks as well as tune write behavior.

var safe = true;
+

option: safe

This option is passed to MongoDB with all operations and specifies if errors should be returned to our callbacks as well as tune write behavior.

var safe = true;
 new Schema({ .. }, { safe: safe });
 

By default this is set to true for all schemas which guarentees that any occurring error gets passed back to our callback. By setting safe to something else like { j: 1, w: 2, wtimeout: 10000 } we can guarantee the write was committed to the MongoDB journal (j: 1), at least 2 replicas (w: 2), and that the write will timeout if it takes longer than 10 seconds (wtimeout: 10000). Errors will still be passed to our callback.

+

NOTE: In 3.6.x, you also need to turn versioning off. In 3.7.x and above, versioning will automatically be disabled when safe is set to false

+ +

**NOTE: this setting overrides any setting specified by passing db options while creating a connection.

+

There are other write concerns like { w: "majority" } too. See the MongoDB docs for more details.

var safe = { w: "majority", wtimeout: 10000 };
 new Schema({ .. }, { safe: safe });
-

option: shardKey

The shardKey option is used when we have a sharded MongoDB architecture. Each sharded collection is given a shard key which must be present in all insert/update operations. We just need to set this schema option to the same shard key and we’ll be all set.

new Schema({ .. }, { shardkey: { tag: 1, name: 1 }})
+

option: shardKey

The shardKey option is used when we have a sharded MongoDB architecture. Each sharded collection is given a shard key which must be present in all insert/update operations. We just need to set this schema option to the same shard key and we’ll be all set.

new Schema({ .. }, { shardKey: { tag: 1, name: 1 }})
 

Note that Mongoose does not send the shardcollection command for you. You must configure your shards yourself.

option: strict

The strict option, (enabled by default), ensures that values passed to our model constructor that were not specified in our schema do not get saved to the db.

var thingSchema = new Schema({..})
-var Thing = db.model('Thing', schemaSchema);
+var Thing = mongoose.model('Thing', thingSchema);
 var thing = new Thing({ iAmNotInTheSchema: true });
 thing.save(); // iAmNotInTheSchema is not saved to the db
 
@@ -125,11 +248,11 @@
 var thing = new Thing({ iAmNotInTheSchema: true });
 thing.save(); // iAmNotInTheSchema is now saved to the db!!
 

This also affects the use of doc.set() to set a property value.

var thingSchema = new Schema({..})
-var Thing = db.model('Thing', schemaSchema);
+var Thing = mongoose.model('Thing', thingSchema);
 var thing = new Thing;
 thing.set('iAmNotInTheSchema', true);
 thing.save(); // iAmNotInTheSchema is not saved to the db
-

This value can be overridden at the model instance level by passing a second boolean argument:

var Thing = db.model('Thing');
+

This value can be overridden at the model instance level by passing a second boolean argument:

var Thing = mongoose.model('Thing');
 var thing = new Thing(doc, true);  // enables strict mode
 var thing = new Thing(doc, false); // disables strict mode
 

The strict option may also be set to "throw" which will cause errors to be produced instead of dropping the bad data.

NOTE: do not set to false unless you have good reason.

@@ -137,7 +260,7 @@

NOTE: in mongoose v2 the default was false.

NOTE: Any key/val set on the instance that does not exist in your schema is always ignored, regardless of schema option.

var thingSchema = new Schema({..})
-var Thing = db.model('Thing', schemaSchema);
+var Thing = mongoose.model('Thing', thingSchema);
 var thing = new Thing;
 thing.iAmNotInTheSchema = true;
 thing.save(); // iAmNotInTheSchema is never saved to the db
@@ -152,7 +275,7 @@
 console.log(m.toJSON()); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }
 // since we know toJSON is called whenever a js object is stringified:
 console.log(JSON.stringify(m)); // { "_id": "504e0cd7dd992d9be2f20b6f", "name": "Max Headroom is my name" }
-

option: toObject

Documents have a toObject method which converts the mongoose document into a plain javascript object. This method accepts a few options. Instead of applying these options on a per-document basis we may declare the options here and have it applied to all of this schemas documents by default.

+

To see all available toJSON/toObject options, read this.

option: toObject

Documents have a toObject method which converts the mongoose document into a plain javascript object. This method accepts a few options. Instead of applying these options on a per-document basis we may declare the options here and have it applied to all of this schemas documents by default.

To have all virtuals show up in your console.log output, set the toObject option to { getters: true }:

var schema = new Schema({ name: String });
 schema.path('name').get(function (v) {
@@ -162,27 +285,108 @@
 var M = mongoose.model('Person', schema);
 var m = new M({ name: 'Max Headroom' });
 console.log(m); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }
-

option: versionKey

The versionKey is a property set on each document when first created by Mongoose. This keys value contains the internal revision of the document. The name of this document property is configurable. The default is __v. If this conflicts with your application you can configure as such:

var schema = new Schema({ name: 'string' });
-var Thing = db.model('Thing', schema);
+

To see all available toObject options, read this.

option: typeKey

By default, if you have an object with key 'type' in your schema, mongoose +will interpret it as a type declaration.

// Mongoose interprets this as 'loc is a String'
+var schema = new Schema({ loc: { type: String, coordinates: [Number] } });
+

However, for applications like geoJSON, +the 'type' property is important. If you want to control which key mongoose +uses to find type declarations, set the 'typeKey' schema option.

var schema = new Schema({
+  // Mongoose interpets this as 'loc is an object with 2 keys, type and coordinates'
+  loc: { type: String, coordinates: [Number] },
+  // Mongoose interprets this as 'name is a String'
+  name: { $type: String }
+}, { typeKey: '$type' }); // A '$type' key means this object is a type declaration
+

option: validateBeforeSave

By default, documents are automatically validated before they are saved to the database. This is to prevent saving an invalid document. If you want to handle validation manually, and be able to save objects which don't pass validation, you can set validateBeforeSave to false.

var schema = new Schema({ name: String });
+schema.set('validateBeforeSave', false);
+schema.path('name').validate(function (value) {
+    return v != null;
+});
+var M = mongoose.model('Person', schema);
+var m = new M({ name: null });
+m.validate(function(err) {
+    console.log(err); // Will tell you that null is not allowed.
+});
+m.save(); // Succeeds despite being invalid
+

option: versionKey

The versionKey is a property set on each document when first created by +Mongoose. This keys value contains the internal +revision +of the document. The versionKey option is a string that represents the +path to use for versioning. The default is __v. If this conflicts with +your application you can configure as such:

var schema = new Schema({ name: 'string' });
+var Thing = mongoose.model('Thing', schema);
 var thing = new Thing({ name: 'mongoose v3' });
 thing.save(); // { __v: 0, name: 'mongoose v3' }
 
 // customized versionKey
 new Schema({..}, { versionKey: '_somethingElse' })
-var Thing = db.model('Thing', schema);
+var Thing = mongoose.model('Thing', schema);
 var thing = new Thing({ name: 'mongoose v3' });
 thing.save(); // { _somethingElse: 0, name: 'mongoose v3' }
-

Document versioning can also be disabled by setting the versionKey to false. DO NOT disable versioning unless you know what you are doing.

new Schema({..}, { versionKey: false });
-var Thing = db.model('Thing', schema);
+

Document versioning can also be disabled by setting the versionKey to +false. +DO NOT disable versioning unless you know what you are doing.

new Schema({..}, { versionKey: false });
+var Thing = mongoose.model('Thing', schema);
 var thing = new Thing({ name: 'no versioning please' });
 thing.save(); // { name: 'no versioning please' }
+

option: skipVersioning

skipVersioning allows excluding paths from versioning (i.e., the internal revision will not be incremented even if these paths are updated). DO NOT do this unless you know what you're doing. For sub-documents, include this on the parent document using the fully qualified path.

new Schema({..}, { skipVersioning: { dontVersionMe: true } });
+thing.dontVersionMe.push('hey');
+thing.save(); // version is not incremented
+

option: timestamps

If set timestamps, mongoose assigns createdAt and updatedAt fields to your schema, the type assigned is Date.

+ +

By default, the name of two fields are createdAt and updatedAt, customize the field name by setting timestamps.createdAt and timestamps.updatedAt.

var thingSchema = new Schema({..}, { timestamps: { createdAt: 'created_at' } });
+var Thing = mongoose.model('Thing', thingSchema);
+var thing = new Thing();
+thing.save(); // `created_at` & `updatedAt` will be included
+

option: useNestedStrict

In mongoose 4, update() and findOneAndUpdate() only check the top-level +schema's strict mode setting.

var childSchema = new Schema({}, { strict: false });
+var parentSchema = new Schema({ child: childSchema }, { strict: 'throw' });
+var Parent = mongoose.model('Parent', parentSchema);
+Parent.update({}, { 'child.name': 'Luke Skywalker' }, function(error) {
+  // Error because parentSchema has `strict: throw`, even though
+  // `childSchema` has `strict: false`
+});
+
+var update = { 'child.name': 'Luke Skywalker' };
+var opts = { strict: false };
+Parent.update({}, update, opts, function(error) {
+  // This works because passing `strict: false` to `update()` overwrites
+  // the parent schema.
+});

If you set useNestedStrict to true, mongoose will use the child schema's +strict option for casting updates.

var childSchema = new Schema({}, { strict: false });
+var parentSchema = new Schema({ child: childSchema },
+  { strict: 'throw', useNestedStrict: true });
+var Parent = mongoose.model('Parent', parentSchema);
+Parent.update({}, { 'child.name': 'Luke Skywalker' }, function(error) {
+  // Works!
+});
+

option: retainKeyOrder

By default, mongoose reverses key order in documents as a performance optimization. For example, new Model({ first: 1, second: 2 }); would actually be stored in MongoDB as { second: 2, first: 1 }. This behavior is considered deprecated because it has numerous unintended side effects, including making it difficult to manipulate documents whose _id field is an object.

-

Next Up

Now that we've covered Schemas, let's take a look at SchemaTypes.

\ No newline at end of file +KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/guide.jade b/docs/guide.jade index da5a8a36515..07d50efcca8 100644 --- a/docs/guide.jade +++ b/docs/guide.jade @@ -27,27 +27,27 @@ block content }); p em - | If you want to add additional keys later, use the + | If you want to add additional keys later, use the a(href="./api.html#schema_Schema-add") Schema#add | method. - p - | Each key in our + p + | Each key in our code blogSchema - | defines a property in our documents which will be cast to its associated + | defines a property in our documents which will be cast to its associated a(href="./api.html#schematype_SchemaType") SchemaType - |. For example, we've defined a + |. For example, we've defined a code title - | which will be cast to the + | which will be cast to the a(href="./api.html#schema-string-js") String - | SchemaType and + | SchemaType and code date - | which will be cast to a + | which will be cast to a code Date | SchemaType. - | Keys may also be assigned nested objects containing further key/type definitions + | Keys may also be assigned nested objects containing further key/type definitions em (e.g. the `meta` property above). p - | The permitted SchemaTypes are + | The permitted SchemaTypes are ul li String li Number @@ -57,17 +57,17 @@ block content li Mixed li ObjectId li Array - | Read more about them + | Read more about them a(href="./schematypes.html") here | . p - | Schemas not only define the structure of your document and casting of properties, they also define document + | Schemas not only define the structure of your document and casting of properties, they also define document a(href="#methods") instance methods - |, static + |, static a(href="#statics") Model methods - |, + |, a(href="#indexes") compound indexes - | and document lifecycle hooks called + | and document lifecycle hooks called a(href="./middleware.html") middleware |. @@ -89,26 +89,29 @@ block content var animalSchema = new Schema({ name: String, type: String }); // assign a function to the "methods" object of our animalSchema - animalSchema.methods.findSimilarTypes = function (cb) { + animalSchema.methods.findSimilarTypes = function(cb) { return this.model('Animal').find({ type: this.type }, cb); - } + }; p - | Now all of our + | Now all of our code animal - | instances have a + | instances have a code findSimilarTypes | method available to it. :js var Animal = mongoose.model('Animal', animalSchema); var dog = new Animal({ type: 'dog' }); - dog.findSimilarTypes(function (err, dogs) { + dog.findSimilarTypes(function(err, dogs) { console.log(dogs); // woof }); .important :markdown - Overwriting a default mongoose document method may lead to unpredictible results. See [this](./api.html#schema_Schema.reserved) for more details. + Overwriting a default mongoose document method may lead to unpredictable results. See [this](./api.html#schema_Schema.reserved) for more details. + .important + :markdown + Do **not** declare methods using ES6 arrow functions (`=>`). Arrow functions [explicitly prevent binding `this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_binding_of_this), so your method will **not** have access to the document and the above examples will not work. h3#statics Statics :markdown @@ -116,12 +119,32 @@ block content :js // assign a function to the "statics" object of our animalSchema - animalSchema.statics.findByName = function (name, cb) { + animalSchema.statics.findByName = function(name, cb) { return this.find({ name: new RegExp(name, 'i') }, cb); - } + }; + + var Animal = mongoose.model('Animal', animalSchema); + Animal.findByName('fido', function(err, animals) { + console.log(animals); + }); + + .important + :markdown + Do **not** declare statics using ES6 arrow functions (`=>`). Arrow functions [explicitly prevent binding `this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_binding_of_this), so the above examples will not work because of the value of `this`. + + h3#query-helpers Query Helpers + :markdown + You can also add query helper functions, which are like instance methods + but for mongoose queries. Query helper methods let you extend mongoose's + [chainable query builder API](./queries.html). + + :js + animalSchema.query.byName = function(name) { + return this.find({ name: new RegExp(name, 'i') }); + }; var Animal = mongoose.model('Animal', animalSchema); - Animal.findByName('fido', function (err, animals) { + Animal.find().byName('fido').exec(function(err, animals) { console.log(animals); }); @@ -129,7 +152,7 @@ block content :markdown MongoDB supports [secondary indexes](http://docs.mongodb.org/manual/indexes/). With mongoose, we define these indexes within our `Schema` [at](./api.html#schematype_SchemaType-index) [the](./api.html#schematype_SchemaType-unique) [path](./api.html#schematype_SchemaType-sparse) [level](./api.html#schema_date_SchemaDate-expires) or the `schema` level. - Defining indexes at the schema level is necessary when creating + Defining indexes at the schema level is necessary when creating [compound indexes](http://www.mongodb.org/display/DOCS/Indexes#Indexes-CompoundKeys). :js @@ -143,15 +166,34 @@ block content .important :markdown - When your application starts up, Mongoose automatically calls `ensureIndex` for each defined index in your schema. - Mongoose will call `ensureIndex` for each index sequentially, and emit an 'index' event on the model when all the `ensureIndex` calls succeeded or when there was an error. - While nice for development, it is recommended this behavior be disabled in production since index creation can cause a [significant performance impact](http://docs.mongodb.org/manual/core/indexes/#index-creation-operations). Disable the behavior by setting the `autoIndex` option of your schema to `false`, or globally on the connection by setting the option `config.autoIndex` to `false`. + When your application starts up, Mongoose automatically calls [`createIndex`](https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#db.collection.createIndex) for each defined index in your schema. + Mongoose will call `createIndex` for each index sequentially, and emit an 'index' event on the model when all the `createIndex` calls succeeded or when there was an error. + While nice for development, it is recommended this behavior be disabled in production since index creation can cause a [significant performance impact](http://docs.mongodb.org/manual/core/indexes/#index-creation-operations). Disable the behavior by setting the `autoIndex` option of your schema to `false`, or globally on the connection by setting the option `autoIndex` to `false`. :js + mongoose.connect('mongodb://user:pass@localhost:port/database', { autoIndex: false }); + // or + mongoose.createConnection('mongodb://user:pass@localhost:port/database', { autoIndex: false }); + // or animalSchema.set('autoIndex', false); // or new Schema({..}, { autoIndex: false }); + :markdown + Mongoose will emit an `index` event on the model when indexes are done + building or an error occurred. + + :js + // Will cause an error because mongodb has an _id index by default that + // is not sparse + animalSchema.index({ _id: 1 }, { sparse: true }); + var Animal = mongoose.model('Animal', animalSchema); + + Animal.on('index', function(error) { + // "_id index cannot be sparse" + console.log(error.message); + }); + :markdown See also the [Model#ensureIndexes](./api.html#model_Model.ensureIndexes) method. @@ -172,59 +214,72 @@ block content var Person = mongoose.model('Person', personSchema); // create a document - var bad = new Person({ - name: { first: 'Walter', last: 'White' } + var axl = new Person({ + name: { first: 'Axl', last: 'Rose' } }); :markdown - Suppose we want to log the full name of `bad`. We could do this manually like so: + Suppose you want to print out the person's full name. You could do it yourself: :js - console.log(bad.name.first + ' ' + bad.name.last); // Walter White + console.log(axl.name.first + ' ' + axl.name.last); // Axl Rose :markdown - Or we could define a [virtual property getter](./api.html#virtualtype_VirtualType-get) on our `personSchema` so we don't need to write out this string concatenation mess each time: + But concatenating the first and last name every time can get cumbersome. And what if you want to do some extra processing on the name, like [removing diacritics?](https://www.npmjs.com/package/diacritics). A [virtual property getter](./api.html#virtualtype_VirtualType-get) lets you define a `fullName` property that won't get persisted to MongoDB. :js - personSchema.virtual('name.full').get(function () { + personSchema.virtual('fullName').get(function () { return this.name.first + ' ' + this.name.last; }); :markdown - Now, when we access our virtual "name.full" property, our getter function will be invoked and the value returned: + Now, mongoose will call your getter function every time you access the `fullName` property: :js - console.log('%s is insane', bad.name.full); // Walter White is insane + console.log(axl.fullName); // Axl Rose :markdown - Note that if the resulting record is converted to an object or JSON, virtuals are not included by default. - Pass `virtuals : true` to either [toObject()](./api.html#document_Document-toObject) or to toJSON() to have them returned. + If you use `toJSON()` or `toObject()` (or use `JSON.stringify()` on a mongoose document) mongoose will *not* include virtuals by default. + Pass `{ virtuals: true }` to either [toObject()](./api.html#document_Document-toObject) or `toJSON()`. - It would also be nice to be able to set `this.name.first` and `this.name.last` by setting `this.name.full`. For example, if we wanted to change `bad`'s `name.first` and `name.last` to 'Breaking' and 'Bad' respectively, it'd be nice to just: + You can also add a custom setter to your virtual that will let you set both first name and last name via the `fullName` virtual. :js - bad.name.full = 'Breaking Bad'; + personSchema.virtual('fullName'). + get(function() { return this.name.first + ' ' + this.name.last; }). + set(function(v) { + this.name.first = v.substr(0, v.indexOf(' ')); + this.name.last = v.substr(v.indexOf(' ') + 1); + }); + + axl.fullName = 'William Rose'; // Now `axl.name.first` is "William" :markdown - Mongoose lets you do this as well through its [virtual property setters](./api.html#virtualtype_VirtualType-set): + Virtual property setters are applied before other validation. So the example above would still work even if the `first` and `last` name fields were required. - :js - personSchema.virtual('name.full').set(function (name) { - var split = name.split(' '); - this.name.first = split[0]; - this.name.last = split[1]; - }); + Only non-virtual properties work as part of queries and for field selection. Since virtuals are not stored in MongoDB, you can't query with them. - ... + h5#aliases Aliases + :markdown + Aliases are a particular type of virtual where the getter and setter seamlessly get and set another property. This is handy for saving network bandwidth, so you can convert a short property name stored in the database into a longer name for code readability. - mad.name.full = 'Breaking Bad'; - console.log(mad.name.first); // Breaking - console.log(mad.name.last); // Bad + :js + var personSchema = new Schema({ + n: { + type: String, + // Now accessing `name` will get you the value of `n`, and setting `n` will set the value of `name` + alias: 'name' + } + }); - :markdown - Virtual property setters are applied before other validation. So the example above would still work even if the `first` and `last` name fields were required. + // Setting `name` will propagate to `n` + var person = new Person({ name: 'Val' }); + console.log(person); // { n: 'Val' } + console.log(person.toObject({ virtuals: true })); // { n: 'Val', name: 'Val' } + console.log(person.name); // "Val" - Only non-virtual properties work as part of queries and for field selection. + person.name = 'Not Val'; + console.log(person); // { n: 'Not Val' } h3#options Options :markdown @@ -241,6 +296,7 @@ block content Valid options: - [autoIndex](#autoIndex) + - [bufferCommands](#bufferCommands) - [capped](#capped) - [collection](#collection) - [emitIndexErrors](#emitIndexErrors) @@ -256,12 +312,14 @@ block content - [typeKey](#typeKey) - [validateBeforeSave](#validateBeforeSave) - [versionKey](#versionKey) + - [collation](#collation) - [skipVersioning](#skipVersioning) - [timestamps](#timestamps) + - [retainKeyOrder](#retainKeyOrder) h4#autoIndex option: autoIndex :markdown - At application startup, Mongoose sends an `ensureIndex` command for each index declared in your `Schema`. As of Mongoose v3, indexes are created in the `background` by default. If you wish to disable the auto-creation feature and manually handle when indexes are created, set your `Schema`s `autoIndex` option to `false` and use the [ensureIndexes](./api.html#model_Model.ensureIndexes) method on your model. + At application startup, Mongoose sends a [`createIndex` command](https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#db.collection.createIndex) for each index declared in your `Schema`. As of Mongoose v3, indexes are created in the `background` by default. If you wish to disable the auto-creation feature and manually handle when indexes are created, set your `Schema`s `autoIndex` option to `false` and use the [ensureIndexes](./api.html#model_Model.ensureIndexes) method on your model. :js var schema = new Schema({..}, { autoIndex: false }); var Clock = mongoose.model('Clock', schema); @@ -269,7 +327,9 @@ block content h4#bufferCommands option: bufferCommands :markdown - When running with the drivers `autoReconnect` option disabled and connected to a single `mongod` (non-replica-set), mongoose buffers commands when the connection goes down until you manually reconnect. To disable mongoose buffering under these conditions, set this option to `false`. + By default, mongoose buffers commands when the connection goes down until + the driver manages to reconnect. To disable buffering, set `bufferCommands` + to false. :js var schema = new Schema({..}, { bufferCommands: false }); @@ -331,9 +391,15 @@ block content h4#_id option: _id :markdown - Mongoose assigns each of your schemas an `_id` field by default if one is not passed into the [Schema](/docs/api.html#schema-js) constructor. The type assigned is an [ObjectId](/docs/api.html#schema_Schema.Types) to coincide with MongoDBs default behavior. If you don't want an `_id` added to your schema at all, you may disable it using this option. + Mongoose assigns each of your schemas an `_id` field by default if one + is not passed into the [Schema](/docs/api.html#schema-js) constructor. + The type assigned is an [ObjectId](/docs/api.html#schema_Schema.Types) + to coincide with MongoDB's default behavior. If you don't want an `_id` + added to your schema at all, you may disable it using this option. - Pass this option *during schema construction* to prevent documents from getting an `_id` created by Mongoose (parent documents will still have an `_id` created by MongoDB when inserted). _Passing the option later using `Schema.set('_id', false)` will not work. See issue [#1512](https://github.com/Automattic/mongoose/issues/1512)._ + You can **only** use this option on sub-documents. Mongoose can't + save a document without knowing its id, so you will get an error if + you try to save a document without an `_id`. :js // default behavior var schema = new Schema({ name: String }); @@ -342,42 +408,29 @@ block content console.log(p); // { _id: '50341373e894ad16347efe01', name: 'mongodb.org' } // disabled _id - var schema = new Schema({ name: String }, { _id: false }); + var childSchema = new Schema({ name: String }, { _id: false }); + var parentSchema = new Schema({ children: [childSchema] }); - // Don't set _id to false after schema construction as in - // var schema = new Schema({ name: String }); - // schema.set('_id', false); + var Model = mongoose.model('Model', parentSchema); - var Page = mongoose.model('Page', schema); - var p = new Page({ name: 'mongodb.org' }); - console.log(p); // { name: 'mongodb.org' } - - // MongoDB will create the _id when inserted - p.save(function (err) { - if (err) return handleError(err); - Page.findById(p, function (err, doc) { - if (err) return handleError(err); - console.log(doc); // { name: 'mongodb.org', _id: '50341373e894ad16347efe12' } - }) - }) - - :markdown - Note that currently you must disable the `_id` + Model.create({ children: [{ name: 'Luke' }] }, function(error, doc) { + // doc.children[0]._id will be undefined + }); h4#minimize option: minimize :markdown Mongoose will, by default, "minimize" schemas by removing empty objects. - + :js var schema = new Schema({ name: String, inventory: {} }); var Character = mongoose.model('Character', schema); - + // will store `inventory` field if it is not empty var frodo = new Character({ name: 'Frodo', inventory: { ringOfPower: 1 }}); Character.findOne({ name: 'Frodo' }, function(err, character) { console.log(character); // { name: 'Frodo', inventory: { ringOfPower: 1 }} }); - + // will not store `inventory` field if it is empty var sam = new Character({ name: 'Sam', inventory: {}}); Character.findOne({ name: 'Sam' }, function(err, character) { @@ -386,11 +439,11 @@ block content :markdown This behavior can be overridden by setting `minimize` option to `false`. It will then store empty objects. - + :js var schema = new Schema({ name: String, inventory: {} }, { minimize: false }); var Character = mongoose.model('Character', schema); - + // will store `inventory` if empty var sam = new Character({ name: 'Sam', inventory: {}}); Character.findOne({ name: 'Sam' }, function(err, character) { @@ -434,7 +487,7 @@ block content :markdown By default this is set to `true` for all schemas which guarentees that any occurring error gets passed back to our callback. By setting `safe` to something else like `{ j: 1, w: 2, wtimeout: 10000 }` we can guarantee the write was committed to the MongoDB journal (j: 1), at least 2 replicas (w: 2), and that the write will timeout if it takes longer than 10 seconds (wtimeout: 10000). Errors will still be passed to our callback. - + NOTE: In 3.6.x, you also need to turn [versioning](#versionKey) off. In 3.7.x and above, versioning will **automatically be disabled** when `safe` is set to `false` **NOTE: this setting overrides any setting specified by passing db options while [creating a connection](/docs/api.html#index_Mongoose-createConnection). @@ -542,21 +595,21 @@ block content :markdown To see all available `toObject` options, read [this](/docs/api.html#document_Document-toObject). - + h4#typeKey option: typeKey :markdown By default, if you have an object with key 'type' in your schema, mongoose will interpret it as a type declaration. - + :js // Mongoose interprets this as 'loc is a String' var schema = new Schema({ loc: { type: String, coordinates: [Number] } }); - + :markdown However, for applications like [geoJSON](http://docs.mongodb.org/manual/reference/geojson/), the 'type' property is important. If you want to control which key mongoose uses to find type declarations, set the 'typeKey' schema option. - + :js var schema = new Schema({ // Mongoose interpets this as 'loc is an object with 2 keys, type and coordinates' @@ -564,7 +617,7 @@ block content // Mongoose interprets this as 'name is a String' name: { $type: String } }, { typeKey: '$type' }); // A '$type' key means this object is a type declaration - + h4#validateBeforeSave option: validateBeforeSave :markdown By default, documents are automatically validated before they are saved to the database. This is to prevent saving an invalid document. If you want to handle validation manually, and be able to save objects which don't pass validation, you can set validateBeforeSave to false. @@ -584,7 +637,12 @@ block content h4#versionKey option: versionKey :markdown - The `versionKey` is a property set on each document when first created by Mongoose. This keys value contains the internal [revision](http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning) of the document. The name of this document property is configurable. The default is `__v`. If this conflicts with your application you can configure as such: + The `versionKey` is a property set on each document when first created by + Mongoose. This keys value contains the internal + [revision](http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning) + of the document. The `versionKey` option is a string that represents the + path to use for versioning. The default is `__v`. If this conflicts with + your application you can configure as such: :js var schema = new Schema({ name: 'string' }); var Thing = mongoose.model('Thing', schema); @@ -598,16 +656,37 @@ block content thing.save(); // { _somethingElse: 0, name: 'mongoose v3' } :markdown - Document versioning can also be disabled by setting the `versionKey` to false. _DO NOT disable versioning unless you [know what you are doing](http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning)._ + Document versioning can also be disabled by setting the `versionKey` to + `false`. + _DO NOT disable versioning unless you [know what you are doing](http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning)._ :js new Schema({..}, { versionKey: false }); var Thing = mongoose.model('Thing', schema); var thing = new Thing({ name: 'no versioning please' }); thing.save(); // { name: 'no versioning please' } + h4#collation option: collation + :markdown + Sets a default [collation](https://docs.mongodb.com/manual/reference/collation/) for every query and aggregation. [Here's a beginner-friendly overview of collations](http://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-collations). + :js + var schema = new Schema({ + name: String + }, { collation: { locale: 'en_US', strength: 1 } }); + + var MyModel = db.model('MyModel', schema); + + MyModel.create([{ name: 'val' }, { name: 'Val' }]). + then(function() { + return MyModel.find({ name: 'val' }); + }). + then(function(docs) { + // `docs` will contain both docs, because `strength: 1` means + // MongoDB will ignore case when matching. + }); + h4#skipVersioning option: skipVersioning :markdown - `skipVersioning` allows excluding paths from versioning (i.e., the internal revision will not be incremented even if these paths are updated). DO NOT do this unless you know what you're doing. For sub-documents, include this on the parent document using the fully qualified path. + `skipVersioning` allows excluding paths from versioning (i.e., the internal revision will not be incremented even if these paths are updated). DO NOT do this unless you know what you're doing. For sub-documents, include this on the parent document using the fully qualified path. :js new Schema({..}, { skipVersioning: { dontVersionMe: true } }); thing.dontVersionMe.push('hey'); @@ -615,21 +694,63 @@ block content h4#timestamps option: timestamps :markdown - If set `timestamps`, mongoose assigns `createdAt` and `updatedAt` fields to your schema, the type assigned is [Date](http://mongoosejs.com/docs/api.html#schema-date-js). + If set `timestamps`, mongoose assigns `createdAt` and `updatedAt` fields to your schema, the type assigned is [Date](./api.html#schema-date-js). - By default, the name of two fields are `createdAt` and `updatedAt`, custom the field name by setting `timestamps.createdAt` and `timestamps.updatedAt`. + By default, the name of two fields are `createdAt` and `updatedAt`, customize the field name by setting `timestamps.createdAt` and `timestamps.updatedAt`. :js var thingSchema = new Schema({..}, { timestamps: { createdAt: 'created_at' } }); var Thing = mongoose.model('Thing', thingSchema); var thing = new Thing(); - thing.save(); // `created_at` & `updatedAt` will included + thing.save(); // `created_at` & `updatedAt` will be included + + h4#useNestedStrict option: useNestedStrict + :markdown + In mongoose 4, `update()` and `findOneAndUpdate()` only check the top-level + schema's strict mode setting. + :js + var childSchema = new Schema({}, { strict: false }); + var parentSchema = new Schema({ child: childSchema }, { strict: 'throw' }); + var Parent = mongoose.model('Parent', parentSchema); + Parent.update({}, { 'child.name': 'Luke Skywalker' }, function(error) { + // Error because parentSchema has `strict: throw`, even though + // `childSchema` has `strict: false` + }); + + var update = { 'child.name': 'Luke Skywalker' }; + var opts = { strict: false }; + Parent.update({}, update, opts, function(error) { + // This works because passing `strict: false` to `update()` overwrites + // the parent schema. + }); + :markdown + If you set `useNestedStrict` to true, mongoose will use the child schema's + `strict` option for casting updates. + :js + var childSchema = new Schema({}, { strict: false }); + var parentSchema = new Schema({ child: childSchema }, + { strict: 'throw', useNestedStrict: true }); + var Parent = mongoose.model('Parent', parentSchema); + Parent.update({}, { 'child.name': 'Luke Skywalker' }, function(error) { + // Works! + }); + + h4#retainKeyOrder option: retainKeyOrder + :markdown + By default, mongoose reverses key order in documents as a performance optimization. For example, `new Model({ first: 1, second: 2 });` would actually be stored in MongoDB as `{ second: 2, first: 1 }`. This behavior is [considered deprecated](https://github.com/Automattic/mongoose/wiki/5.0-Deprecation-Warnings) because it has numerous unintended side effects, including making it difficult to manipulate documents whose `_id` field is an object. + + Mongoose >= 4.6.4 has a `retainKeyOrder` option for schemas that ensures that mongoose will always keep the correct order for your object keys. + + :js + var testSchema = new Schema({ first: Number, second: Number }, { retainKeyOrder: true }); + var Test = mongoose.model('Test', testSchema); + Test.create({ first: 1, second: 2 }); // Will be stored in mongodb as `{ first: 1, second: 2 }` h3#plugins Pluggable p - | Schemas are also + | Schemas are also a(href="./plugins.html") pluggable - | which allows us to package up reusable features into - a(href="http://plugins.mongoosejs.com") plugins + | which allows us to package up reusable features into + a(href="http://plugins.mongoosejs.io") plugins | that can be shared with the community or just between your projects. h3#next Next Up @@ -638,4 +759,4 @@ block content script. document.body.className = 'load'; -include includes/googleanalytics +include includes/keen diff --git a/docs/helpers/href.js b/docs/helpers/href.js index e6d6568b123..471843440af 100644 --- a/docs/helpers/href.js +++ b/docs/helpers/href.js @@ -1,6 +1,6 @@ module.exports = exports = function (str, char) { - if ('string' != typeof str) return str; + if ('string' !== typeof str) return str; return encodeURIComponent( str.replace(/\.js$/, '') .replace(/#/g, char || '-') diff --git a/docs/helpers/linktype.js b/docs/helpers/linktype.js index 28c25e0eeb0..fdd4ec02389 100644 --- a/docs/helpers/linktype.js +++ b/docs/helpers/linktype.js @@ -39,7 +39,7 @@ types.any = 'nolink'; module.exports= function (type) { if (types[type]) { - if ('nolink' == types[type]) return 'T'; + if ('nolink' === types[type]) return 'T'; return '' + type + ''; } return '' + type + ''; diff --git a/docs/images/favicon/android-icon-144x144.png b/docs/images/favicon/android-icon-144x144.png new file mode 100644 index 00000000000..81e370f6e6d Binary files /dev/null and b/docs/images/favicon/android-icon-144x144.png differ diff --git a/docs/images/favicon/android-icon-192x192.png b/docs/images/favicon/android-icon-192x192.png new file mode 100644 index 00000000000..998892e98f6 Binary files /dev/null and b/docs/images/favicon/android-icon-192x192.png differ diff --git a/docs/images/favicon/android-icon-36x36.png b/docs/images/favicon/android-icon-36x36.png new file mode 100644 index 00000000000..608a3f156a6 Binary files /dev/null and b/docs/images/favicon/android-icon-36x36.png differ diff --git a/docs/images/favicon/android-icon-48x48.png b/docs/images/favicon/android-icon-48x48.png new file mode 100644 index 00000000000..c1ceb6d93d9 Binary files /dev/null and b/docs/images/favicon/android-icon-48x48.png differ diff --git a/docs/images/favicon/android-icon-72x72.png b/docs/images/favicon/android-icon-72x72.png new file mode 100644 index 00000000000..39b576e8013 Binary files /dev/null and b/docs/images/favicon/android-icon-72x72.png differ diff --git a/docs/images/favicon/android-icon-96x96.png b/docs/images/favicon/android-icon-96x96.png new file mode 100644 index 00000000000..c1bcfdc8891 Binary files /dev/null and b/docs/images/favicon/android-icon-96x96.png differ diff --git a/docs/images/favicon/apple-icon-114x114.png b/docs/images/favicon/apple-icon-114x114.png new file mode 100644 index 00000000000..d0468c7f6ee Binary files /dev/null and b/docs/images/favicon/apple-icon-114x114.png differ diff --git a/docs/images/favicon/apple-icon-120x120.png b/docs/images/favicon/apple-icon-120x120.png new file mode 100644 index 00000000000..8e4a5743af6 Binary files /dev/null and b/docs/images/favicon/apple-icon-120x120.png differ diff --git a/docs/images/favicon/apple-icon-144x144.png b/docs/images/favicon/apple-icon-144x144.png new file mode 100644 index 00000000000..81e370f6e6d Binary files /dev/null and b/docs/images/favicon/apple-icon-144x144.png differ diff --git a/docs/images/favicon/apple-icon-152x152.png b/docs/images/favicon/apple-icon-152x152.png new file mode 100644 index 00000000000..73544243085 Binary files /dev/null and b/docs/images/favicon/apple-icon-152x152.png differ diff --git a/docs/images/favicon/apple-icon-180x180.png b/docs/images/favicon/apple-icon-180x180.png new file mode 100644 index 00000000000..05539f0c499 Binary files /dev/null and b/docs/images/favicon/apple-icon-180x180.png differ diff --git a/docs/images/favicon/apple-icon-57x57.png b/docs/images/favicon/apple-icon-57x57.png new file mode 100644 index 00000000000..27e96fb526e Binary files /dev/null and b/docs/images/favicon/apple-icon-57x57.png differ diff --git a/docs/images/favicon/apple-icon-60x60.png b/docs/images/favicon/apple-icon-60x60.png new file mode 100644 index 00000000000..81600df0a55 Binary files /dev/null and b/docs/images/favicon/apple-icon-60x60.png differ diff --git a/docs/images/favicon/apple-icon-72x72.png b/docs/images/favicon/apple-icon-72x72.png new file mode 100644 index 00000000000..39b576e8013 Binary files /dev/null and b/docs/images/favicon/apple-icon-72x72.png differ diff --git a/docs/images/favicon/apple-icon-76x76.png b/docs/images/favicon/apple-icon-76x76.png new file mode 100644 index 00000000000..bb39efbe5b0 Binary files /dev/null and b/docs/images/favicon/apple-icon-76x76.png differ diff --git a/docs/images/favicon/apple-icon-precomposed.png b/docs/images/favicon/apple-icon-precomposed.png new file mode 100644 index 00000000000..06d1d487a8c Binary files /dev/null and b/docs/images/favicon/apple-icon-precomposed.png differ diff --git a/docs/images/favicon/apple-icon.png b/docs/images/favicon/apple-icon.png new file mode 100644 index 00000000000..06d1d487a8c Binary files /dev/null and b/docs/images/favicon/apple-icon.png differ diff --git a/docs/images/favicon/browserconfig.xml b/docs/images/favicon/browserconfig.xml new file mode 100644 index 00000000000..c5541482230 --- /dev/null +++ b/docs/images/favicon/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/docs/images/favicon/favicon-16x16.png b/docs/images/favicon/favicon-16x16.png new file mode 100644 index 00000000000..2c76d9a7777 Binary files /dev/null and b/docs/images/favicon/favicon-16x16.png differ diff --git a/docs/images/favicon/favicon-32x32.png b/docs/images/favicon/favicon-32x32.png new file mode 100644 index 00000000000..48afe4233ec Binary files /dev/null and b/docs/images/favicon/favicon-32x32.png differ diff --git a/docs/images/favicon/favicon-96x96.png b/docs/images/favicon/favicon-96x96.png new file mode 100644 index 00000000000..c1bcfdc8891 Binary files /dev/null and b/docs/images/favicon/favicon-96x96.png differ diff --git a/docs/images/favicon/favicon.ico b/docs/images/favicon/favicon.ico new file mode 100644 index 00000000000..057632c29bd Binary files /dev/null and b/docs/images/favicon/favicon.ico differ diff --git a/docs/images/favicon/manifest.json b/docs/images/favicon/manifest.json new file mode 100644 index 00000000000..013d4a6a533 --- /dev/null +++ b/docs/images/favicon/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "App", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/docs/images/favicon/ms-icon-144x144.png b/docs/images/favicon/ms-icon-144x144.png new file mode 100644 index 00000000000..81e370f6e6d Binary files /dev/null and b/docs/images/favicon/ms-icon-144x144.png differ diff --git a/docs/images/favicon/ms-icon-150x150.png b/docs/images/favicon/ms-icon-150x150.png new file mode 100644 index 00000000000..1e3250c66bf Binary files /dev/null and b/docs/images/favicon/ms-icon-150x150.png differ diff --git a/docs/images/favicon/ms-icon-310x310.png b/docs/images/favicon/ms-icon-310x310.png new file mode 100644 index 00000000000..20b18479ad4 Binary files /dev/null and b/docs/images/favicon/ms-icon-310x310.png differ diff --git a/docs/images/favicon/ms-icon-70x70.png b/docs/images/favicon/ms-icon-70x70.png new file mode 100644 index 00000000000..a83d8a61ad5 Binary files /dev/null and b/docs/images/favicon/ms-icon-70x70.png differ diff --git a/docs/includes/favicon.jade b/docs/includes/favicon.jade new file mode 100644 index 00000000000..4bc3e53c60a --- /dev/null +++ b/docs/includes/favicon.jade @@ -0,0 +1,13 @@ +link(rel='apple-touch-icon', sizes='57x57', href='images/favicon/apple-icon-57x57.png') +link(rel='apple-touch-icon', sizes='60x60', href='images/favicon/apple-icon-60x60.png') +link(rel='apple-touch-icon', sizes='72x72', href='images/favicon/apple-icon-72x72.png') +link(rel='apple-touch-icon', sizes='76x76', href='images/favicon/apple-icon-76x76.png') +link(rel='apple-touch-icon', sizes='114x114', href='images/favicon/apple-icon-114x114.png') +link(rel='apple-touch-icon', sizes='120x120', href='images/favicon/apple-icon-120x120.png') +link(rel='apple-touch-icon', sizes='144x144', href='images/favicon/apple-icon-144x144.png') +link(rel='apple-touch-icon', sizes='152x152', href='images/favicon/apple-icon-152x152.png') +link(rel='apple-touch-icon', sizes='180x180', href='images/favicon/apple-icon-180x180.png') +link(rel='icon', type='image/png', sizes='192x192', href='images/favicon/android-icon-192x192.png') +link(rel='icon', type='image/png', sizes='32x32', href='images/favicon/favicon-32x32.png') +link(rel='icon', type='image/png', sizes='96x96', href='images/favicon/favicon-96x96.png') +link(rel='icon', type='image/png', sizes='16x16', href='images/favicon/favicon-16x16.png') diff --git a/docs/includes/googleanalytics.jade b/docs/includes/googleanalytics.jade deleted file mode 100644 index aaafe24e32d..00000000000 --- a/docs/includes/googleanalytics.jade +++ /dev/null @@ -1,10 +0,0 @@ -script. - var _gaq = _gaq || []; - _gaq.push(['_setAccount', 'UA-1122274-9']); - _gaq.push(['_trackPageview', location.pathname + location.search + location.hash]); - - (function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); - })(); diff --git a/docs/includes/googlesearch.jade b/docs/includes/googlesearch.jade index 3a285f28c8e..04596e9cfd2 100644 --- a/docs/includes/googlesearch.jade +++ b/docs/includes/googlesearch.jade @@ -4,7 +4,7 @@ script var gcse = document.createElement('script'); gcse.type = 'text/javascript'; gcse.async = true; - gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + + gcse.src = (document.location.protocol === 'https:' ? 'https:' : 'http:') + '//www.google.com/cse/cse.js?cx=' + cx; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(gcse, s); diff --git a/docs/includes/keen.jade b/docs/includes/keen.jade new file mode 100644 index 00000000000..d6b3bd6c6e4 --- /dev/null +++ b/docs/includes/keen.jade @@ -0,0 +1,18 @@ +script(type="text/javascript") + !function(name,path,ctx){ + var latest,prev=name!=='Keen'&&window.Keen?window.Keen:false;ctx[name]=ctx[name]||{ready:function(fn){var h=document.getElementsByTagName('head')[0],s=document.createElement('script'),w=window,loaded;s.onload=s.onerror=s.onreadystatechange=function(){if((s.readyState&&!(/^c|loade/.test(s.readyState)))||loaded){return}s.onload=s.onreadystatechange=null;loaded=1;latest=w.Keen;if(prev){w.Keen=prev}else{try{delete w.Keen}catch(e){w.Keen=void 0}}ctx[name]=latest;ctx[name].ready(fn)};s.async=1;s.src=path;h.parentNode.insertBefore(s,h)}} + }('KeenAsync','https://d26b395fwzu5fz.cloudfront.net/keen-tracking-1.1.3.min.js',this); + + KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); + }); diff --git a/docs/includes/nav.jade b/docs/includes/nav.jade index 2047d2a6eb9..5f7afcc3ba2 100644 --- a/docs/includes/nav.jade +++ b/docs/includes/nav.jade @@ -22,6 +22,12 @@ ul a(href="./schematypes.html") span schema | types + li.custom-schema-types + a(href="./customschematypes.html") + | custom schema types + li.advanced-schemas + a(href="./advanced_schemas.html") + | advanced usage li a(href="./models.html") | models @@ -32,6 +38,9 @@ ul li.subdocs a(href="./subdocs.html") | sub docs + li.defaults + a(href="./defaults.html") + | defaults li a(href="./queries.html") | queries @@ -61,7 +70,7 @@ ul | contributing li a(href="./harmony.html") - | ES6 integration + | ES2015 integration li a(href="./browser.html") | schemas in the browser diff --git a/docs/index.html b/docs/index.html index d8d1095828e..caf3771929d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,36 +1,51 @@ -Mongoose Quick Start v3.3.1Fork me on GitHub

Getting Started

First be sure you have MongoDB and Nodejs installed.

Now say we like fuzzy kittens and want to record every kitten we ever meet in MongoDB. -The first thing we need to do is include mongoose in our project and open a connection.

var mongoose = require('mongoose')
-  , db = mongoose.createConnection('localhost', 'test');

We have a pending connection object to the test database running on localhost. We now need to get notified if we connect successfully or if a connection error occurs:

db.on('error', console.error.bind(console, 'connection error:'));
-db.once('open', function () {
-  // yay!
-});

Once our connection opens, our callback will be called. For brevity, let's assume that all following code is within this callback.

With Mongoose, everything is derived from a Schema. Let's get a reference to it and define our kittens.

var kittySchema = new mongoose.Schema({
+Mongoose Quick Start v4.11.9Fork me on GitHub

Getting Started

First be sure you have MongoDB and Node.js installed.

+ +

Next install Mongoose from the command line using npm:

+ +
$ npm install mongoose

Now say we like fuzzy kittens and want to record every kitten we ever meet in MongoDB. +The first thing we need to do is include mongoose in our project and open a connection to the test database on our locally running instance of MongoDB.

// getting-started.js
+var mongoose = require('mongoose');
+mongoose.connect('mongodb://localhost/test');

We have a pending connection to the test database running on localhost. We now need to get notified if we connect successfully or if a connection error occurs:

var db = mongoose.connection;
+db.on('error', console.error.bind(console, 'connection error:'));
+db.once('open', function() {
+  // we're connected!
+});

Once our connection opens, our callback will be called. For brevity, let's assume that all following code is within this callback.

With Mongoose, everything is derived from a Schema. Let's get a reference to it and define our kittens.

var kittySchema = mongoose.Schema({
     name: String
-})

So far so good. We've got a schema with one property, name, which will be a String. The next step is compiling our schema into a model.

var Kitten = db.model('Kitten', kittySchema)

A model is a class with which we construct documents. +});

So far so good. We've got a schema with one property, name, which will be a String. The next step is compiling our schema into a Model.

var Kitten = mongoose.model('Kitten', kittySchema);

A model is a class with which we construct documents. In this case, each document will be a kitten with properties and behaviors as declared in our schema. -Let's create a kitten document representing the little guy we just met on the sidewalk outside:

var silence = new Kitten({ name: 'Silence' })
-console.log(silence.name) // 'Silence'

Kittens can meow, so let's take a look at how to add "speak" functionality to our documents:

kittySchema.methods.speak = function () {
+Let's create a kitten document representing the little guy we just met on the sidewalk outside:

var silence = new Kitten({ name: 'Silence' });
+console.log(silence.name); // 'Silence'

Kittens can meow, so let's take a look at how to add "speak" functionality to our documents:

// NOTE: methods must be added to the schema before compiling it with mongoose.model()
+kittySchema.methods.speak = function () {
   var greeting = this.name
     ? "Meow name is " + this.name
-    : "I don't have a name"
+    : "I don't have a name";
   console.log(greeting);
 }
 
-var Kitten = db.model('Kitten', kittySchema)

Functions added to the methods property of a schema get compiled into the Model prototype and exposed on each document instance:

var fluffy = new Kitten({ name: 'fluffy' });
-fluffy.speak() // "Meow name is fluffy"

We have talking kittens! But we still haven't saved anything to MongoDB. -Each document can be saved to the database by calling its save method. The first argument to the callback will be an error if any occured.

fluffy.save(function (err) {
-  if (err) // TODO handle the error
-  console.log('meow')
-});

Say time goes by and we want to display all the kittens we've seen. -We can access all of the kitten documents through our Kitten model.

Kitten.find(function (err, kittens) {
-  if (err) // TODO handle err
-  console.log(kittens)
+var Kitten = mongoose.model('Kitten', kittySchema);

Functions added to the methods property of a schema get compiled into the Model prototype and exposed on each document instance:

var fluffy = new Kitten({ name: 'fluffy' });
+fluffy.speak(); // "Meow name is fluffy"

We have talking kittens! But we still haven't saved anything to MongoDB. +Each document can be saved to the database by calling its save method. The first argument to the callback will be an error if any occured.

fluffy.save(function (err, fluffy) {
+  if (err) return console.error(err);
+  fluffy.speak();
+});

Say time goes by and we want to display all the kittens we've seen. +We can access all of the kitten documents through our Kitten model.

Kitten.find(function (err, kittens) {
+  if (err) return console.error(err);
+  console.log(kittens);
 })

We just logged all of the kittens in our db to the console. -If we want to filter our kittens by name, Mongoose supports MongoDbs rich querying syntax.

Kitten.find({ name: /fluff/i }, callback)

This performs a case-insensitive search for all documents with a name property containing "fluff" and returns the result as an array of kittens to the callback.

Congratulations

That's the end of our quick start. We created a schema, added a custom document method, saved and queried kittens in MongoDB using Mongoose. Head over to the guide, or api docs for more.

\ No newline at end of file +KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/index.jade b/docs/index.jade index c306bc442f2..52911e2c61a 100644 --- a/docs/index.jade +++ b/docs/index.jade @@ -4,6 +4,7 @@ html(lang='en') meta(charset="utf-8") meta(name="viewport", content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no") title Mongoose Quick Start v#{package.version} + include ./includes/favicon.jade link(href='http://fonts.googleapis.com/css?family=Anonymous+Pro:400,700|Droid+Sans+Mono|Open+Sans:400,700|Linden+Hill|Quattrocento:400,700|News+Cycle:400,700|Antic+Slab|Cabin+Condensed:400,700', rel='stylesheet', type='text/css') link(href='/docs/css/default.css', rel='stylesheet', type='text/css') link(href='/docs/css/guide.css', rel='stylesheet', type='text/css') @@ -38,8 +39,8 @@ html(lang='en') :js var db = mongoose.connection; db.on('error', console.error.bind(console, 'connection error:')); - db.once('open', function (callback) { - // yay! + db.once('open', function() { + // we're connected! }); :markdown Once our connection opens, our callback will be called. For brevity, let's assume that all following code is within this callback. @@ -97,7 +98,7 @@ html(lang='en') We just logged all of the kittens in our db to the console. If we want to filter our kittens by name, Mongoose supports MongoDBs rich [querying](/docs/queries.html) syntax. :js - Kitten.find({ name: /^Fluff/ }, callback); + Kitten.find({ name: /^fluff/ }, callback); :markdown This performs a search for all documents with a name property that begins with "Fluff" and returns the result as an array of kittens to the callback. h3 Congratulations @@ -105,4 +106,4 @@ html(lang='en') That's the end of our quick start. We created a schema, added a custom document method, saved and queried kittens in MongoDB using Mongoose. Head over to the [guide](guide.html), or [API docs](api.html) for more. script. document.body.className = 'load'; - include includes/googleanalytics + include includes/keen diff --git a/docs/layout.jade b/docs/layout.jade index 4e019387c93..9bd93a8cfb4 100644 --- a/docs/layout.jade +++ b/docs/layout.jade @@ -4,10 +4,28 @@ html(lang='en') meta(charset="utf-8") meta(name="viewport", content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no") title Mongoose #{title} v#{package.version} + include ./includes/favicon.jade block style link(href='http://fonts.googleapis.com/css?family=Anonymous+Pro:400,700|Droid+Sans+Mono|Open+Sans:400,700|Linden+Hill|Quattrocento:400,700|News+Cycle:400,700|Antic+Slab|Cabin+Condensed:400,700', rel='stylesheet', type='text/css') link(href='css/default.css', rel='stylesheet', type='text/css') link(href='css/guide.css', rel='stylesheet', type='text/css') + link(rel='apple-touch-icon', sizes='57x57', href='images/favicon/apple-icon-57x57.png') + link(rel='apple-touch-icon', sizes='60x60', href='images/favicon/apple-icon-60x60.png') + link(rel='apple-touch-icon', sizes='72x72', href='images/favicon/apple-icon-72x72.png') + link(rel='apple-touch-icon', sizes='76x76', href='images/favicon/apple-icon-76x76.png') + link(rel='apple-touch-icon', sizes='114x114', href='images/favicon/apple-icon-114x114.png') + link(rel='apple-touch-icon', sizes='120x120', href='images/favicon/apple-icon-120x120.png') + link(rel='apple-touch-icon', sizes='144x144', href='images/favicon/apple-icon-144x144.png') + link(rel='apple-touch-icon', sizes='152x152', href='images/favicon/apple-icon-152x152.png') + link(rel='apple-touch-icon', sizes='180x180', href='images/favicon/apple-icon-180x180.png') + link(rel='icon', type='image/png', sizes='192x192', href='images/favicon/android-icon-192x192.png') + link(rel='icon', type='image/png', sizes='32x32', href='images/favicon/favicon-32x32.png') + link(rel='icon', type='image/png', sizes='96x96', href='images/favicon/favicon-96x96.png') + link(rel='icon', type='image/png', sizes='16x16', href='images/favicon/favicon-16x16.png') + link(rel='manifest', href='images/favicon/manifest.json') + meta(name='msapplication-TileColor', content='#ffffff') + meta(name='msapplication-TileImage', content='images/favicon/ms-icon-144x144.png') + meta(name='theme-color', content='#ffffff') body a#forkbanner(href="http://github.com/Automattic/mongoose") img(style="position: absolute; top: 0; right: 0; border: 0;", src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png", alt="Fork me on GitHub") @@ -24,4 +42,4 @@ html(lang='en') script. document.body.className = 'load'; include includes/googlesearch - include includes/googleanalytics + include includes/keen diff --git a/docs/middleware.html b/docs/middleware.html index a40ade38c52..50cfa87fcbb 100644 --- a/docs/middleware.html +++ b/docs/middleware.html @@ -1,29 +1,202 @@ -Mongoose Middleware v3.3.1Fork me on GitHub

Middleware

Middleware are functions which are passed control of flow during execution of init, validate, save and remove methods.

-There are two types of middleware, serial and parallel.

Serial

Serial middleware are executed one after another, when each middleware calls next

var schema = new Schema(..);
-schema.pre('save', function (next) {
+Mongoose Middleware v4.11.9Fork me on GitHub

Middleware

Middleware (also called pre and post hooks) are functions which are passed +control during execution of asynchronous functions. Middleware is specified +on the schema level and is useful for writing plugins. +Mongoose 4.x has 3 types +of middleware: document middleware, model middleware, and query middleware. +Document middleware is supported for the following document functions. +In document middleware functions, this refers to the document.

+ + + +

Query middleware is supported for the following Model and Query functions. +In query middleware functions, this refers to the query.

+ + + +

Model middleware is supported for the following model functions. +In model middleware functions, this refers to the model.

+ + + +

All middleware types support pre and post hooks. +How pre and post hooks work is described in more detail below.

+ +

Note: There is no query hook for remove(), only for documents. +If you set a 'remove' hook, it will be fired when you call myDoc.remove(), +not when you call MyModel.remove(). +Note: The create() function fires save() hooks.

Pre

There are two types of pre hooks, serial and parallel.

Serial

Serial middleware are executed one after another, when each middleware calls next.

var schema = new Schema(..);
+schema.pre('save', function(next) {
   // do stuff
   next();
-});

Parallel

Parallel middleware offer more fine-grained flow control.

var schema = new Schema(..);
-schema.pre('save', true, function (next, done) {
+});

Parallel

Parallel middleware offer more fine-grained flow control.

var schema = new Schema(..);
+
+// `true` means this is a parallel middleware. You **must** specify `true`
+// as the second parameter if you want to use parallel middleware.
+schema.pre('save', true, function(next, done) {
   // calling next kicks off the next middleware in parallel
   next();
-  doAsync(done);
-});

The hooked method, in this case save, will not be executed until done is called by each middleware.

Use Cases

Middleware are useful for atomizing model logic and avoiding nested blocks of async code. Here are some other ideas:

  • complex validation
  • removing dependent documents
    • (removing a user removes all his blogposts)
  • asynchronous defaults
  • asynchronous tasks that a certain action triggers
    • triggering custom events
    • notifications

Error handling

If any middleware calls next or done with an Error instance, the flow is interrupted, and the error is passed to the callback.

schema.pre('save', function (next) {
+  setTimeout(done, 100);
+});

The hooked method, in this case save, will not be executed until done is called by each middleware.

Use Cases

Middleware are useful for atomizing model logic and avoiding nested blocks of async code. Here are some other ideas:

  • complex validation
  • removing dependent documents
    • (removing a user removes all his blogposts)
  • asynchronous defaults
  • asynchronous tasks that a certain action triggers
    • triggering custom events
    • notifications

Error handling

If any middleware calls next or done with a parameter of type Error, +the flow is interrupted, and the error is passed to the callback.

schema.pre('save', function(next) {
+  // You **must** do `new Error()`. `next('something went wrong')` will
+  // **not** work
   var err = new Error('something went wrong');
   next(err);
 });
 
 // later...
 
-myModel.save(function (err) {
+myDoc.save(function(err) {
   console.log(err.message) // something went wrong
 });
-

Next Up

Now that we've covered middleware, let's take a look at Mongooses approach to faking JOINs with its query population helper.

\ No newline at end of file +KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/middleware.jade b/docs/middleware.jade index bb8567083fc..e6574d5a2bc 100644 --- a/docs/middleware.jade +++ b/docs/middleware.jade @@ -6,9 +6,10 @@ block content Middleware (also called pre and post *hooks*) are functions which are passed control during execution of asynchronous functions. Middleware is specified on the schema level and is useful for writing [plugins](./plugins.html). - Mongoose 4.0 has 2 types - of middleware: document middleware and query middleware. + Mongoose 4.x has 4 types + of middleware: document middleware, model middleware, aggregate middleware, and query middleware. Document middleware is supported for the following document functions. + In document middleware functions, `this` refers to the document. * [init](./api.html#document_Document-init) * [validate](./api.html#document_Document-validate) @@ -16,6 +17,7 @@ block content * [remove](./api.html#model_Model-remove) Query middleware is supported for the following Model and Query functions. + In query middleware functions, `this` refers to the query. * [count](./api.html#query_Query-count) * [find](./api.html#query_Query-find) @@ -24,30 +26,61 @@ block content * [findOneAndUpdate](./api.html#query_Query-findOneAndUpdate) * [update](./api.html#query_Query-update) - Both document middleware and query middleware support pre and post hooks. + Aggregate middleware is for `MyModel.aggregate()`. Aggregate middleware + executes when you call `exec()` on an aggregate object. + In aggregate middleware, `this` refers to the [aggregation object](./api.html#model_Model.aggregate). + + * [aggregate](./api.html#model_Model.aggregate) + + Model middleware is supported for the following model functions. + In model middleware functions, `this` refers to the model. + + * [insertMany](./api.html#model_Model.insertMany) + + All middleware types support pre and post hooks. How pre and post hooks work is described in more detail below. - + **Note:** There is no query hook for `remove()`, only for documents. If you set a 'remove' hook, it will be fired when you call `myDoc.remove()`, not when you call `MyModel.remove()`. + **Note:** The `create()` function fires `save()` hooks. - h3 Pre + h3#pre Pre :markdown There are two types of `pre` hooks, serial and parallel. - h4 Serial + h4#serial Serial :markdown - Serial middleware are executed one after another, when each middleware calls `next`. + Serial middleware functions are executed one after another, when each + middleware calls `next`. :js var schema = new Schema(..); schema.pre('save', function(next) { // do stuff next(); }); - h4 Parallel + :markdown + The `next()` call does **not** stop the rest of the code in your middleware function from executing. Use + [the early `return` pattern](https://www.bennadel.com/blog/2323-use-a-return-statement-when-invoking-callbacks-especially-in-a-guard-statement.htm) + to prevent the rest of your middleware function from running when you call `next()`. + :js + var schema = new Schema(..); + schema.pre('save', function(next) { + if (foo()) { + console.log('calling next!'); + // `return next();` will make sure the rest of this function doesn't run + /*return*/ next(); + } + // Unless you comment out the `return` above, 'after next' will print + console.log('after next'); + }); + h4#parallel Parallel p | Parallel middleware offer more fine-grained flow control. :js var schema = new Schema(..); + + // `true` means this is a parallel middleware. You **must** specify `true` + // as the second parameter if you want to use parallel middleware. schema.pre('save', true, function(next, done) { // calling next kicks off the next middleware in parallel next(); @@ -55,7 +88,7 @@ block content }); :markdown The hooked method, in this case `save`, will not be executed until `done` is called by each middleware. - h4 Use Cases + h4#use-cases Use Cases p | Middleware are useful for atomizing model logic and avoiding nested blocks of async code. Here are some other ideas: ul @@ -69,12 +102,14 @@ block content ul li triggering custom events li notifications - h4 Error handling + h4#error-handling Error handling :markdown - If any middleware calls `next` or `done` with an error, the flow - is interrupted, and the error is passed to the callback. + If any middleware calls `next` or `done` with a parameter of type `Error`, + the flow is interrupted, and the error is passed to the callback. :js schema.pre('save', function(next) { + // You **must** do `new Error()`. `next('something went wrong')` will + // **not** work var err = new Error('something went wrong'); next(err); }); @@ -107,6 +142,50 @@ block content console.log('%s has been removed', doc._id); }); + h3#post-async Asynchronous Post Hooks + :markdown + While post middleware doesn't receive flow control, you can still make + sure that asynchronous post hooks are executed in a pre-defined order. + If your post hook function takes at least 2 parameters, mongoose will + assume the second parameter is a `next()` function that you will call to + trigger the next middleware in the sequence. + + :js + // Takes 2 parameters: this is an asynchronous post hook + schema.post('save', function(doc, next) { + setTimeout(function() { + console.log('post1'); + // Kick off the second post hook + next(); + }, 10); + }); + + // Will not execute until the first middleware calls `next()` + schema.post('save', function(doc, next) { + console.log('post2'); + next(); + }); + + h3#order Save/Validate Hooks + :markdown + The `save()` function triggers `validate()` hooks, because mongoose + has a built-in `pre('save')` hook that calls `validate()`. This means + that all `pre('validate')` and `post('validate')` hooks get called + **before** any `pre('save')` hooks. + :js + schema.pre('validate', function() { + console.log('this gets printed first'); + }); + schema.post('validate', function() { + console.log('this gets printed second'); + }); + schema.pre('save', function() { + console.log('this gets printed third'); + }); + schema.post('save', function() { + console.log('this gets printed fourth'); + }); + h3#notes Notes on findAndUpdate() and Query Middleware :markdown Pre and post `save()` hooks are **not** executed on `update()`, @@ -143,6 +222,67 @@ block content this.update({},{ $set: { updatedAt: new Date() } }); }); + h3#error-handling Error Handling Middleware + :markdown + _New in 4.5.0_ + + Middleware execution normally stops the first time a piece of middleware + calls `next()` with an error. However, there is a special kind of post + middleware called "error handling middleware" that executes specifically + when an error occurs. + + Error handling middleware is defined as middleware that takes one extra + parameter: the 'error' that occurred as the first parameter to the function. + Error handling middleware can then transform the error however you want. + + :js + var schema = new Schema({ + name: { + type: String, + // Will trigger a MongoError with code 11000 when + // you save a duplicate + unique: true + } + }); + + // Handler **must** take 3 parameters: the error that occurred, the document + // in question, and the `next()` function + schema.post('save', function(error, doc, next) { + if (error.name === 'MongoError' && error.code === 11000) { + next(new Error('There was a duplicate key error')); + } else { + next(error); + } + }); + + // Will trigger the `post('save')` error handler + Person.create([{ name: 'Axl Rose' }, { name: 'Axl Rose' }]); + + :markdown + Error handling middleware also works with query middleware. You can + also define a post `update()` hook that will catch MongoDB duplicate key + errors. + + :js + // The same E11000 error can occur when you call `update()` + // This function **must** take 3 parameters. If you use the + // `passRawResult` function, this function **must** take 4 + // parameters + schema.post('update', function(error, res, next) { + if (error.name === 'MongoError' && error.code === 11000) { + next(new Error('There was a duplicate key error')); + } else { + next(error); + } + }); + + var people = [{ name: 'Axl Rose' }, { name: 'Slash' }]; + Person.create(people, function(error) { + Person.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } }, function(error) { + // `error.message` will be "There was a duplicate key error" + }); + }); + h3#next Next Up :markdown Now that we've covered middleware, let's take a look at Mongoose's approach diff --git a/docs/migration.html b/docs/migration.html index 4d22bf79ead..b63279ad884 100644 --- a/docs/migration.html +++ b/docs/migration.html @@ -1,40 +1,43 @@ -Mongoose Migration Guide v3.3.1Fork me on GitHub

Migrating from 2.x to 3.x

Migrating from Mongoose 2.x to 3.x brings with it several changes to be aware of:

Query clean-up

Many methods of the Query API were nothing but aliases and have been removed in an effort to keep Mongoose clean and focused on there being as close to one way of doing things as possible. If you really love all that extra noise, you can bring most of it back with this module.

- -

Here are the removed methods are their still existing aliases:

RemovedAlternative
query.runquery.exec
query.$orquery.or
query.$norquery.nor
query.$gtquery.gt
query.$gtequery.gte
query.$ltquery.lt
query.$ltequery.lte
query.$nequery.ne
query.$inquery.in
query.$ninquery.nin
query.$allquery.all
query.$regexquery.regex
query.$sizequery.size
query.$maxDistancequery.maxDistance
query.$modquery.mod
query.$nearquery.near
query.$existsquery.exists
query.$elemMatchquery.elemMatch
query.$withinquery.within
query.$boxquery.box
query.$centerquery.center
query.$centerSpherequery.centerSphere
query.$slicequery.slice
query.notEqualToquery.ne
query.whereinquery.within
query.ascquery.sort *
query.descquery.sort *
query.fieldsquery.select *

query#asc

The asc method of Query has been removed in favor of sort. The sort method accepts slightly different arguments so read the docs to make sure your application is all set.

query#desc

The desc method of Query has been removed in favor of sort. The sort method accepts slightly different arguments so read the docs to make sure your application is all set.

query#sort

The sort method of Queries now accepts slightly different arguments. Read the docs to make sure your application is all set.

query#fields

The fields method of Query has been removed, it being mostly an alias for the select method anyway. The select method has slightly different arguments so read the docs to make sure your application is all set.

- -

Because of the change to the fields method, the field selection argument for query.populate and model methods like findById, find, etc, is slightly different as well (no longer accepts arrays for example), so read the docs to make sure your application is all set.

Connecting to ReplicaSets

To connect to a ReplicaSet you no longer use the separate connectSet or createSetConnection methods. Both mongoose.connect and mongoose.createConnection are now smart enough to just do the right thing with your connection string. If you really want to bring connectSet and createSetConnection back use this module.

Schemas

  • are now strict by default.
  • Arrays of object literal now creates document arrays instead of arrays of Mixed.
  • Indexes are now created in background by default.
  • Index errors are now emitted on their model instead of the connection. See issue #984.

Arrays

  • pop is now fixed causing a $set of the entire array.
  • $pop is now fixed and behaves just as MongoDB $pop does, removing at most the last element of the array.
  • shift is now fixed causing a $set of the entire array.
  • $shift is now fixed and behaves just as a MongoDB $pop -1 does, removing at most the first element of array.
  • $unshift was removed, use unshift instead.
  • $addToSet was removed, use addToSet instead.
  • $pushAll was removed, use push instead.
  • $pull was removed, use pull instead.
  • $pullAll was removed, use pull instead.
  • doAtomics was changed to the hasAtomics private method

Number type

The custom subclassed Number type Mongoose used to use for all numbers is now gone. It caused too many problems when doing comparisons and had other bad side-effects.

- -

With it out of the picture, the following helper methods of MongooseNumbers are now also gone:

- -
  • $inc
  • $increment
  • $decrement
- -

If you really want this behavior back, include the mongoose-number module in your project.

- -

A good alternative is to start using the new findAndModify helpers. -Say we have an inventory of 10 products and a customer purchases 7 of them. In Mongoose v2 you could have depended on MongooseNumber:

var inventorySchema = new Schema({ productCount: Number });
-...
-Inventory.findById(id, function (err, inventory) {
-  if (err) return handleError(err);
-  inventory.productCount.$decrement(7);
-  inventory.save(function (err) {
-    // sends Inventory.update({ _id: id }, { $inc: { balance: -7 }}, callback);
-    if (err) return handleError(err);
-    res.send(inventory.productCount); // 3
+Fork me on GitHub

Migrating from 3.x to 4.x

There are several backwards-breaking changes to be aware of when migrating from Mongoose 3 to Mongoose 4.

`findOneAndUpdate()` new field is now `false` by default

Mongoose's findOneAndUpdate(), findOneAndRemove(), +findByIdAndUpdate(), and findByIdAndRemove() functions are just +wrappers around MongoDB's +findAndModify command. +Both the MongoDB server and the MongoDB NodeJS driver set the new option +to false by default, but mongoose 3 overwrote this default. In order to be +more consistent with the MongoDB server's documentation, mongoose will +use false by default. That is, +findOneAndUpdate({}, { $set: { test: 1 } }, callback); will return the +document as it was before the $set operation was applied.

+ +

To return the document with modifications made on the update, use the new: true option.

MyModel.findOneAndUpdate({}, { $set: { test: 1 } }, { new: true }, callback);
+

CastError and ValidationError now use `kind` instead of `type` to report error types

In Mongoose 3, CastError and ValidationError had a type field. For instance, user defined validation errors would have a type property that contained the string 'user defined'. In Mongoose 4, this property has been renamed to kind due to the V8 JavaScript engine using the Error.type property internally.

Query now has a `.then()` function

In mongoose 3, you needed to call .exec() on a query chain to get a promise back, like MyModel.find().exec().then();. Mongoose 4 queries are promises, so you can do MyModel.find().then() instead. Be careful if you're using functions like q's Q.ninvoke() or otherwise returning a mongoose query from a promise.

More Info

Related blog posts:

+ +
\ No newline at end of file + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/migration.jade b/docs/migration.jade index 0dae65916ba..b59f3b58428 100644 --- a/docs/migration.jade +++ b/docs/migration.jade @@ -11,17 +11,24 @@ block content :markdown There are several [backwards-breaking changes](https://github.com/Automattic/mongoose/wiki/4.0-Release-Notes) to be aware of when migrating from Mongoose 3 to Mongoose 4. - h3 findOneAndUpdate() new field is now `false` by default + h3#findandmodify-new + | `findOneAndUpdate()` new field is now `false` by default :markdown - Mongoose's `findOneAndUpdate()` and `findOneAndRemove()` functions are just wrappers around MongoDB's [`findAndModify` command](http://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/). - Both the MongoDB server and the MongoDB NodeJS driver set the `new` option to false by default, but mongoose 3 overwrote this default. - In order to be more consistent with the MongoDB server's documentation, mongoose will use false by default. - That is, `findOneAndUpdate({}, { $set: { test: 1 } }, callback);` will return the document as it was *before* the `$set` operation was applied. + Mongoose's `findOneAndUpdate()`, `findOneAndRemove()`, + `findByIdAndUpdate()`, and `findByIdAndRemove()` functions are just + wrappers around MongoDB's + [`findAndModify` command](http://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/). + Both the MongoDB server and the MongoDB NodeJS driver set the `new` option + to false by default, but mongoose 3 overwrote this default. In order to be + more consistent with the MongoDB server's documentation, mongoose will + use false by default. That is, + `findOneAndUpdate({}, { $set: { test: 1 } }, callback);` will return the + document as it was *before* the `$set` operation was applied. To return the document with modifications made on the update, use the `new: true` option. :js - MyModel.findOneAndUpdate({}, { $set: { test: 1 } }, { 'new': true }, callback); + MyModel.findOneAndUpdate({}, { $set: { test: 1 } }, { new: true }, callback); h3 CastError and ValidationError now use `kind` instead of `type` to report error types :markdown diff --git a/docs/models.html b/docs/models.html index 97c269bea17..d0ed2530354 100644 --- a/docs/models.html +++ b/docs/models.html @@ -1,10 +1,8 @@ -Mongoose Models v3.3.1Fork me on GitHub

Models

Models are fancy constructors compiled from our Schema definitions. Instances of these models represent documents which can be saved and retreived from our database. All document creation and retreival from the database is handled by these models.

Compiling your first model

var schema = new Schema({ name: 'string', size: 'string' });
+Mongoose Models v4.11.9Fork me on GitHub

Models

Models are fancy constructors compiled from our Schema definitions. Instances of these models represent documents which can be saved and retrieved from our database. All document creation and retrieval from the database is handled by these models.

Compiling your first model

var schema = new mongoose.Schema({ name: 'string', size: 'string' });
 var Tank = mongoose.model('Tank', schema);
-
-// or, if you are using separate connections
-var db = mongoose.createConnection(..);
-var Tank = db.model('Tank', schema);
-

Constructing documents

Documents are instances of our model. Creating them and saving to the database is easy:

var Tank = db.model('Tank', yourSchema);
+

The first argument is the singular name of the collection your model is for. Mongoose automatically looks for the plural version of your model name. +Thus, for the example above, the model Tank is for the tanks collection in the database. +The .model() function makes a copy of schema. Make sure that you've added everything you want to schema before calling .model()!

Constructing documents

Documents are instances of our model. Creating them and saving to the database is easy:

var Tank = mongoose.model('Tank', yourSchema);
 
 var small = new Tank({ size: 'small' });
 small.save(function (err) {
@@ -14,19 +12,42 @@
 
 // or
 
-Tank.create({ size: 'small' }, function (err) {
+Tank.create({ size: 'small' }, function (err, small) {
   if (err) return handleError(err);
   // saved!
-})
-

Querying

Finding documents is easy with Mongoose, which supports the rich query syntax of MongoDB. Documents can be retreived using each models find, findById, findOne, or where static methods.

Tank.find({ type: 'small' }).where('createdDate').gt(oneYearAgo).exec(callback);

See the chapter on querying for more details on how to use the Query api.

Removing

Models have a static remove method available for removing all documents matching conditions.

Tank.remove({ size: 'large' }, function (err) {
+})

Note that no tanks will be created/removed until the connection your model +uses is open. Every model has an associated connection. When you use +mongoose.model(), your model will use the default mongoose connection.

mongoose.connect('localhost', 'gettingstarted');

If you create a custom connection, use that connection's model() function +instead.

var connection = mongoose.createConnection('mongodb://localhost:27017/test');
+var Tank = connection.model('Tank', yourSchema);
+

Querying

Finding documents is easy with Mongoose, which supports the rich query syntax of MongoDB. Documents can be retreived using each models find, findById, findOne, or where static methods.

Tank.find({ size: 'small' }).where('createdDate').gt(oneYearAgo).exec(callback);

See the chapter on querying for more details on how to use the Query api.

Removing

Models have a static remove method available for removing all documents matching conditions.

Tank.remove({ size: 'large' }, function (err) {
   if (err) return handleError(err);
   // removed!
-});

Updating

Each model has its own update method for modifying documents in the database without returning them to your application. See the API docs for more detail.

Yet more

The API docs cover many additional methods available like count, mapReduce, aggregate, and more.

Next Up

Now that we've covered Models, let's take a look at Documents.

\ No newline at end of file +KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/models.jade b/docs/models.jade index fd6b7e7093b..5d654ab0cf7 100644 --- a/docs/models.jade +++ b/docs/models.jade @@ -34,9 +34,17 @@ block content // saved! }) :markdown - Note that no tanks will be created/removed until the connection your model uses is open. In this case we are using `mongoose.model()` so let's open the default mongoose connection: + Note that no tanks will be created/removed until the connection your model + uses is open. Every model has an associated connection. When you use + `mongoose.model()`, your model will use the default mongoose connection. :js mongoose.connect('localhost', 'gettingstarted'); + :markdown + If you create a custom connection, use that connection's `model()` function + instead. + :js + var connection = mongoose.createConnection('mongodb://localhost:27017/test'); + var Tank = connection.model('Tank', yourSchema); h3 Querying :markdown diff --git a/docs/plugins.html b/docs/plugins.html index 1930e6c0f3b..a83f0e81aa3 100644 --- a/docs/plugins.html +++ b/docs/plugins.html @@ -1,4 +1,4 @@ -Mongoose Plugins v3.3.1Fork me on GitHub

Plugins

Schemas are pluggable, that is, they allow for applying pre-packaged capabilities to extend their functionality. This is a very powerful feature.

+Mongoose Plugins v4.11.9Fork me on GitHub

Plugins

Schemas are pluggable, that is, they allow for applying pre-packaged capabilities to extend their functionality. This is a very powerful feature.

Suppose that we have several collections in our database and want to add last-modified functionality to each one. With plugins this is easy. Just create a plugin once and apply it to each Schema:

// lastMod.js
 module.exports = exports = function lastModifiedPlugin (schema, options) {
@@ -22,12 +22,38 @@
 // player-schema.js
 var lastMod = require('./lastMod');
 var Player = new Schema({ ... });
-Player.plugin(lastMod);

We just added last-modified behavior to both our Game and Player schemas and declared an index on the lastMod path of our Games to boot. Not bad for a few lines of code.

Community!

Not only can you re-use schema functionality in your own projects but you also reap the benefits of the Mongoose community as well. Any plugin published to npm and tagged with mongoose will show up on our search results page.

Next Up

Now that we've covered plugins and how to get involved in the great community growing around them, let's take a look how you can help contribute to the continuing development of Mongoose itself.

\ No newline at end of file +var gameSchema = new Schema({ ... }); +var playerSchema = new Schema({ ... }); +// `lastModifiedPlugin` gets attached to both schemas +var Game = mongoose.model('Game', gameSchema); +var Player = mongoose.model('Player', playerSchema);

Community!

Not only can you re-use schema functionality in your own projects but you also reap the benefits of the Mongoose community as well. Any plugin published to npm and tagged with mongoose will show up on our search results page.

Next Up

Now that we've covered plugins and how to get involved in the great community growing around them, let's take a look how you can help contribute to the continuing development of Mongoose itself.

\ No newline at end of file diff --git a/docs/plugins.jade b/docs/plugins.jade index d768c95d4d9..a78a97e4e0b 100644 --- a/docs/plugins.jade +++ b/docs/plugins.jade @@ -33,9 +33,23 @@ block content Player.plugin(lastMod); :markdown We just added last-modified behavior to both our `Game` and `Player` schemas and declared an index on the `lastMod` path of our Games to boot. Not bad for a few lines of code. + h3#global Global Plugins + :markdown + Want to register a plugin for all schemas? The mongoose singleton has a + `.plugin()` function that registers a plugin for every schema. For + example: + :js + var mongoose = require('mongoose'); + mongoose.plugin(require('./lastMod')); + + var gameSchema = new Schema({ ... }); + var playerSchema = new Schema({ ... }); + // `lastModifiedPlugin` gets attached to both schemas + var Game = mongoose.model('Game', gameSchema); + var Player = mongoose.model('Player', playerSchema); h3 Community! :markdown - Not only can you re-use schema functionality in your own projects but you also reap the benefits of the Mongoose community as well. Any plugin published to [npm](https://npmjs.org/) and [tagged](https://npmjs.org/doc/tag.html) with `mongoose` will show up on our [search results](http://plugins.mongoosejs.com) page. + Not only can you re-use schema functionality in your own projects but you also reap the benefits of the Mongoose community as well. Any plugin published to [npm](https://npmjs.org/) and [tagged](https://npmjs.org/doc/tag.html) with `mongoose` will show up on our [search results](http://plugins.mongoosejs.io) page. h3#next Next Up :markdown diff --git a/docs/populate.html b/docs/populate.html index e265c3d167f..35ca6f8a8da 100644 --- a/docs/populate.html +++ b/docs/populate.html @@ -1,104 +1,291 @@ -Mongoose Query Population v3.3.1Fork me on GitHub

Query Population

There are no joins in MongoDB but sometimes we still want references to documents in other collections. This is where query#populate comes in.

+Mongoose Query Population v4.11.9Fork me on GitHub

Population

MongoDB has the join-like $lookup aggregation operator in versions >= 3.2. Mongoose has a more powerful alternative called populate(), which lets you reference documents in other collections.

-

ObjectIds can refer to another document in a collection within our database and be populate()d when querying:

var mongoose = require('mongoose')
-  , Schema = mongoose.Schema
-  
-var PersonSchema = new Schema({
-  name    : String,
-  age     : Number,
-  stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }]
+

Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s). We may populate a single document, multiple documents, plain object, multiple plain objects, or all objects returned from a query. Let's look at some examples.

var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var personSchema = Schema({
+  _id: Schema.Types.ObjectId,
+  name: String,
+  age: Number,
+  stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
 });
 
-var StorySchema = new Schema({
-  _creator : { type: Schema.Types.ObjectId, ref: 'Person' },
-  title    : String,
-  fans     : [{ type: Schema.Types.ObjectId, ref: 'Person' }]
+var storySchema = Schema({
+  author: { type: Schema.Types.ObjectId, ref: 'Person' },
+  title: String,
+  fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
 });
 
-var Story  = mongoose.model('Story', StorySchema);
-var Person = mongoose.model('Person', PersonSchema);

So far we've created two models. Our Person model has it's stories field set to an array of ObjectIds. The ref option is what tells Mongoose in which model to look, in our case the Story model. All _ids we store here must be document _ids from the Story model. We also added a _creator ObjectId to our Story schema which refers to a single Person.

Saving refs

Saving refs to other documents works the same way you normally save objectids, just assign an ObjectId:

var aaron = new Person({ name: 'Aaron', age: 100 });
+var Story = mongoose.model('Story', storySchema);
+var Person = mongoose.model('Person', personSchema);

So far we've created two Models. Our Person model has +its stories field set to an array of ObjectIds. The ref option is +what tells Mongoose which model to use during population, in our case +the Story model. All _ids we store here must be document _ids from +the Story model.

Note: ObjectId, Number, String, and Buffer are valid for use +as refs. However, you should use ObjectId unless you are an advanced +user and have a good reason for doing so.

Saving refs

Saving refs to other documents works the same way you normally save +properties, just assign the _id value:

var author = new Person({
+  _id: new mongoose.Types.ObjectId(),
+  name: 'Ian Fleming',
+  age: 50
+});
 
-aaron.save(function (err) {
+author.save(function (err) {
   if (err) return handleError(err);
   
   var story1 = new Story({
-    title: "Once upon a timex.",
-    _creator: aaron._id    // assign an ObjectId
+    title: 'Casino Royale',
+    author: author._id    // assign the _id from the person
   });
   
   story1.save(function (err) {
     if (err) return handleError(err);
     // thats it!
   });
-})

Population

So far we haven't done anything special. We've merely created a Person and a Story. Now let's take a look at populating our story's _creator:

Story
-.findOne({ title: /timex/ })
-.populate('_creator')
-.exec(function (err, story) {
-  if (err) return handleError(err);
-  console.log('The creator is %s', story._creator.name); // prints "The creator is Aaron"
-})
-

Populated paths are no longer set to their original ObjectIds, their value is replaced with the mongoose document returned from the database by performing a separate query before returning the results.

- -

Arrays of ObjectId refs work the same way. Just call the populate method on the query and an array of documents will be returned in place of the ObjectIds.

Field selection

What if we only want a few specific fields returned for the query? This can be accomplished by passing the usual field name syntax as the second argument to the populate method:

Story
-.findOne({ title: /timex/i })
-.populate('_creator', 'name') // only return the Persons name
-.exec(function (err, story) {
-  if (err) return handleError(err);
-  
-  console.log('The creator is %s', story._creator.name);
-  // prints "The creator is Aaron"
-  
-  console.log('The creators age is %s', story._creator.age);
-  // prints "The creators age is null'
-})

Query conditions for populate

What if we wanted to populate our fans array based on their age, and return, at most, any 5 of them?

Story
-.find(...)
-.populate('fans', null, { age: { $gte: 21 }}, { limit: 5 })

Done. Conditions and options for populate queries are passed as the third and fourth arguments respectively.

Refs to children

We may find however, if we use the aaron object, we are unable to get a list of the stories. This is because no story objects were ever 'pushed' on to aaron.stories.

+});

Population

So far we haven't done anything much different. We've merely created a Person and a Story. Now let's take a look at populating our story's author using the query builder:

Story.
+  findOne({ title: 'Casino Royale' }).
+  populate('author').
+  exec(function (err, story) {
+    if (err) return handleError(err);
+    console.log('The author is %s', story.author.name);
+    // prints "The author is Ian Fleming"
+  });

Populated paths are no longer set to their original _id , their value +is replaced with the mongoose document returned from the database by +performing a separate query before returning the results.

-

There are two perspectives to this story. First, it's nice to have aaron know which are his stories.

aaron.stories.push(story1);
-aaron.save();

This allows us to perform a find and populate combo:

Person
-.findOne({ name: 'Aaron' })
-.populate('stories') // only works if we pushed refs to children
-.exec(function (err, person) {
-  if (err) return handleError(err);
-  console.log(person);
-})

However, it is debatable that we really want two sets of pointers as they may get out of sync. So we could instead merely find() the documents we are interested in.

Story
-.find({ _creator: aaron._id })
-.populate('_creator') // not really necessary
-.exec(function (err, stories) {
-  if (err) return handleError(err);
-  console.log('The stories are an array: ', stories);
-})
-

Updating refs

Now that we have a story we realized that the _creator was incorrect. We can update ObjectId refs the same as any other property through the magic of Mongooses internal casting:

var guille = new Person({ name: 'Guillermo' });
-guille.save(function (err) {
-  if (err) return handleError(err);
-  
-  story._creator = guille; // or guille._id
-  
-  story.save(function (err) {
+

Arrays of refs work the same way. Just call the +populate method on the query and an +array of documents will be returned in place of the original _ids.

Note: mongoose >= 3.6 exposes the original _ids used during +population through the +document#populated() method.

Setting Populated Fields

In Mongoose >= 4.0, you can manually populate a field as well.

Story.findOne({ title: 'Casino Royale' }, function(error, story) {
+  if (error) {
+    return handleError(error);
+  }
+  story.author = author;
+  console.log(story.author.name); // prints "Ian Fleming"
+});

Note that this only works for single refs. You currently can't +manually populate an array of refs.

Field selection

What if we only want a few specific fields returned for the populated +documents? This can be accomplished by passing the usual +field name syntax as the second argument +to the populate method:

Story.
+  findOne({ title: /casino royale/i }).
+  populate('author', 'name'). // only return the Persons name
+  exec(function (err, story) {
     if (err) return handleError(err);
     
-    Story
-    .findOne({ title: /timex/i })
-    .populate('_creator', 'name')
-    .exec(function (err, story) {
-      if (err) return handleError(err);
-      
-      console.log('The creator is %s', story._creator.name)
-      // prints "The creator is Guillermo"
-    })
-  })
-})
-

NOTE:

The documents returned from calling populate become fully functional, removeable, saveable documents. Do not confuse them with sub docs. Take caution when calling its remove method because you'll be removing it from the database, not just the array.

NOTE:

Field selection in v3 is slightly different than v2. Arrays of fields are no longer accepted.

// this works
-Story.findOne(..).populate('_creator', 'name age').exec(..);
+    console.log('The author is %s', story.author.name);
+    // prints "The author is Ian Fleming"
+    
+    console.log('The authors age is %s', story.author.age);
+    // prints "The authors age is null'
+  })

Populating multiple paths

What if we wanted to populate multiple paths at the same time?

Story.
+  find(...).
+  populate('fans').
+  populate('author').
+  exec()

If you call populate() multiple times with the same path, only the last +one will take effect.

// The 2nd `populate()` call below overwrites the first because they
+// both populate 'fans'.
+Story.
+  find().
+  populate({ path: 'fans', select: 'name' }).
+  populate({ path: 'fans', select: 'email' });
+// The above is equivalent to:
+Story.find().populate({ path: 'fans', select: 'email' });

Query conditions and other options

What if we wanted to populate our fans array based on their age, select +just their names, and return at most, any 5 of them?

Story.
+  find(...).
+  populate({
+    path: 'fans',
+    match: { age: { $gte: 21 }},
+    // Explicitly exclude `_id`, see http://bit.ly/2aEfTdB
+    select: 'name -_id',
+    options: { limit: 5 }
+  }).
+  exec()

Refs to children

We may find however, if we use the author object, we are unable to get a +list of the stories. This is because no story objects were ever 'pushed' +onto author.stories.

-// this doesn't -Story.findOne(..).populate('_creator', ['name', 'age']).exec(..); -

See the migration guide for more detail.

Next Up

Now that we've covered query population, let's take a look at connections.

\ No newline at end of file +var Event = db1.model('Event', eventSchema); +var Conversation = db2.model('Conversation', conversationSchema);

In this situation, you will not be able to populate() normally. +The conversation field will always be null, because populate() doesn't +know which model to use. However, +you can specify the model explicitly.

Event.
+  find().
+  populate({ path: 'conversation', model: Conversation }).
+  exec(function(error, docs) { /* ... */ });

This is known as a "cross-database populate," because it enables you to +populate across MongoDB databases and even across MongoDB instances.

Dynamic References

Mongoose can also populate from multiple collections at the same time. +Let's say you have a user schema that has an array of "connections" - a +user can be connected to either other users or an organization.

var userSchema = new Schema({
+  name: String,
+  connections: [{
+    kind: String,
+    item: { type: ObjectId, refPath: 'connections.kind' }
+  }]
+});
+
+var organizationSchema = new Schema({ name: String, kind: String });
+
+var User = mongoose.model('User', userSchema);
+var Organization = mongoose.model('Organization', organizationSchema);

The refPath property above means that mongoose will look at the +connections.kind path to determine which model to use for populate(). +In other words, the refPath property enables you to make the ref +property dynamic.

// Say we have one organization:
+// `{ _id: ObjectId('000000000000000000000001'), name: "Guns N' Roses", kind: 'Band' }`
+// And two users:
+// {
+//   _id: ObjectId('000000000000000000000002')
+//   name: 'Axl Rose',
+//   connections: [
+//     { kind: 'User', item: ObjectId('000000000000000000000003') },
+//     { kind: 'Organization', item: ObjectId('000000000000000000000001') }
+//   ]
+// },
+// {
+//   _id: ObjectId('000000000000000000000003')
+//   name: 'Slash',
+//   connections: []
+// }
+User.
+  findOne({ name: 'Axl Rose' }).
+  populate('connections.item').
+  exec(function(error, doc) {
+    // doc.connections[0].item is a User doc
+    // doc.connections[1].item is an Organization doc
+  });
+  

Populate Virtuals

New in 4.5.0

+ +

So far you've only populated based on the _id field. However, that's +sometimes not the right choice. +In particular, arrays that grow without bound are a MongoDB anti-pattern. +Using mongoose virtuals, you can define more sophisticated relationships +between documents.

var PersonSchema = new Schema({
+  name: String,
+  band: String
+});
+
+var BandSchema = new Schema({
+  name: String
+});
+BandSchema.virtual('members', {
+  ref: 'Person', // The model to use
+  localField: 'name', // Find people where `localField`
+  foreignField: 'band', // is equal to `foreignField`
+  // If `justOne` is true, 'members' will be a single doc as opposed to
+  // an array. `justOne` is false by default.
+  justOne: false
+});
+
+var Person = mongoose.model('Person', personSchema);
+var Band = mongoose.model('Band', bandSchema);
+
+/**
+ * Suppose you have 2 bands: "Guns N' Roses" and "Motley Crue"
+ * And 4 people: "Axl Rose" and "Slash" with "Guns N' Roses", and
+ * "Vince Neil" and "Nikki Sixx" with "Motley Crue"
+ */
+Band.find({}).populate('members').exec(function(error, bands) {
+  /* `bands.members` is now an array of instances of `Person` */
+});
+

Keep in mind that virtuals are not included in toJSON() output by +default. If you want populate virtuals to show up when using functions +that rely on JSON.stringify(), like Express' +res.json() function, +set the virtuals: true option on your schema's toJSON options.

// Set `virtuals: true` so `res.json()` works
+var BandSchema = new Schema({
+  name: String
+}, { toJSON: { virtuals: true } });
+

If you're using populate projections, make sure foreignField is included +in the projection.

Band.
+  find({}).
+  populate({ path: 'members', select: 'name' }).
+  exec(function(error, bands) {
+    // Won't work, foreign field `band` is not selected in the projection
+  });
+  
+Band.
+  find({}).
+  populate({ path: 'members', select: 'name band' }).
+  exec(function(error, bands) {
+    // Works, foreign field `band` is selected
+  });
+  

Next Up

Now that we've covered query population, let's take a look at connections.

\ No newline at end of file diff --git a/docs/populate.jade b/docs/populate.jade index f7140c6c191..1cf25bf1858 100644 --- a/docs/populate.jade +++ b/docs/populate.jade @@ -3,45 +3,56 @@ extends layout block content h2 Population :markdown - There are no joins in MongoDB but sometimes we still want references to documents in other collections. This is where population comes in. + MongoDB has the join-like [$lookup](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/) aggregation operator in versions >= 3.2. Mongoose has a more powerful alternative called `populate()`, which lets you reference documents in other collections. Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s). We may populate a single document, multiple documents, plain object, multiple plain objects, or all objects returned from a query. Let's look at some examples. :js - var mongoose = require('mongoose') - , Schema = mongoose.Schema + var mongoose = require('mongoose'); + var Schema = mongoose.Schema; var personSchema = Schema({ - _id : Number, - name : String, - age : Number, - stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }] + _id: Schema.Types.ObjectId, + name: String, + age: Number, + stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }] }); var storySchema = Schema({ - _creator : { type: Number, ref: 'Person' }, - title : String, - fans : [{ type: Number, ref: 'Person' }] + author: { type: Schema.Types.ObjectId, ref: 'Person' }, + title: String, + fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }] }); - var Story = mongoose.model('Story', storySchema); + var Story = mongoose.model('Story', storySchema); var Person = mongoose.model('Person', personSchema); :markdown - So far we've created two [Models](./models.html). Our `Person` model has it's `stories` field set to an array of `ObjectId`s. The `ref` option is what tells Mongoose which model to use during population, in our case the `Story` model. All `_id`s we store here must be document `_id`s from the `Story` model. We also declared the `Story` `_creator` property as a `Number`, the same type as the `_id` used in the `personSchema`. It is important to match the type of `_id` to the type of ref. + So far we've created two [Models](./models.html). Our `Person` model has + its `stories` field set to an array of `ObjectId`s. The `ref` option is + what tells Mongoose which model to use during population, in our case + the `Story` model. All `_id`s we store here must be document `_id`s from + the `Story` model. .important :markdown - **Note**: `ObjectId`, `Number`, `String`, and `Buffer` are valid for use as refs. + **Note**: `ObjectId`, `Number`, `String`, and `Buffer` are valid for use + as refs. However, you should use `ObjectId` unless you are an advanced + user and have a good reason for doing so. h3 Saving refs :markdown - Saving refs to other documents works the same way you normally save properties, just assign the `_id` value: + Saving refs to other documents works the same way you normally save + properties, just assign the `_id` value: :js - var aaron = new Person({ _id: 0, name: 'Aaron', age: 100 }); + var author = new Person({ + _id: new mongoose.Types.ObjectId(), + name: 'Ian Fleming', + age: 50 + }); - aaron.save(function (err) { + author.save(function (err) { if (err) return handleError(err); var story1 = new Story({ - title: "Once upon a timex.", - _creator: aaron._id // assign the _id from the person + title: 'Casino Royale', + author: author._id // assign the _id from the person }); story1.save(function (err) { @@ -51,150 +62,336 @@ block content }); h3 Population :markdown - So far we haven't done anything much different. We've merely created a `Person` and a `Story`. Now let's take a look at populating our story's `_creator` using the query builder: + So far we haven't done anything much different. We've merely created a `Person` and a `Story`. Now let's take a look at populating our story's `author` using the query builder: :js - Story - .findOne({ title: 'Once upon a timex.' }) - .populate('_creator') - .exec(function (err, story) { - if (err) return handleError(err); - console.log('The creator is %s', story._creator.name); - // prints "The creator is Aaron" - }); + Story. + findOne({ title: 'Casino Royale' }). + populate('author'). + exec(function (err, story) { + if (err) return handleError(err); + console.log('The author is %s', story.author.name); + // prints "The author is Ian Fleming" + }); :markdown - Populated paths are no longer set to their original `_id` , their value is replaced with the mongoose document returned from the database by performing a separate query before returning the results. + Populated paths are no longer set to their original `_id` , their value + is replaced with the mongoose document returned from the database by + performing a separate query before returning the results. - Arrays of refs work the same way. Just call the [populate](./api.html#query_Query-populate) method on the query and an array of documents will be returned _in place_ of the original `_id`s. + Arrays of refs work the same way. Just call the + [populate](./api.html#query_Query-populate) method on the query and an + array of documents will be returned _in place_ of the original `_id`s. .important :markdown - **Note**: mongoose >= 3.6 exposes the original `_ids` used during population through the [document#populated()](./api.html#document_Document-populated) method. + **Note**: mongoose >= 3.6 exposes the original `_ids` used during + population through the + [document#populated()](./api.html#document_Document-populated) method. h3 Setting Populated Fields :markdown In Mongoose >= 4.0, you can manually populate a field as well. :js - Story.findOne({ title: 'Once upon a timex.' }, function(error, story) { + Story.findOne({ title: 'Casino Royale' }, function(error, story) { if (error) { return handleError(error); } - story._creator = aaron; - console.log(story._creator.name); // prints "Aaron" + story.author = author; + console.log(story.author.name); // prints "Ian Fleming" }); :markdown - Note that this only works for single refs. You currently **can't** manually populate an array of refs. + Note that this only works for single refs. You currently **can't** + manually populate an array of refs. h3 Field selection :markdown - What if we only want a few specific fields returned for the populated documents? This can be accomplished by passing the usual [field name syntax](./api.html#query_Query-select) as the second argument to the populate method: + What if we only want a few specific fields returned for the populated + documents? This can be accomplished by passing the usual + [field name syntax](./api.html#query_Query-select) as the second argument + to the populate method: :js - Story - .findOne({ title: /timex/i }) - .populate('_creator', 'name') // only return the Persons name - .exec(function (err, story) { - if (err) return handleError(err); + Story. + findOne({ title: /casino royale/i }). + populate('author', 'name'). // only return the Persons name + exec(function (err, story) { + if (err) return handleError(err); - console.log('The creator is %s', story._creator.name); - // prints "The creator is Aaron" + console.log('The author is %s', story.author.name); + // prints "The author is Ian Fleming" - console.log('The creators age is %s', story._creator.age); - // prints "The creators age is null' - }) + console.log('The authors age is %s', story.author.age); + // prints "The authors age is null' + }) h3 Populating multiple paths :markdown What if we wanted to populate multiple paths at the same time? :js - Story - .find(...) - .populate('fans _creator') // space delimited path names - .exec() - .important - :markdown - In **mongoose >= 3.6**, we can pass a space delimited string of path names to populate. Before 3.6 you must execute the `populate()` method multiple times. + Story. + find(...). + populate('fans'). + populate('author'). + exec() + :markdown + If you call `populate()` multiple times with the same path, only the last + one will take effect. :js - Story - .find(...) - .populate('fans') - .populate('_creator') - .exec() + // The 2nd `populate()` call below overwrites the first because they + // both populate 'fans'. + Story. + find(). + populate({ path: 'fans', select: 'name' }). + populate({ path: 'fans', select: 'email' }); + // The above is equivalent to: + Story.find().populate({ path: 'fans', select: 'email' }); h3 Query conditions and other options :markdown - What if we wanted to populate our fans array based on their age, select just their names, and return at most, any 5 of them? - :js - Story - .find(...) - .populate({ - path: 'fans', - match: { age: { $gte: 21 }}, - select: 'name -_id', - options: { limit: 5 } - }) - .exec() + What if we wanted to populate our fans array based on their age, select + just their names, and return at most, any 5 of them? + :js + Story. + find(...). + populate({ + path: 'fans', + match: { age: { $gte: 21 }}, + // Explicitly exclude `_id`, see http://bit.ly/2aEfTdB + select: 'name -_id', + options: { limit: 5 } + }). + exec() h3 Refs to children :markdown - We may find however, if we use the `aaron` object, we are unable to get a list of the stories. This is because no `story` objects were ever 'pushed' onto `aaron.stories`. + We may find however, if we use the `author` object, we are unable to get a + list of the stories. This is because no `story` objects were ever 'pushed' + onto `author.stories`. - There are two perspectives here. First, it's nice to have `aaron` know which stories are his. + There are two perspectives here. First, you may want the `author` know + which stories are theirs. Usually, your schema should resolve + one-to-many relationships by having a parent pointer in the 'many' side. + But, if you have a good reason to want an array of child pointers, you + can `push()` documents onto the array as shown below. :js - aaron.stories.push(story1); - aaron.save(callback); + author.stories.push(story1); + author.save(callback); :markdown This allows us to perform a `find` and `populate` combo: :js - Person - .findOne({ name: 'Aaron' }) - .populate('stories') // only works if we pushed refs to children - .exec(function (err, person) { - if (err) return handleError(err); - console.log(person); - }) + Person. + findOne({ name: 'Ian Fleming' }). + populate('stories'). // only works if we pushed refs to children + exec(function (err, person) { + if (err) return handleError(err); + console.log(person); + }); .important :markdown - It is debatable that we really want two sets of pointers as they may get out of sync. Instead we could skip populating and directly `find()` the stories we are interested in. + It is debatable that we really want two sets of pointers as they may get + out of sync. Instead we could skip populating and directly `find()` the + stories we are interested in. :js - Story - .find({ _creator: aaron._id }) - .exec(function (err, stories) { - if (err) return handleError(err); - console.log('The stories are an array: ', stories); - }) + Story. + find({ author: author._id }). + exec(function (err, stories) { + if (err) return handleError(err); + console.log('The stories are an array: ', stories); + }); - h3 Updating refs + .important + :markdown + The documents returned from + [query population](./api.html#query_Query-populate) become fully + functional, `remove`able, `save`able documents unless the + [lean](./api.html#query_Query-lean) option is specified. Do not confuse + them with [sub docs](./subdocs.html). Take caution when calling its + remove method because you'll be removing it from the database, not just + the array. + + h3#populate_an_existing_mongoose_document Populating an existing document + :markdown + If we have an existing mongoose document and want to populate some of its + paths, **mongoose >= 3.6** supports the + [document#populate()](./api.html#document_Document-populate) method. + + h3#populate_multiple_documents Populating multiple existing documents + :markdown + If we have one or many mongoose documents or even plain objects + (_like [mapReduce](./api.html#model_Model.mapReduce) output_), we may + populate them using the [Model.populate()](./api.html#model_Model.populate) + method available in **mongoose >= 3.6**. This is what `document#populate()` + and `query#populate()` use to populate documents. + + h3#deep-populate Populating across multiple levels :markdown - Now that we have a `story` we realized that the `_creator` was incorrect. We can update refs the same as any other property through Mongoose's internal casting: + Say you have a user schema which keeps track of the user's friends. :js - var guille = new Person({ name: 'Guillermo' }); - guille.save(function (err) { - if (err) return handleError(err); + var userSchema = new Schema({ + name: String, + friends: [{ type: ObjectId, ref: 'User' }] + }); + :markdown + Populate lets you get a list of a user's friends, but what if you also + wanted a user's friends of friends? Specify the `populate` option to tell + mongoose to populate the `friends` array of all the user's friends: + :js + User. + findOne({ name: 'Val' }). + populate({ + path: 'friends', + // Get friends of friends - populate the 'friends' array for every friend + populate: { path: 'friends' } + }); + + h3#cross-db-populate Populating across Databases + :markdown + Let's say you have a schema representing events, and a schema representing + conversations. Each event has a corresponding conversation thread. + :js + var eventSchema = new Schema({ + name: String, + // The id of the corresponding conversation + // Notice there's no ref here! + conversation: ObjectId + }); + var conversationSchema = new Schema({ + numMessages: Number + }); + :markdown + Also, suppose that events and conversations are stored in separate MongoDB + instances. + :js + var db1 = mongoose.createConnection('localhost:27000/db1'); + var db2 = mongoose.createConnection('localhost:27001/db2'); - story._creator = guille; - console.log(story._creator.name); - // prints "Guillermo" in mongoose >= 3.6 - // see https://github.com/Automattic/mongoose/wiki/3.6-release-notes + var Event = db1.model('Event', eventSchema); + var Conversation = db2.model('Conversation', conversationSchema); + :markdown + In this situation, you will **not** be able to `populate()` normally. + The `conversation` field will always be null, because `populate()` doesn't + know which model to use. However, + [you can specify the model explicitly](./api.html#model_Model.populate). + :js + Event. + find(). + populate({ path: 'conversation', model: Conversation }). + exec(function(error, docs) { /* ... */ }); + :markdown + This is known as a "cross-database populate," because it enables you to + populate across MongoDB databases and even across MongoDB instances. - story.save(function (err) { - if (err) return handleError(err); + h3#dynamic-ref Dynamic References + :markdown + Mongoose can also populate from multiple collections at the same time. + Let's say you have a user schema that has an array of "connections" - a + user can be connected to either other users or an organization. + :js + var userSchema = new Schema({ + name: String, + connections: [{ + kind: String, + item: { type: ObjectId, refPath: 'connections.kind' } + }] + }); - Story - .findOne({ title: /timex/i }) - .populate({ path: '_creator', select: 'name' }) - .exec(function (err, story) { - if (err) return handleError(err); + var organizationSchema = new Schema({ name: String, kind: String }); - console.log('The creator is %s', story._creator.name) - // prints "The creator is Guillermo" - }) - }) - }) + var User = mongoose.model('User', userSchema); + var Organization = mongoose.model('Organization', organizationSchema); + :markdown + The `refPath` property above means that mongoose will look at the + `connections.kind` path to determine which model to use for `populate()`. + In other words, the `refPath` property enables you to make the `ref` + property dynamic. + :js + // Say we have one organization: + // `{ _id: ObjectId('000000000000000000000001'), name: "Guns N' Roses", kind: 'Band' }` + // And two users: + // { + // _id: ObjectId('000000000000000000000002') + // name: 'Axl Rose', + // connections: [ + // { kind: 'User', item: ObjectId('000000000000000000000003') }, + // { kind: 'Organization', item: ObjectId('000000000000000000000001') } + // ] + // }, + // { + // _id: ObjectId('000000000000000000000003') + // name: 'Slash', + // connections: [] + // } + User. + findOne({ name: 'Axl Rose' }). + populate('connections.item'). + exec(function(error, doc) { + // doc.connections[0].item is a User doc + // doc.connections[1].item is an Organization doc + }); - .important - :markdown - The documents returned from [query population](./api.html#query_Query-populate) become fully functional, `remove`able, `save`able documents unless the [lean](./api.html#query_Query-lean) option is specified. Do not confuse them with [sub docs](./subdocs.html). Take caution when calling its remove method because you'll be removing it from the database, not just the array. + h3#populate-virtuals Populate Virtuals + :markdown + _New in 4.5.0_ + + So far you've only populated based on the `_id` field. However, that's + sometimes not the right choice. + In particular, [arrays that grow without bound are a MongoDB anti-pattern](https://docs.mongodb.com/manual/tutorial/model-referenced-one-to-many-relationships-between-documents/). + Using mongoose virtuals, you can define more sophisticated relationships + between documents. + + :js + var PersonSchema = new Schema({ + name: String, + band: String + }); + + var BandSchema = new Schema({ + name: String + }); + BandSchema.virtual('members', { + ref: 'Person', // The model to use + localField: 'name', // Find people where `localField` + foreignField: 'band', // is equal to `foreignField` + // If `justOne` is true, 'members' will be a single doc as opposed to + // an array. `justOne` is false by default. + justOne: false + }); + + var Person = mongoose.model('Person', PersonSchema); + var Band = mongoose.model('Band', BandSchema); + + /** + * Suppose you have 2 bands: "Guns N' Roses" and "Motley Crue" + * And 4 people: "Axl Rose" and "Slash" with "Guns N' Roses", and + * "Vince Neil" and "Nikki Sixx" with "Motley Crue" + */ + Band.find({}).populate('members').exec(function(error, bands) { + /* `bands.members` is now an array of instances of `Person` */ + }); - h3#populate_an_existing_mongoose_document Populating an existing document :markdown - If we have an existing mongoose document and want to populate some of its paths, **mongoose >= 3.6** supports the [document#populate()](./api.html#document_Document-populate) method. + Keep in mind that virtuals are _not_ included in `toJSON()` output by + default. If you want populate virtuals to show up when using functions + that rely on `JSON.stringify()`, like Express' + [`res.json()` function](http://expressjs.com/en/4x/api.html#res.json), + set the `virtuals: true` option on your schema's `toJSON` options. + + :js + // Set `virtuals: true` so `res.json()` works + var BandSchema = new Schema({ + name: String + }, { toJSON: { virtuals: true } }); - h3#populate_multiple_documents Populating multiple existing documents :markdown - If we have one or many mongoose documents or even plain objects (_like [mapReduce](./api.html#model_Model.mapReduce) output_), we may populate them using the [Model.populate()](./api.html#model_Model.populate) method available in **mongoose >= 3.6**. This is what `document#populate()` and `query#populate()` use to populate documents. + If you're using populate projections, make sure `foreignField` is included + in the projection. + + :js + Band. + find({}). + populate({ path: 'members', select: 'name' }). + exec(function(error, bands) { + // Won't work, foreign field `band` is not selected in the projection + }); + + Band. + find({}). + populate({ path: 'members', select: 'name band' }). + exec(function(error, bands) { + // Works, foreign field `band` is selected + }); h3#next Next Up :markdown diff --git a/docs/prior.html b/docs/prior.html index f8a92d3494e..9b0a4964396 100644 --- a/docs/prior.html +++ b/docs/prior.html @@ -1,9 +1,26 @@ -Mongoose v3.3.1Fork me on GitHub

Prior Release Documentation

\ No newline at end of file +KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/queries.html b/docs/queries.html index 3955265f398..43330ea9930 100644 --- a/docs/queries.html +++ b/docs/queries.html @@ -1,17 +1,16 @@ -Mongoose Queries v3.3.1Fork me on GitHub

Queries

Documents can be retrieved through several static helper methods of models.

Any model method which involves specifying query conditions can be executed two ways:

+Mongoose Queries v4.11.9Fork me on GitHub

Queries

Documents can be retrieved through several static helper methods of models.

Any model method which involves specifying query conditions can be executed two ways:

When a callback function:

-
  • is passed, the operation will be executed immediately with the results passed to the callback.
  • is not passed, an instance of Query is returned, which provides a special QueryBuilder interface for you.
- -

Let's take a look at what happens when passing a callback:

var Person = db.model('Person', yourSchema);
+
  • is passed, the operation will be executed immediately with the results passed to the callback.
  • is not passed, an instance of Query is returned, which provides a special query builder interface.

In mongoose 4, a Query has a .then() function, and thus can be used as a promise.

When executing a query with a callback function, you specify your query as a JSON document. The JSON document's syntax is the same as the MongoDB shell.

var Person = mongoose.model('Person', yourSchema);
 
 // find each person with a last name matching 'Ghost', selecting the `name` and `occupation` fields
 Person.findOne({ 'name.last': 'Ghost' }, 'name occupation', function (err, person) {
   if (err) return handleError(err);
   console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation) // Space Ghost is a talk show host.
-})

Here we see that the query was executed immediately and the results passed to our callback. -Now let's look at what happens when no callback is passed:

// find each person with a last name matching 'Ghost'
+})

Here we see that the query was executed immediately and the results passed to our callback. All callbacks in Mongoose use the pattern: +callback(error, result). If an error occurs executing the query, the error parameter will contain an error document, and result +will be null. If the query is successful, the error parameter will be null, and the result will be populated with the results of the query.

Anywhere a callback is passed to a query in Mongoose, the callback follows the pattern callback(error, results). What results is depends on the operation: For findOne() it is a potentially-null single document, find() a list of documents, count() the number of documents, update() the number of documents affected, etc. The API docs for Models provide more detail on what is passed to the callbacks.

Now let's look at what happens when no callback is passed:

// find each person with a last name matching 'Ghost'
 var query = Person.findOne({ 'name.last': 'Ghost' });
 
 // selecting the `name` and `occupation` fields
@@ -21,21 +20,78 @@
 query.exec(function (err, person) {
   if (err) return handleError(err);
   console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation) // Space Ghost is a talk show host.
-})

An instance of Query was returned which allows us to build up our query. Taking this example further:

Person
-.find({ occupation: /host/ })
-.where('name.last').equals('Ghost')
-.where('age').gt(17).lt(66)
-.where('likes').in(['vaporizing', 'talking'])
-.limit(10)
-.sort('-occupation')
-.select('name occupation')
-.exec(callback);
-

References to other documents

There are no joins in MongoDB but sometimes we still want references to documents in other collections. This is where query#populate comes in. Read more here.

Streaming

Queries can be streamed from MongoDB to your application as well. Simply call the query's stream method instead of exec to return an instance of QueryStream.

Next Up

Now that we've covered Queries, let's take a look at validation.

\ No newline at end of file +KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/queries.jade b/docs/queries.jade index 4c253ebefae..2711fb01c9d 100644 --- a/docs/queries.jade +++ b/docs/queries.jade @@ -28,7 +28,7 @@ block content }) :markdown Here we see that the query was executed immediately and the results passed to our callback. All callbacks in Mongoose use the pattern: - `callback(error, result)`. If an error occurs executing the query, the `error` parameter will contain an error document, and `result` + `callback(error, result)`. If an error occurs executing the query, the `error` parameter will contain an error document, and `result` will be null. If the query is successful, the `error` parameter will be null, and the `result` will be populated with the results of the query. .important :markdown @@ -77,7 +77,31 @@ block content exec(callback); :markdown - A full list of [Query helper functions can be found in the API docs](http://mongoosejs.com/docs/api.html#query-js). + A full list of [Query helper functions can be found in the API docs](./api.html#query-js). + + h3#setters + :markdown + Setters are not executed by default in 4.x. For example, if you lowercase emails in your schema: + + :js + var personSchema = new Schema({ + email: { + type: String, + lowercase: true + } + }); + + :markdown + Mongoose will **not** automatically lowercase the email in your queries, so `Person.find({ email: 'Val@karpov.io' })` would return no results. + Use the `runSettersOnQuery` option to turn on this behavior: + + :js + var personSchema = new Schema({ + email: { + type: String, + lowercase: true + } + }, { runSettersOnQuery: true }); h3#refs References to other documents :markdown @@ -85,7 +109,19 @@ block content h3 Streaming :markdown - Queries can be [streamed](http://nodejs.org/api/stream.html) from MongoDB to your application as well. Simply call the query's [stream](./api.html#query_Query-stream) method instead of [exec](./api.html#query_Query-exec) to return an instance of [QueryStream](./api.html#querystream_QueryStream). Note: QueryStreams are Node.js 0.8 style read streams not Node.js 0.10 style. + You can [stream](http://nodejs.org/api/stream.html) query results from + MongoDB. You need to call the + [Query#cursor()](./api.html#query_Query-cursor) function instead of + [Query#exec](./api.html#query_Query-exec) to return an instance of + [QueryCursor](./api.html#querycursor-js). + :js + var cursor = Person.find({ occupation: /host/ }).cursor(); + cursor.on('data', function(doc) { + // Called once for every document + }); + cursor.on('close', function() { + // Called when done + }); h3#next Next Up :markdown diff --git a/docs/redirect.jade b/docs/redirect.jade index d2abdb05201..9210193ce93 100644 --- a/docs/redirect.jade +++ b/docs/redirect.jade @@ -4,6 +4,7 @@ html(lang='en') meta(charset="utf-8") meta(name="viewport", content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no") title Mongoose + include ./includes/favicon.jade block meta block style link(href='http://fonts.googleapis.com/css?family=Anonymous+Pro:400,700|Droid+Sans+Mono|Open+Sans:400,700|Linden+Hill|Quattrocento:400,700|News+Cycle:400,700|Antic+Slab|Cabin+Condensed:400,700', rel='stylesheet', type='text/css') @@ -23,4 +24,4 @@ html(lang='en') block content script. document.body.className = 'load'; - include includes/googleanalytics + include includes/keen diff --git a/docs/schematypes.html b/docs/schematypes.html index 1676ab74f34..faa1765980f 100644 --- a/docs/schematypes.html +++ b/docs/schematypes.html @@ -1,9 +1,9 @@ -Mongoose SchemaTypes v3.3.1Fork me on GitHub

SchemaTypes

SchemaTypes handle definition of path defaults, validation, getters, setters, field selection defaults for queries and other general characteristics for Strings and Numbers. Check out their respective API documentation for more detail.

Following are all valid Schema Types.

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array

Example

var schema = new Schema({
+Mongoose SchemaTypes v4.11.9Fork me on GitHub

SchemaTypes

SchemaTypes handle definition of path defaultsvalidationgetterssettersfield selection defaults for queries and other general characteristics for Strings and Numbers. Check out their respective API documentation for more detail.

Following are all valid Schema Types.

Example

var schema = new Schema({
   name:    String,
   binary:  Buffer,
   living:  Boolean,
-  updated: { type: Date, default: Date.now }
-  age:     { type: Number, min: 18, max: 65 }
+  updated: { type: Date, default: Date.now },
+  age:     { type: Number, min: 18, max: 65 },
   mixed:   Schema.Types.Mixed,
   _someId: Schema.Types.ObjectId,
   array:      [],
@@ -14,6 +14,8 @@
   ofBoolean:  [Boolean],
   ofMixed:    [Schema.Types.Mixed],
   ofObjectId: [Schema.Types.ObjectId],
+  ofArrays:   [[]]
+  ofArrayOfNumbers: [[Number]]
   nested: {
     stuff: { type: String, lowercase: true, trim: true }
   }
@@ -24,29 +26,79 @@
 var Thing = mongoose.model('Thing', schema);
 
 var m = new Thing;
-m.name = 'Statue of Liberty'
+m.name = 'Statue of Liberty';
 m.age = 125;
 m.updated = new Date;
 m.binary = new Buffer(0);
 m.living = false;
-m.mixed = {[ any: { thing: 'i want' } ]};
+m.mixed = { any: { thing: 'i want' } };
 m.markModified('mixed');
 m._someId = new mongoose.Types.ObjectId;
 m.array.push(1);
 m.ofString.push("strings!");
 m.ofNumber.unshift(1,2,3,4);
-m.ofDate.addToSet(new Date);
+m.ofDates.addToSet(new Date);
 m.ofBuffer.pop();
 m.ofMixed = [1, [], 'three', { four: 5 }];
 m.nested.stuff = 'good';
 m.save(callback);
-

Usage notes:

Mixed

An "anything goes" SchemaType, its flexibility comes at a trade-off of it being harder to maintain. Mixed is available either through Schema.Types.Mixed or by passing an empty object literal. The following are equivalent:

var Any = new Schema({ any: {} });
+

SchemaType Options

You can declare a schema type using the type directly, or an object with +a type property.

var schema1 = new Schema({
+  test: String // `test` is a path of type String
+});
+
+var schema2 = new Schema({
+  test: { type: String } // `test` is a path of type string
+});

In addition to the type property, you can specify additional properties +for a path. For example, if you want to lowercase a string before saving:

var schema2 = new Schema({
+  test: {
+    type: String,
+    lowercase: true // Always convert `test` to lowercase
+  }
+});

The lowercase property only works for strings. There are certain options +which apply for all schema types, and some that apply for specific schema +types.

All Schema Types
  • required: boolean or function, if true adds a required validator for this property
  • default: Any or function, sets a default value for the path. If the value is a function, the return value of the function is used as the default.
  • select: boolean, specifies default projections for queries
  • validate: function, adds a validator function for this property
  • get: function, defines a custom getter for this property using Object.defineProperty().
  • set: function, defines a custom setter for this property using Object.defineProperty().
  • alias: string, mongoose >= 4.10.0 only. Defines a virtual with the given name that gets/sets this path.
var numberSchema = new Schema({
+  integerOnly: {
+    type: Number,
+    get: v => Math.round(v),
+    set: v => Math.round(v),
+    alias: 'i'
+  }
+});
+
+var Number = mongoose.model('Number', numberSchema);
+
+var doc = new Number();
+doc.integerOnly = 2.001;
+doc.integerOnly; // 2
+doc.i; // 2
+doc.i = 3.001;
+doc.integerOnly; // 3
+doc.i; // 3
+
Indexes

You can also define MongoDB indexes +using schema type options.

  • index: boolean, whether to define an on this property.
  • unique: boolean, whether to define a unique index on this property.
  • sparse: boolean, whether to define a sparse index on this property.
var schema2 = new Schema({
+  test: {
+    type: String,
+    index: true,
+    unique: true // Unique index. If you specify `unique: true`
+    // specifying `index: true` is optional if you do `unique: true`
+  }
+});
String
  • lowercase: boolean, whether to always call .toLowerCase() on the value
  • uppercase: boolean, whether to always call .toUpperCase() on the value
  • trim: boolean, whether to always call .trim() on the value
  • match: RegExp, creates a validator that checks if the value matches the given regular expression
  • enum: Array, creates a validator that checks if the value is in the given array.
Number
  • min: Number, creates a validator that checks if the value is greater than or equal to the given minimum.
  • max: Number, creates a validator that checks if the value is less than or equal to the given maximum.
Date
  • min: Date
  • max: Date

Usage notes:

Dates

Built-in Date methods are not hooked into the mongoose change tracking logic which in English means that if you use a Date in your document and modify it with a method like setMonth(), mongoose will be unaware of this change and doc.save() will not persist this modification. If you must modify Date types using built-in methods, tell mongoose about the change with doc.markModified('pathToYourDate') before saving.

var Assignment = mongoose.model('Assignment', { dueDate: Date });
+Assignment.findOne(function (err, doc) {
+  doc.dueDate.setMonth(3);
+  doc.save(callback); // THIS DOES NOT SAVE YOUR CHANGE
+  
+  doc.markModified('dueDate');
+  doc.save(callback); // works
+})
+

Mixed

An "anything goes" SchemaType, its flexibility comes at a trade-off of it being harder to maintain. Mixed is available either through Schema.Types.Mixed or by passing an empty object literal. The following are equivalent:

var Any = new Schema({ any: {} });
+var Any = new Schema({ any: Object });
 var Any = new Schema({ any: Schema.Types.Mixed });

Since it is a schema-less type, you can change the value to anything else you like, but Mongoose loses the ability to auto detect and save those changes. To "tell" Mongoose that the value of a Mixed type has changed, call the .markModified(path) method of the document passing the path to the Mixed type you just changed.

person.anything = { x: [3, 4, { y: "changed" }] };
 person.markModified('anything');
 person.save(); // anything will now get saved

ObjectIds

To specify a type of ObjectId, use Schema.Types.ObjectId in your declaration.

var mongoose = require('mongoose');
 var ObjectId = mongoose.Schema.Types.ObjectId;
-var Car = new Schema({ driver: ObjectId })
-// or just Schema.ObjectId for backwards compatibility with v2

Arrays

Provide creation of arrays of SchemaTypes or Sub-Documents.

var ToySchema = new Schema({ name: String });
+var Car = new Schema({ driver: ObjectId });
+// or just Schema.ObjectId for backwards compatibility with v2

Arrays

Provide creation of arrays of SchemaTypes or Sub-Documents.

var ToySchema = new Schema({ name: String });
 var ToyBox = new Schema({
   toys: [ToySchema],
   buffers: [Buffer],
@@ -56,12 +108,56 @@
 });

Note: specifying an empty array is equivalent to Mixed. The following all create arrays of Mixed:

var Empty1 = new Schema({ any: [] });
 var Empty2 = new Schema({ any: Array });
 var Empty3 = new Schema({ any: [Schema.Types.Mixed] });
-var Empty4 = new Schema({ any: [{}] });

Creating Custom Types

Mongoose can also be extended with custom SchemaTypes. Search the plugins site for compatible types like mongoose-long and other types.

Next Up

Now that we've covered SchemaTypes, let's take a look at Models.

\ No newline at end of file +KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/schematypes.jade b/docs/schematypes.jade index 3dac61f6bed..7be49cf5628 100644 --- a/docs/schematypes.jade +++ b/docs/schematypes.jade @@ -3,25 +3,25 @@ extends layout block content h2 SchemaTypes p - | SchemaTypes handle definition of path + | SchemaTypes handle definition of path  a(href="./api.html#schematype_SchemaType-default") defaults - |, + |,  a(href="./api.html#schematype_SchemaType-validate") validation - |, + |,  a(href="./api.html#schematype_SchemaType-get") getters - |, + |,  a(href="./api.html#schematype_SchemaType-set") setters - |, + |,  a(href="./api.html#schematype_SchemaType-select") field selection defaults - | for + | for  a(href="./api.html#query-js") queries - | and other general characteristics for + | and other general characteristics for  a(href="./api.html#schema-string-js") Strings - | and + | and a(href="./api.html#schema-number-js") Numbers |. Check out their respective API documentation for more detail. p - | Following are all valid + | Following are all valid  a(href="./api.html#schema_Schema.Types") Schema Types |. :markdown @@ -51,6 +51,8 @@ block content ofBoolean: [Boolean], ofMixed: [Schema.Types.Mixed], ofObjectId: [Schema.Types.ObjectId], + ofArrays: [[]], + ofArrayOfNumbers: [[Number]], nested: { stuff: { type: String, lowercase: true, trim: true } } @@ -78,6 +80,99 @@ block content m.nested.stuff = 'good'; m.save(callback); + h3 SchemaType Options + :markdown + You can declare a schema type using the type directly, or an object with + a `type` property. + :js + var schema1 = new Schema({ + test: String // `test` is a path of type String + }); + + var schema2 = new Schema({ + test: { type: String } // `test` is a path of type string + }); + :markdown + In addition to the type property, you can specify additional properties + for a path. For example, if you want to lowercase a string before saving: + :js + var schema2 = new Schema({ + test: { + type: String, + lowercase: true // Always convert `test` to lowercase + } + }); + :markdown + The `lowercase` property only works for strings. There are certain options + which apply for all schema types, and some that apply for specific schema + types. + h5 All Schema Types + :markdown + * `required`: boolean or function, if true adds a [required validator](./validation.html#built-in-validators) for this property + * `default`: Any or function, sets a default value for the path. If the value is a function, the return value of the function is used as the default. + * `select`: boolean, specifies default [projections](https://docs.mongodb.com/manual/tutorial/project-fields-from-query-results/) for queries + * `validate`: function, adds a [validator function](./validation.html#built-in-validators) for this property + * `get`: function, defines a custom getter for this property using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). + * `set`: function, defines a custom setter for this property using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). + * `alias`: string, mongoose >= 4.10.0 only. Defines a [virtual](./guide.html#virtuals) with the given name that gets/sets this path. + + :js + var numberSchema = new Schema({ + integerOnly: { + type: Number, + get: v => Math.round(v), + set: v => Math.round(v), + alias: 'i' + } + }); + + var Number = mongoose.model('Number', numberSchema); + + var doc = new Number(); + doc.integerOnly = 2.001; + doc.integerOnly; // 2 + doc.i; // 2 + doc.i = 3.001; + doc.integerOnly; // 3 + doc.i; // 3 + + h5 Indexes + p + :markdown + You can also define [MongoDB indexes](https://docs.mongodb.com/manual/indexes/) + using schema type options. + + :markdown + * `index`: boolean, whether to define an [index](https://docs.mongodb.com/manual/indexes/) on this property. + * `unique`: boolean, whether to define a [unique index](https://docs.mongodb.com/manual/core/index-unique/) on this property. + * `sparse`: boolean, whether to define a [sparse index](https://docs.mongodb.com/manual/core/index-sparse/) on this property. + :js + var schema2 = new Schema({ + test: { + type: String, + index: true, + unique: true // Unique index. If you specify `unique: true` + // specifying `index: true` is optional if you do `unique: true` + } + }); + h5 String + :markdown + * `lowercase`: boolean, whether to always call `.toLowerCase()` on the value + * `uppercase`: boolean, whether to always call `.toUpperCase()` on the value + * `trim`: boolean, whether to always call `.trim()` on the value + * `match`: RegExp, creates a [validator](./validation.html) that checks if the value matches the given regular expression + * `enum`: Array, creates a [validator](./validation.html) that checks if the value is in the given array. + + h5 Number + :markdown + * `min`: Number, creates a [validator](./validation.html) that checks if the value is greater than or equal to the given minimum. + * `max`: Number, creates a [validator](./validation.html) that checks if the value is less than or equal to the given maximum. + + h5 Date + :markdown + * `min`: Date + * `max`: Date + h3 Usage notes: h4#Dates Dates :markdown @@ -97,9 +192,10 @@ block content p An "anything goes" SchemaType, its flexibility comes at a trade-off of it being harder to maintain. Mixed is available either through Schema.Types.Mixed or by passing an empty object literal. The following are equivalent: :js var Any = new Schema({ any: {} }); + var Any = new Schema({ any: Object }); var Any = new Schema({ any: Schema.Types.Mixed }); p - | Since it is a schema-less type, you can change the value to anything else you like, but Mongoose loses the ability to auto detect and save those changes. To "tell" Mongoose that the value of a Mixed type has changed, call the + | Since it is a schema-less type, you can change the value to anything else you like, but Mongoose loses the ability to auto detect and save those changes. To "tell" Mongoose that the value of a Mixed type has changed, call the code .markModified(path) | method of the document passing the path to the Mixed type you just changed. :js @@ -108,7 +204,7 @@ block content person.save(); // anything will now get saved h4#objectids ObjectIds p - | To specify a type of ObjectId, use + | To specify a type of ObjectId, use code Schema.Types.ObjectId | in your declaration. :js @@ -118,9 +214,9 @@ block content // or just Schema.ObjectId for backwards compatibility with v2 h4#arrays Arrays p - | Provide creation of arrays of + | Provide creation of arrays of a(href="./api.html#schema_Schema.Types") SchemaTypes - | or + | or a(href="./subdocs.html") Sub-Documents |. :js @@ -133,9 +229,9 @@ block content // ... etc }); p - | Note: specifying an empty array is equivalent to + | Note: specifying an empty array is equivalent to code Mixed - |. The following all create arrays of + |. The following all create arrays of code Mixed |: :js @@ -143,18 +239,67 @@ block content var Empty2 = new Schema({ any: Array }); var Empty3 = new Schema({ any: [Schema.Types.Mixed] }); var Empty4 = new Schema({ any: [{}] }); + p + | Arrays implicitly have a default value of `[]` (empty array). + :js + var Toy = mongoose.model('Test', ToySchema); + console.log((new Toy()).toys); // [] + p + | To overwrite this default, you need to set the default value to `undefined` + :js + var ToySchema = new Schema({ + toys: { + type: [ToySchema], + default: undefined + } + }); + p + | If an array is marked as `required`, it must have at least one element. + :js + var ToySchema = new Schema({ + toys: { + type: [ToySchema], + required: true + } + }); + var Toy = mongoose.model('Toy', ToySchema); + Toy.create({ toys: [] }, function(error) { + console.log(error.errors['toys'].message); // Path "toys" is required. + }); h3#customtypes Creating Custom Types p - | Mongoose can also be extended with custom SchemaTypes. Search the + | Mongoose can also be extended with custom SchemaTypes. Search the a(href="http://plugins.mongoosejs.com") plugins - | site for compatible types like + | site for compatible types like a(href="https://github.com/aheckmann/mongoose-long") mongoose-long - | and + | ,  + a(href="https://github.com/vkarpov15/mongoose-int32") mongoose-int32 + | and a(href="https://github.com/aheckmann/mongoose-function") other - | + | a(href="https://github.com/OpenifyIt/mongoose-types") types |. + h3#path The `schema.path()` Function + :markdown + The `schema.path()` function returns the instantiated schema type for a + given path. + :js + var sampleSchema = new Schema({ name: { type: String, required: true } }); + console.log(sampleSchema.path('name')); + // Output looks like: + /** + * SchemaString { + * enumValues: [], + * regExp: null, + * path: 'name', + * instance: 'String', + * validators: ... + */ + :markdown + You can use this function to inspect the schema type for a given path, + including what validators it has and what the type is. + h3#next Next Up :markdown Now that we've covered `SchemaTypes`, let's take a look at [Models](/docs/models.html). diff --git a/docs/source/acquit.js b/docs/source/acquit.js index e260fcefabf..91ede15c43e 100644 --- a/docs/source/acquit.js +++ b/docs/source/acquit.js @@ -1,10 +1,16 @@ var fs = require('fs'); var acquit = require('acquit'); +var hl = require('highlight.js'); var marked = require('marked'); require('acquit-ignore')(); var files = [ + { + input: 'test/docs/defaults.test.js', + output: 'defaults.html', + title: 'Defaults' + }, { input: 'test/docs/discriminators.test.js', output: 'discriminators.html', @@ -14,6 +20,21 @@ var files = [ input: 'test/docs/promises.test.js', output: 'promises.html', title: 'Promises' + }, + { + input: 'test/docs/schematypes.test.js', + output: 'customschematypes.html', + title: 'Custom Schema Types' + }, + { + input: 'test/docs/validation.test.js', + output: 'validation.html', + title: 'Validation' + }, + { + input: 'test/docs/schemas.test.es6.js', + output: 'advanced_schemas.html', + title: 'Advanced Schemas' } ]; @@ -29,6 +50,9 @@ files.forEach(function(file) { block.comments[last] = marked(acquit.trimEachLine(block.comments[last])); } + if (block.code) { + b.code = hl.highlight('javascript', b.code).value; + } for (var j = 0; j < block.blocks.length; ++j) { var b = block.blocks[j]; @@ -38,6 +62,9 @@ files.forEach(function(file) { var last = b.comments.length - 1; b.comments[last] = marked(acquit.trimEachLine(b.comments[last])); } + if (b.code) { + b.code = hl.highlight('javascript', b.code).value; + } } } diff --git a/docs/source/api.js b/docs/source/api.js index f1e01d1dcb9..031a9a9089d 100644 --- a/docs/source/api.js +++ b/docs/source/api.js @@ -4,8 +4,8 @@ var fs = require('fs'); var link = require('../helpers/linktype'); -var hl = require('highlight.js') -var md = require('markdown') +var hl = require('highlight.js'); +var md = require('markdown'); module.exports = { docs: [] @@ -39,8 +39,9 @@ function parse (docs) { var constructor = null; json.forEach(function (comment) { - if (comment.description) + if (comment.description) { highlight(comment.description); + } var prop = false; comment.params = []; @@ -102,6 +103,8 @@ function parse (docs) { case 'param': comment.params.unshift(tag); comment.tags.splice(i, 1); + tag.description = tag.description ? + md.parse(tag.description).replace(/^

/, '').replace(/<\/p>$/, '') : ''; break; case 'return': comment.return = tag; @@ -139,7 +142,14 @@ function parse (docs) { props = props.filter(ignored); function ignored (method) { - if (method.ignore) return false; + if (method.ignore) { + return false; + } + // Ignore eslint declarations + if (method.description && method.description.full && + method.description.full.indexOf('

eslint') === 0) { + return false; + } return true; } @@ -147,7 +157,7 @@ function parse (docs) { // add constructor to properties too methods.some(function (method) { - if (method.ctx && 'method' == method.ctx.type && method.ctx.hasOwnProperty('constructor')) { + if (method.ctx && 'method' === method.ctx.type && method.ctx.hasOwnProperty('constructor')) { props.forEach(function (prop) { prop.ctx.constructor = method.ctx.constructor; }); @@ -201,7 +211,7 @@ function fix (str) { // parse out the ```language var code = /^(?:`{3}([^\n]+)\n)?([\s\S]*)/gm.exec($2); - if ('js' == code[1] || !code[1]) { + if ('js' === code[1] || !code[1]) { code[1] = 'javascript'; } @@ -226,9 +236,9 @@ function order (docs) { for (var i = 0; i < docs.length; ++i) { var doc = docs[i]; - if ('index.js' == doc.title) { + if ('index.js' === doc.title) { docs.unshift(docs.splice(i, 1)[0]); - } else if ('collection.js' == doc.title) { + } else if ('collection.js' === doc.title) { docs.push(docs.splice(i, 1)[0]); } diff --git a/docs/source/index.js b/docs/source/index.js index 487bf20b8f0..1e63db42054 100644 --- a/docs/source/index.js +++ b/docs/source/index.js @@ -13,7 +13,6 @@ exports['docs/documents.jade'] = { guide: true, docs: true, title: 'Documents' } exports['docs/models.jade'] = { guide: true, title: 'Models' } exports['docs/queries.jade'] = { guide: true, title: 'Queries' } exports['docs/populate.jade'] = { guide: true, title: 'Query Population' } -exports['docs/validation.jade'] = { guide: true, title: 'Validation' } exports['docs/migration.jade'] = { guide: true, title: 'Migration Guide' } exports['docs/contributing.jade'] = { guide: true, title: 'Contributing' } exports['docs/connections.jade'] = { guide: true, title: 'Connecting to MongoDB' } diff --git a/docs/subdocs.html b/docs/subdocs.html index efec661a579..7a0c8ae5590 100644 --- a/docs/subdocs.html +++ b/docs/subdocs.html @@ -1,49 +1,138 @@ -Mongoose SubDocuments v3.3.1Fork me on GitHub

Sub Docs

Sub-documents are docs with schemas of their own which are elements of a parents document array:

var childSchema = new Schema({ name: 'string' });
+Mongoose SubDocuments v4.11.9Fork me on GitHub

Sub Docs

Subdocuments are documents embedded in other documents. In Mongoose, this +means you can nest schemas in other schemas. Mongoose has two +distinct notions of subdocuments: arrays of subdocuments and single nested +subdocuments.

var childSchema = new Schema({ name: 'string' });
 
 var parentSchema = new Schema({
-  children: [childSchema]
-})
-

Sub-documents enjoy all the same features as normal documents. The only difference is that they are not saved individually, they are saved whenever their top-level parent document is saved.

var Parent = db.model('Parent', parentSchema);
+  // Array of subdocuments
+  children: [childSchema],
+  // Single nested subdocuments. Caveat: single nested subdocs only work
+  // in mongoose >= 4.2.0
+  child: childSchema
+});
+

Subdocuments are similar to normal documents. Nested schemas can have +middleware, custom validation logic, +virtuals, and any other feature top-level schemas can use. The major +difference is that subdocuments are +not saved individually, they are saved whenever their top-level parent +document is saved.

var Parent = mongoose.model('Parent', parentSchema);
 var parent = new Parent({ children: [{ name: 'Matt' }, { name: 'Sarah' }] })
 parent.children[0].name = 'Matthew';
+
+// `parent.children[0].save()` is a no-op, it triggers middleware but
+// does **not** actually save the subdocument. You need to save the parent
+// doc.
 parent.save(callback);
-

If an error occurs in a sub-documents' middleware, it is bubbled up to the save() callback of the parent, so error handling is a snap!

childSchema.pre('save', function (next) {
-  if ('invalid' == this.name) return next(new Error('#sadpanda'));
+

Subdocuments have save and validate middleware +just like top-level documents. Calling save() on the parent document triggers +the save() middleware for all its subdocuments, and the same for validate() +middleware.

childSchema.pre('save', function (next) {
+  if ('invalid' == this.name) {
+    return next(new Error('#sadpanda'));
+  }
   next();
 });
 
 var parent = new Parent({ children: [{ name: 'invalid' }] });
 parent.save(function (err) {
   console.log(err.message) // #sadpanda
-})
-

Finding a sub-document

Each document has an _id. DocumentArrays have a special id method for looking up a document by its _id.

var doc = parent.children.id(id);
-

Adding sub-docs

MongooseArray methods such as push, unshift, addToSet, and others cast arguments to their proper types transparently:

var Parent = db.model('Parent');
+});
+

Subdocuments' pre('save') and pre('validate') middleware execute +before the top-level document's pre('save') but after the +top-level document's pre('validate') middleware. This is because validating +before save() is actually a piece of built-in middleware.

// Below code will print out 1-4 in order
+var childSchema = new mongoose.Schema({ name: 'string' });
+
+childSchema.pre('validate', function(next) {
+  console.log('2');
+  next();
+});
+
+childSchema.pre('save', function(next) {
+  console.log('3');
+  next();
+});
+
+var parentSchema = new mongoose.Schema({
+  child: childSchema,
+    });
+    
+parentSchema.pre('validate', function(next) {
+  console.log('1');
+  next();
+});
+
+parentSchema.pre('save', function(next) {
+  console.log('4');
+  next();
+});
+
+

Finding a sub-document

Each subdocument has an _id by default. Mongoose document arrays have a +special id method +for searching a document array to find a document with a given _id.

var doc = parent.children.id(_id);
+

Adding sub-docs to arrays

MongooseArray methods such as +push, +unshift, +addToSet, +and others cast arguments to their proper types transparently:

var Parent = mongoose.model('Parent');
 var parent = new Parent;
 
 // create a comment
-post.children.push({ name: 'Liesl' });
-var doc = post.children[0];
-console.log(doc) // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' }
-doc.isNew; // true
+parent.children.push({ name: 'Liesl' });
+var subdoc = parent.children[0];
+console.log(subdoc) // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' }
+subdoc.isNew; // true
 
-post.save(function (err) {
+parent.save(function (err) {
   if (err) return handleError(err)
   console.log('Success!');
-});

Sub-docs may also be created without adding them to the array by using the create method of MongooseArrays.

var newdoc = post.children.create({ name: 'Aaron' });
-

Removing docs

Each sub-document has it's own remove method.

var doc = parent.children.id(id).remove();
+});

Sub-docs may also be created without adding them to the array by using the +create +method of MongooseArrays.

var newdoc = parent.children.create({ name: 'Aaron' });
+

Removing subdocs

Each subdocument has it's own +remove method. For +an array subdocument, this is equivalent to calling .pull() on the +subdocument. For a single nested subdocument, remove() is equivalent +to setting the subdocument to null.

// Equivalent to `parent.children.pull(_id)`
+parent.children.id(_id).remove();
+// Equivalent to `parent.child = null`
+parent.child.remove();
 parent.save(function (err) {
   if (err) return handleError(err);
-  console.log('the sub-doc was removed')
+  console.log('the subdocs were removed');
 });
-

Alternate declaration syntax

New in v3 If you don't need access to the sub-document schema instance, you may also declare sub-docs by simply passing an object literal:

var parentSchema = new Schema({
+

Alternate declaration syntax for arrays

If you create a schema with an array of objects, mongoose will automatically +convert the object to a schema for you:

var parentSchema = new Schema({
   children: [{ name: 'string' }]
-})
-

Next Up

Now that we've covered Sub-documents, let's take a look at querying.

\ No newline at end of file +}); +// Equivalent +var parentSchema = new Schema({ + children: [new Schema({ name: 'string' })] +}); +

Next Up

Now that we've covered Sub-documents, let's take a look at +querying.

\ No newline at end of file diff --git a/docs/subdocs.jade b/docs/subdocs.jade index 5b54e2092eb..9d59e37225c 100644 --- a/docs/subdocs.jade +++ b/docs/subdocs.jade @@ -3,47 +3,101 @@ extends layout block content h2 Sub Docs :markdown - [Sub-documents](./api.html#types-embedded-js) are docs with schemas of - their own which are elements of a parents document array: + Subdocuments are documents embedded in other documents. In Mongoose, this + means you can nest schemas in other schemas. Mongoose has two + distinct notions of subdocuments: arrays of subdocuments and single nested + subdocuments. :js var childSchema = new Schema({ name: 'string' }); var parentSchema = new Schema({ - children: [childSchema] - }) + // Array of subdocuments + children: [childSchema], + // Single nested subdocuments. Caveat: single nested subdocs only work + // in mongoose >= 4.2.0 + child: childSchema + }); :markdown - Sub-documents enjoy all the same features as normal - [documents](./api.html#document-js). The only difference is that they are + Subdocuments are similar to normal documents. Nested schemas can have + [middleware](./middleware.html), [custom validation logic](./validation.html), + virtuals, and any other feature top-level schemas can use. The major + difference is that subdocuments are not saved individually, they are saved whenever their top-level parent document is saved. :js var Parent = mongoose.model('Parent', parentSchema); var parent = new Parent({ children: [{ name: 'Matt' }, { name: 'Sarah' }] }) parent.children[0].name = 'Matthew'; + + // `parent.children[0].save()` is a no-op, it triggers middleware but + // does **not** actually save the subdocument. You need to save the parent + // doc. parent.save(callback); :markdown - If an error occurs in a sub-documents' middleware, it is bubbled up to the `save()` callback of the parent, so error handling is a snap! + Subdocuments have `save` and `validate` [middleware](./middleware.html) + just like top-level documents. Calling `save()` on the parent document triggers + the `save()` middleware for all its subdocuments, and the same for `validate()` + middleware. :js childSchema.pre('save', function (next) { - if ('invalid' == this.name) return next(new Error('#sadpanda')); + if ('invalid' == this.name) { + return next(new Error('#sadpanda')); + } next(); }); var parent = new Parent({ children: [{ name: 'invalid' }] }); parent.save(function (err) { console.log(err.message) // #sadpanda - }) + }); + + :markdown + Subdocuments' `pre('save')` and `pre('validate')` middleware execute + **before** the top-level document's `pre('save')` but **after** the + top-level document's `pre('validate')` middleware. This is because validating + before `save()` is actually a piece of built-in middleware. + + :js + // Below code will print out 1-4 in order + var childSchema = new mongoose.Schema({ name: 'string' }); + + childSchema.pre('validate', function(next) { + console.log('2'); + next(); + }); + + childSchema.pre('save', function(next) { + console.log('3'); + next(); + }); + + var parentSchema = new mongoose.Schema({ + child: childSchema, + }); + + parentSchema.pre('validate', function(next) { + console.log('1'); + next(); + }); + + parentSchema.pre('save', function(next) { + console.log('4'); + next(); + }); + h3 Finding a sub-document :markdown - Each document has an `_id`. DocumentArrays have a special [id](./api.html#types_documentarray_MongooseDocumentArray-id) method for looking up a document by its `_id`. + Each subdocument has an `_id` by default. Mongoose document arrays have a + special [id](./api.html#types_documentarray_MongooseDocumentArray-id) method + for searching a document array to find a document with a given `_id`. :js - var doc = parent.children.id(id); + var doc = parent.children.id(_id); - h3 Adding sub-docs + h3 Adding sub-docs to arrays :markdown MongooseArray methods such as [push](./api.html#types_array_MongooseArray.push), @@ -71,27 +125,37 @@ block content :js var newdoc = parent.children.create({ name: 'Aaron' }); - h3 Removing docs + h3 Removing subdocs :markdown - Each sub-document has it's own - [remove](./api.html#types_embedded_EmbeddedDocument-remove) method. + Each subdocument has it's own + [remove](./api.html#types_embedded_EmbeddedDocument-remove) method. For + an array subdocument, this is equivalent to calling `.pull()` on the + subdocument. For a single nested subdocument, `remove()` is equivalent + to setting the subdocument to `null`. :js - var doc = parent.children.id(id).remove(); + // Equivalent to `parent.children.pull(_id)` + parent.children.id(_id).remove(); + // Equivalent to `parent.child = null` + parent.child.remove(); parent.save(function (err) { if (err) return handleError(err); - console.log('the sub-doc was removed') + console.log('the subdocs were removed'); }); - h4#altsyntax Alternate declaration syntax + h4#altsyntax Alternate declaration syntax for arrays :markdown - _New in v3_ If you don't need access to the sub-document schema instance, - you may also declare sub-docs by simply passing an object literal: + If you create a schema with an array of objects, mongoose will automatically + convert the object to a schema for you: :js var parentSchema = new Schema({ children: [{ name: 'string' }] - }) + }); + // Equivalent + var parentSchema = new Schema({ + children: [new Schema({ name: 'string' })] + }); h3#next Next Up :markdown Now that we've covered `Sub-documents`, let's take a look at - [querying](/docs/queries.html). + [querying](./queries.html). diff --git a/docs/validation.html b/docs/validation.html index 079ab5315a6..fe7c9c6355a 100644 --- a/docs/validation.html +++ b/docs/validation.html @@ -1,37 +1,451 @@ -Mongoose Validation v3.3.1Fork me on GitHub

Validation

Before we get into the specifics of validation syntax, please keep the following rules in mind:

+Mongoose Validation v4.11.9Fork me on GitHub

Validation

+

Before we get into the specifics of validation syntax, please keep the following rules in mind:

+
    +
  • Validation is defined in the SchemaType
  • +
  • Validation is middleware. Mongoose registers validation as a pre('save') hook on every schema by default.
  • +
  • You can manually run validation using doc.validate(callback) or doc.validateSync()
  • +
  • Validators are not run on undefined values. The only exception is the required validator.
  • +
  • Validation is asynchronously recursive; when you call Model#save, sub-document validation is executed as well. If an error occurs, your Model#save callback receives it
  • +
  • Validation is customizable
  • +
+

+    var schema = new Schema({
+      name: {
+        type: String,
+        required: true
+      }
+    });
+    var Cat = db.model('Cat', schema);
 
-
  • Validation is defined in the SchemaType
  • Validation is an internal piece of middleware
  • Validation occurs when a document attempts to be saved, after defaults have been applied
  • Validation is asynchronously recursive: when you call Model#save, sub-document validation is executed. If an error happens, your Model#save callback receives it
  • Mongoose doesn't care about complex error message construction. Errors have type identifiers. For example, "min" is the identifier for the error triggered when a number doesn't meet the minimum value. The path and value that triggered the error can be accessed in the ValidationError object

Built in validators

Mongoose has several built in validators.

+ // This cat has no name :( + var cat = new Cat(); + cat.save(function(error) { + assert.equal(error.errors['name'].message, + 'Path `name` is required.'); -

Custom validators

Custom validation is declared by passing a validation function and an error type to your SchemaTypes validate method. Read the API docs for details on custom validators, async validators, and more.

Validation errors

Errors returned after failed validation contain an errors object holding the actual ValidatorErrors. Each ValidatorError has a type and path property providing us with a little more error handling flexibility.

var toySchema = new Schema({
-  color: String,
-  name: String
-});
+      error = cat.validateSync();
+      assert.equal(error.errors['name'].message,
+        'Path `name` is required.');
+    });
+  

Built-in Validators

+

Mongoose has several built-in validators.

+ +

Each of the validator links above provide more information about how to enable them and customize their error messages.

+

+    var breakfastSchema = new Schema({
+      eggs: {
+        type: Number,
+        min: [6, 'Too few eggs'],
+        max: 12
+      },
+      bacon: {
+        type: Number,
+        required: [true, 'Why no bacon?']
+      },
+      drink: {
+        type: String,
+        enum: ['Coffee', 'Tea'],
+        required: function() {
+          return this.bacon > 3;
+        }
+      }
+    });
+    var Breakfast = db.model('Breakfast', breakfastSchema);
 
-var Toy = db.model('Toy', toySchema);
+    var badBreakfast = new Breakfast({
+      eggs: 2,
+      bacon: 0,
+      drink: 'Milk'
+    });
+    var error = badBreakfast.validateSync();
+    assert.equal(error.errors['eggs'].message,
+      'Too few eggs');
+    assert.ok(!error.errors['bacon']);
+    assert.equal(error.errors['drink'].message,
+      '`Milk` is not a valid enum value for path `drink`.');
 
-Toy.schema.path('color').validate(function (value) {
-  return /blue|green|white|red|orange|periwinkel/i.test(value);
-}, 'Invalid color');
+    badBreakfast.bacon = 5;
+    badBreakfast.drink = null;
 
-var toy = new Toy({ color: 'grease'});
+    error = badBreakfast.validateSync();
+    assert.equal(error.errors['drink'].message, 'Path `drink` is required.');
 
-toy.save(function (err) {
-  // err.errors.color is a ValidatorError object
+    badBreakfast.bacon = null;
+    error = badBreakfast.validateSync();
+    assert.equal(error.errors['bacon'].message, 'Why no bacon?');
+  

The unique Option is Not a Validator

+

A common gotcha for beginners is that the unique option for schemas +is not a validator. It's a convenient helper for building MongoDB unique indexes. +See the FAQ for more information.

+

+    var uniqueUsernameSchema = new Schema({
+      username: {
+        type: String,
+        unique: true
+      }
+    });
+    var U1 = db.model('U1', uniqueUsernameSchema);
+    var U2 = db.model('U2', uniqueUsernameSchema);
+
+    var dup = [{ username: 'Val' }, { username: 'Val' }];
+    U1.create(dup, function(error) {
+      // Will save successfully!
+    });
+
+    // Need to wait for the index to finish building before saving,
+    // otherwise unique constraints may be violated.
+    U2.on('index', function(error) {
+      assert.ifError(error);
+      U2.create(dup, function(error) {
+        // Will error, but will *not* be a mongoose validation error, but
+        // a duplicate key error.
+        assert.ok(error);
+        assert.ok(!error.errors);
+        assert.ok(error.message.indexOf('duplicate key error') !== -1);
+      });
+    });
+  

Custom Validators

+

If the built-in validators aren't enough, you can define custom validators +to suit your needs.

+

Custom validation is declared by passing a validation function. +You can find detailed instructions on how to do this in the +SchemaType#validate() API docs.

+

+    var userSchema = new Schema({
+      phone: {
+        type: String,
+        validate: {
+          validator: function(v) {
+            return /\d{3}-\d{3}-\d{4}/.test(v);
+          },
+          message: '{VALUE} is not a valid phone number!'
+        },
+        required: [true, 'User phone number required']
+      }
+    });
+
+    var User = db.model('user', userSchema);
+    var user = new User();
+    var error;
+
+    user.phone = '555.0123';
+    error = user.validateSync();
+    assert.equal(error.errors['phone'].message,
+      '555.0123 is not a valid phone number!');
+
+    user.phone = '';
+    error = user.validateSync();
+    assert.equal(error.errors['phone'].message,
+      'User phone number required');
+
+    user.phone = '201-555-0123';
+    // Validation succeeds! Phone number is defined
+    // and fits `DDD-DDD-DDDD`
+    error = user.validateSync();
+    assert.equal(error, null);
+  

Async Custom Validators

+

Custom validators can also be asynchronous. If your validator function +takes 2 arguments, mongoose will assume the 2nd argument is a callback.

+

Even if you don't want to use asynchronous validators, be careful, +because mongoose 4 will assume that all functions that take 2 +arguments are asynchronous, like the +validator.isEmail function. +This behavior is considered deprecated as of 4.9.0, and you can shut +it off by specifying isAsync: false on your custom validator.

+

+    var userSchema = new Schema({
+      phone: {
+        type: String,
+        validate: {
+          // `isAsync` is not strictly necessary in mongoose 4.x, but relying
+          // on 2 argument validators being async is deprecated. Set the
+          // `isAsync` option to `true` to make deprecation warnings go away.
+          isAsync: true,
+          validator: function(v, cb) {
+            setTimeout(function() {
+              var phoneRegex = /\d{3}-\d{3}-\d{4}/;
+              var msg = v + ' is not a valid phone number!';
+              // First argument is a boolean, whether validator succeeded
+              // 2nd argument is an optional error message override
+              cb(phoneRegex.test(v), msg);
+            }, 5);
+          },
+          // Default error message, overridden by 2nd argument to `cb()` above
+          message: 'Default error message'
+        },
+        required: [true, 'User phone number required']
+      },
+      name: {
+        type: String,
+        // You can also make a validator async by returning a promise. If you
+        // return a promise, do **not** specify the `isAsync` option.
+        validate: function(v) {
+          return new Promise(function(resolve, reject) {
+            setTimeout(function() {
+              resolve(false);
+            }, 5);
+          });
+        }
+      }
+    });
+
+    var User = db.model('User', userSchema);
+    var user = new User();
+    var error;
+
+    user.phone = '555.0123';
+    user.name = 'test';
+    user.validate(function(error) {
+      assert.ok(error);
+      assert.equal(error.errors['phone'].message,
+        '555.0123 is not a valid phone number!');
+      assert.equal(error.errors['name'].message,
+        'Validator failed for path `name` with value `test`');
+    });
+  

Validation Errors

+

Errors returned after failed validation contain an errors object +holding the actual ValidatorError objects. Each +ValidatorError has kind, path, +value, and message properties.

+

+    var toySchema = new Schema({
+      color: String,
+      name: String
+    });
+
+    var Toy = db.model('Toy', toySchema);
+
+    var validator = function (value) {
+      return /blue|green|white|red|orange|periwinkle/i.test(value);
+    };
+    Toy.schema.path('color').validate(validator,
+      'Color `{VALUE}` not valid', 'Invalid color');
+
+    var toy = new Toy({ color: 'grease'});
+
+    toy.save(function (err) {
+      // err is our ValidationError object
+      // err.errors.color is a ValidatorError object
+      assert.equal(err.errors.color.message, 'Color `grease` not valid');
+      assert.equal(err.errors.color.kind, 'Invalid color');
+      assert.equal(err.errors.color.path, 'color');
+      assert.equal(err.errors.color.value, 'grease');
+      assert.equal(err.name, 'ValidationError');
+    });
+  

Required Validators On Nested Objects

+

Defining validators on nested objects in mongoose is tricky, because +nested objects are not fully fledged paths.

+

+    var personSchema = new Schema({
+      name: {
+        first: String,
+        last: String
+      }
+    });
+
+    assert.throws(function() {
+      // This throws an error, because 'name' isn't a full fledged path
+      personSchema.path('name').required(true);
+    }, /Cannot.*'required'/);
+
+    // To make a nested object required, use a single nested schema
+    var nameSchema = new Schema({
+      first: String,
+      last: String
+    });
+
+    personSchema = new Schema({
+      name: {
+        type: nameSchema,
+        required: true
+      }
+    });
+
+    var Person = db.model('Person', personSchema);
+
+    var person = new Person();
+    var error = person.validateSync();
+    assert.ok(error.errors['name']);
+  

Update Validators

+

In the above examples, you learned about document validation. Mongoose also +supports validation for update() and findOneAndUpdate() operations. +In Mongoose 4.x, update validators are off by default - you need to specify +the runValidators option.

+

To turn on update validators, set the runValidators option for +update() or findOneAndUpdate(). Be careful: update validators +are off by default because they have several caveats.

+

+    var toySchema = new Schema({
+      color: String,
+      name: String
+    });
+
+    var Toy = db.model('Toys', toySchema);
+
+    Toy.schema.path('color').validate(function (value) {
+      return /blue|green|white|red|orange|periwinkle/i.test(value);
+    }, 'Invalid color');
+
+    var opts = { runValidators: true };
+    Toy.update({}, { color: 'bacon' }, opts, function (err) {
+      assert.equal(err.errors.color.message,
+        'Invalid color');
+    });
+  

Update Validators and this

+

There are a couple of key differences between update validators and +document validators. In the color validation function above, this refers +to the document being validated when using document validation. +However, when running update validators, the document being updated +may not be in the server's memory, so by default the value of this is +not defined.

+

+    var toySchema = new Schema({
+      color: String,
+      name: String
+    });
+
+    toySchema.path('color').validate(function(value) {
+      // When running in `validate()` or `validateSync()`, the
+      // validator can access the document using `this`.
+      // Does **not** work with update validators.
+      if (this.name.toLowerCase().indexOf('red') !== -1) {
+        return value !== 'red';
+      }
+      return true;
+    });
+
+    var Toy = db.model('ActionFigure', toySchema);
+
+    var toy = new Toy({ color: 'red', name: 'Red Power Ranger' });
+    var error = toy.validateSync();
+    assert.ok(error.errors['color']);
+
+    var update = { color: 'red', name: 'Red Power Ranger' };
+    var opts = { runValidators: true };
+
+    Toy.update({}, update, opts, function(error) {
+      // The update validator throws an error:
+      // "TypeError: Cannot read property 'toLowerCase' of undefined",
+      // because `this` is **not** the document being updated when using
+      // update validators
+      assert.ok(error);
+    });
+  

The context option

+

The context option lets you set the value of this in update validators +to the underlying query.

+

+    toySchema.path('color').validate(function(value) {
+      // When running update validators with the `context` option set to
+      // 'query', `this` refers to the query object.
+      if (this.getUpdate().$set.name.toLowerCase().indexOf('red') !== -1) {
+        return value === 'red';
+      }
+      return true;
+    });
+
+    var Toy = db.model('Figure', toySchema);
+
+    var update = { color: 'blue', name: 'Red Power Ranger' };
+    // Note the context option
+    var opts = { runValidators: true, context: 'query' };
+
+    Toy.update({}, update, opts, function(error) {
+      assert.ok(error.errors['color']);
+    });
+  

Update Validator Paths

+

The other key difference that update validators only run on the paths +specified in the update. For instance, in the below example, because +'name' is not specified in the update operation, update validation will +succeed.

+

When using update validators, required validators only fail when +you try to explicitly $unset the key.

+

+    var kittenSchema = new Schema({
+      name: { type: String, required: true },
+      age: Number
+    });
+
+    var Kitten = db.model('Kitten', kittenSchema);
+
+    var update = { color: 'blue' };
+    var opts = { runValidators: true };
+    Kitten.update({}, update, opts, function(err) {
+      // Operation succeeds despite the fact that 'name' is not specified
+    });
+
+    var unset = { $unset: { name: 1 } };
+    Kitten.update({}, unset, opts, function(err) {
+      // Operation fails because 'name' is required
+      assert.ok(err);
+      assert.ok(err.errors['name']);
+    });
+  

Update Validators Only Run On Specified Paths

+

One final detail worth noting: update validators only run on $set +and $unset operations (and $push and $addToSet in >= 4.8.0). +For instance, the below update will succeed, regardless of the value of +number, because update validators ignore $inc.

+

+    var testSchema = new Schema({
+      number: { type: Number, max: 0 },
+    });
+
+    var Test = db.model('Test', testSchema);
+
+    var update = { $inc: { number: 1 } };
+    var opts = { runValidators: true };
+    Test.update({}, update, opts, function(error) {
+      // There will never be a validation error here
+    });
+  

On $push and $addToSet

+

New in 4.8.0: update validators also run on $push and $addToSet

+

+    var testSchema = new Schema({
+      numbers: [{ type: Number, max: 0 }],
+      docs: [{
+        name: { type: String, required: true }
+      }]
+    });
+
+    var Test = db.model('TestPush', testSchema);
+
+    var update = {
+      $push: {
+        numbers: 1,
+        docs: { name: null }
+      }
+    };
+    var opts = { runValidators: true };
+    Test.update({}, update, opts, function(error) {
+      assert.ok(error.errors['numbers']);
+      assert.ok(error.errors['docs']);
+    });
+  
\ No newline at end of file + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/docs/validation.jade b/docs/validation.jade deleted file mode 100644 index 180e986bec9..00000000000 --- a/docs/validation.jade +++ /dev/null @@ -1,213 +0,0 @@ -extends layout - -block content - h2 Validation - :markdown - Before we get into the specifics of validation syntax, please keep the following rules in mind: - - - Validation is defined in the [SchemaType](./schematypes.html) - - Validation is an internal piece of [middleware](./middleware.html) - - Validation occurs when a document attempts to be [saved](./api.html#model_Model-save), after defaults have been applied - - Validators are not run on undefined values. The only exception is the [`required` validator](./api.html#schematype_SchemaType-required). - - Validation is asynchronously recursive; when you call [Model#save](./api.html#model_Model-save), sub-document validation is executed as well. If an error occurs, your [Model#save](./api.html#model_Model-save) callback receives it - - Validation supports complete customization - - h3#built-in-validators Built in validators - :markdown - Mongoose has several built in validators. - - - All [SchemaTypes](./schematypes.html) have the built in [required](./api.html#schematype_SchemaType-required) validator. - - [Numbers](./api.html#schema-number-js) have [min](./api.html#schema_number_SchemaNumber-min) and [max](./api.html#schema_number_SchemaNumber-max) validators. - - [Strings](./api.html#schema-string-js) have [enum](./api.html#schema_string_SchemaString-enum), [match](./api.html#schema_string_SchemaString-match), [maxlength](./api.html#schema_string_SchemaString-maxlength) and [minlength](./api.html#schema_string_SchemaString-minlength) validators. - - Each of the validator links above provide more information about how to enable them as well as customize their associated error messages. - h3#customized Custom validators - :markdown - If the built-in validators aren't enough, validation can be completely tailored to suite your needs. - - Custom validation is declared by passing a validation function. - You can find detailed instructions on how to do this in the - [`SchemaType#validate()` API docs](./api.html#schematype_SchemaType-validate). - - :js - var userSchema = new Schema({ - phone: { - type: String, - validate: { - validator: function(v) { - return /\d{3}-\d{3}-\d{4}/.test(v); - }, - message: '{VALUE} is not a valid phone number!' - } - } - }); - - var User = mongoose.model('user', userSchema); - - var u = new User(); - - u.phone = '555.0123'; - // Prints "ValidationError: 555.0123 is not a valid phone number!" - console.log(u.validateSync().toString()); - - u.phone = '201-555-0123'; - // Prints undefined - validation succeeded! - console.log(u.validateSync()); - h3#validation-errors Validation errors - :markdown - Errors returned after failed validation contain an `errors` object - holding the actual `ValidatorError`s. Each - [ValidatorError](./api.html#error-validation-js) has a `type`, `path`, - and `value` property providing us with a little more error handling - flexibility. - :js - var toySchema = new Schema({ - color: String, - name: String - }); - - var Toy = mongoose.model('Toy', toySchema); - - Toy.schema.path('color').validate(function (value) { - return /blue|green|white|red|orange|periwinkle/i.test(value); - }, 'Invalid color'); - - var toy = new Toy({ color: 'grease'}); - - toy.save(function (err) { - // err is our ValidationError object - // err.errors.color is a ValidatorError object - - console.log(err.errors.color.message) // prints 'Validator "Invalid color" failed for path color with value `grease`' - console.log(String(err.errors.color)) // prints 'Validator "Invalid color" failed for path color with value `grease`' - console.log(err.errors.color.type) // prints "Invalid color" - console.log(err.errors.color.path) // prints "color" - console.log(err.errors.color.value) // prints "grease" - console.log(err.name) // prints "ValidationError" - console.log(err.message) // prints "Validation failed" - }); - - :markdown - After a validation error, the document will also have the same `errors` property available: - :js - toy.errors.color.message === err.errors.color.message - - h3#validate-sync Synchronous Validation - :markdown - Validation is asynchronous by default. Mongoose evaluates each individual - path in a separate `process.nextTick()` call, which helps make sure - validation doesn't block the event loop if there are a lot of paths to - validate. However, mongoose's built-in validators are all synchronous, and - it's often more convenient to validate synchronously. - - Mongoose documents also have a `validateSync()` function, which returns - a `ValidationError` if there was an error, or falsy if there was no error. - Note that `validateSync()` **only** executes synchronous validators. - Custom asynchronous validators won't execute. - - :js - var toySchema = new Schema({ - color: String, - name: String - }); - - var Toy = mongoose.model('Toy', toySchema); - - Toy.schema.path('color').validate(function (value) { - return /blue|green|white|red|orange|periwinkle/i.test(value); - }, 'Invalid color'); - - var toy = new Toy({ color: 'grease'}); - - // `error` is a ValidationError analogous to the one from `validate()` - var error = toy.validateSync(); - - // prints 'Validator "Invalid color" failed for path color with value `grease`' - console.log(error.errors.color.message); - - h3#update-validators Update Validators - :markdown - In the above examples, you learned about document validation. Mongoose also - supports validation for `update()` and `findOneAndUpdate()` operations. - In Mongoose 4.x, update validators are off by default - you need to specify - the `runValidators` option. - :js - var toySchema = new Schema({ - color: String, - name: String - }); - - var Toy = mongoose.model('Toy', toySchema); - - Toy.schema.path('color').validate(function (value) { - return /blue|green|white|red|orange|periwinkle/i.test(value); - }, 'Invalid color'); - - Toy.update({}, { color: 'bacon' }, { runValidators: true }, function (err) { - console.log(err.errors.color.message); // prints 'Validator "Invalid color" failed for path color with value `bacon`' - }); - :markdown - There are a couple of key differences between update validators and - document validators. In the color validation function above, `this` refers - to the document being validated when using document validation. - However, when running update validators, the document being updated - may not be in the server's memory, so by default the value of `this` is - not defined. However, you can set the `context` option to 'query' to make - `this` refer to the underlying query. - - :js - Toy.schema.path('color').validate(function (value) { - this.schema; // refers to the query's schema if you set the `context` option - return /blue|green|white|red|orange|periwinkle/i.test(value); - }, 'Invalid color'); - - var options = { runValidators: true, context: 'query' }; - Toy.update({}, { color: 'bacon' }, options, function (err) { - console.log(err.errors.color.message); // prints 'Validator "Invalid color" failed for path color with value `bacon`' - }); - - :markdown - The other key difference that update validators only run on the paths - specified in the update. For instance, in the below example, because - 'name' is not specified in the update operation, update validation will - succeed. - - :js - var toySchema = new Schema({ - color: String, - name: { type: String, required: true } - }); - - var Toy = mongoose.model('Toy', toySchema); - - Toy.update({}, { color: 'blue' }, { runValidators: true }, function(err) { - // Operation succeeds despite the fact that 'name' is not specified - }); - - :markdown - When using update validators, `required` validators **only** fail when - you try to explicitly `$unset` the key. - :js - var unsetOp = { $unset: { name: 1 } }; - Toy.update({}, unsetOp, { runValidators: true }, function(err) { - // Operation fails because 'name' is required - }); - - :markdown - One final detail worth noting: update validators **only** run on `$set` - and `$unset` operations. For instance, the below update will succeed, - regardless of the value of `number`. - :js - var testSchema = new Schema({ - number: { type: Number, max: 0 }, - }); - - var Test = mongoose.model('Test', testSchema); - - Test.update({}, { $inc: { number: 1 } }, { runValidators: true }, function (err) { - // There will never be a validation error here - }); - - h3#next Next Up - :markdown - Now that we've covered `validation`, let's take a look at how you might handle advanced validation with Mongoose's [middleware](/docs/middleware.html). diff --git a/examples/aggregate/aggregate.js b/examples/aggregate/aggregate.js index dd55642f667..793c8cb2fcf 100644 --- a/examples/aggregate/aggregate.js +++ b/examples/aggregate/aggregate.js @@ -11,41 +11,41 @@ var Person = mongoose.model('Person'); // define some dummy data var data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)), - gender: "Male", - likes: ['movies', 'games', 'dogs'] - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)), - gender: "Female", - likes: ['movies', 'birds', 'cats'] - }, - { - name: 'bob', - age: 21, - birthday: new Date().setFullYear((new Date().getFullYear() - 21)), - gender: "Male", - likes: ['tv', 'games', 'rabbits'] - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)), - gender: "Female", - likes: ['books', 'cats', 'dogs'] - }, - { - name: 'alucard', - age: 1000, - birthday : new Date().setFullYear((new Date().getFullYear() - 1000)), - gender : "Male", - likes : ['glasses', 'wine', 'the night'] - } + { + name: 'bill', + age: 25, + birthday: new Date().setFullYear((new Date().getFullYear() - 25)), + gender: 'Male', + likes: ['movies', 'games', 'dogs'] + }, + { + name: 'mary', + age: 30, + birthday: new Date().setFullYear((new Date().getFullYear() - 30)), + gender: 'Female', + likes: ['movies', 'birds', 'cats'] + }, + { + name: 'bob', + age: 21, + birthday: new Date().setFullYear((new Date().getFullYear() - 21)), + gender: 'Male', + likes: ['tv', 'games', 'rabbits'] + }, + { + name: 'lilly', + age: 26, + birthday: new Date().setFullYear((new Date().getFullYear() - 26)), + gender: 'Female', + likes: ['books', 'cats', 'dogs'] + }, + { + name: 'alucard', + age: 1000, + birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), + gender: 'Male', + likes: ['glasses', 'wine', 'the night'] + } ]; @@ -65,29 +65,31 @@ mongoose.connect('mongodb://localhost/persons', function(err) { // framework, see http://docs.mongodb.org/manual/core/aggregation/ Person.aggregate( // select the fields we want to deal with - { $project : { name : 1, likes : 1 } }, + {$project: {name: 1, likes: 1}}, // unwind 'likes', which will create a document for each like - { $unwind : "$likes" }, + {$unwind: '$likes'}, // group everything by the like and then add each name with that like to // the set for the like - { $group : { - _id : { likes : "$likes" }, - likers : { $addToSet : "$name" } - } }, + {$group: { + _id: {likes: '$likes'}, + likers: {$addToSet: '$name'} + }}, function(err, result) { if (err) throw err; console.log(result); - //[ { _id: { likes: 'the night' }, likers: [ 'alucard' ] }, - //{ _id: { likes: 'wine' }, likers: [ 'alucard' ] }, - //{ _id: { likes: 'books' }, likers: [ 'lilly' ] }, - //{ _id: { likes: 'glasses' }, likers: [ 'alucard' ] }, - //{ _id: { likes: 'birds' }, likers: [ 'mary' ] }, - //{ _id: { likes: 'rabbits' }, likers: [ 'bob' ] }, - //{ _id: { likes: 'cats' }, likers: [ 'lilly', 'mary' ] }, - //{ _id: { likes: 'dogs' }, likers: [ 'lilly', 'bill' ] }, - //{ _id: { likes: 'tv' }, likers: [ 'bob' ] }, - //{ _id: { likes: 'games' }, likers: [ 'bob', 'bill' ] }, - //{ _id: { likes: 'movies' }, likers: [ 'mary', 'bill' ] } ] + /* [ + { _id: { likes: 'the night' }, likers: [ 'alucard' ] }, + { _id: { likes: 'wine' }, likers: [ 'alucard' ] }, + { _id: { likes: 'books' }, likers: [ 'lilly' ] }, + { _id: { likes: 'glasses' }, likers: [ 'alucard' ] }, + { _id: { likes: 'birds' }, likers: [ 'mary' ] }, + { _id: { likes: 'rabbits' }, likers: [ 'bob' ] }, + { _id: { likes: 'cats' }, likers: [ 'lilly', 'mary' ] }, + { _id: { likes: 'dogs' }, likers: [ 'lilly', 'bill' ] }, + { _id: { likes: 'tv' }, likers: [ 'bob' ] }, + { _id: { likes: 'games' }, likers: [ 'bob', 'bill' ] }, + { _id: { likes: 'movies' }, likers: [ 'mary', 'bill' ] } + ] */ cleanup(); }); diff --git a/examples/doc-methods.js b/examples/doc-methods.js index f7c7cad0cec..1f648b9c002 100644 --- a/examples/doc-methods.js +++ b/examples/doc-methods.js @@ -57,7 +57,7 @@ mongoose.connect(uri, function(err) { */ function example() { - Character.create({ name: 'Link', health: 100 }, function(err, link) { + Character.create({name: 'Link', health: 100}, function(err, link) { if (err) return done(err); console.log('found', link); link.attack(); // 'Link is attacking' diff --git a/examples/express/connection-sharing/modelA.js b/examples/express/connection-sharing/modelA.js index 31e9b937868..78f9ff6d30e 100644 --- a/examples/express/connection-sharing/modelA.js +++ b/examples/express/connection-sharing/modelA.js @@ -1,5 +1,5 @@ var Schema = require('../../../lib').Schema; -var mySchema = Schema({ name: String }); +var mySchema = Schema({name: String}); /* global db */ module.exports = db.model('MyModel', mySchema); diff --git a/examples/express/connection-sharing/routes.js b/examples/express/connection-sharing/routes.js index 5a70c18370f..35e4f8f6efb 100644 --- a/examples/express/connection-sharing/routes.js +++ b/examples/express/connection-sharing/routes.js @@ -13,7 +13,7 @@ exports.modelName = function(req, res) { }; exports.insert = function(req, res, next) { - model.create({ name: 'inserting ' + Date.now() }, function(err, doc) { + model.create({name: 'inserting ' + Date.now()}, function(err, doc) { if (err) return next(err); res.send(doc); }); diff --git a/examples/geospatial/geoJSONSchema.js b/examples/geospatial/geoJSONSchema.js index bd1ec5affe9..f950dea27fc 100644 --- a/examples/geospatial/geoJSONSchema.js +++ b/examples/geospatial/geoJSONSchema.js @@ -11,12 +11,12 @@ module.exports = function() { // MUST BE VANILLA var LocationObject = new Schema({ loc: { - type: { type: String }, + type: {type: String}, coordinates: [] } }); // define the index - LocationObject.index({ loc : '2dsphere' }); + LocationObject.index({loc: '2dsphere'}); mongoose.model('Location', LocationObject); }; diff --git a/examples/geospatial/geoJSONexample.js b/examples/geospatial/geoJSONexample.js index d4795952d04..8e5dd2b5081 100644 --- a/examples/geospatial/geoJSONexample.js +++ b/examples/geospatial/geoJSONexample.js @@ -1,4 +1,3 @@ - // import async to make control flow simplier var async = require('async'); @@ -12,30 +11,38 @@ var Location = mongoose.model('Location'); // define some dummy data // note: the type can be Point, LineString, or Polygon var data = [ - { loc: { type: 'Point', coordinates: [-20.0, 5.0] }}, - { loc: { type: 'Point', coordinates: [6.0, 10.0] }}, - { loc: { type: 'Point', coordinates: [34.0, -50.0] }}, - { loc: { type: 'Point', coordinates: [-100.0, 70.0] }}, - { loc: { type: 'Point', coordinates: [38.0, 38.0] }} + {loc: {type: 'Point', coordinates: [-20.0, 5.0]}}, + {loc: {type: 'Point', coordinates: [6.0, 10.0]}}, + {loc: {type: 'Point', coordinates: [34.0, -50.0]}}, + {loc: {type: 'Point', coordinates: [-100.0, 70.0]}}, + {loc: {type: 'Point', coordinates: [38.0, 38.0]}} ]; mongoose.connect('mongodb://localhost/locations', function(err) { - if (err) throw err; + if (err) { + throw err; + } Location.on('index', function(err) { - if (err) throw err; + if (err) { + throw err; + } // create all of the dummy locations async.each(data, function(item, cb) { Location.create(item, cb); }, function(err) { - if (err) throw err; + if (err) { + throw err; + } // create the location we want to search for - var coords = { type : 'Point', coordinates : [-5, 5] }; + var coords = {type: 'Point', coordinates: [-5, 5]}; // search for it - Location.find({ loc : { $near : coords }}).limit(1).exec(function(err, res) { - if (err) throw err; - console.log("Closest to %s is %s", JSON.stringify(coords), res); + Location.find({loc: {$near: coords}}).limit(1).exec(function(err, res) { + if (err) { + throw err; + } + console.log('Closest to %s is %s', JSON.stringify(coords), res); cleanup(); }); }); diff --git a/examples/geospatial/geospatial.js b/examples/geospatial/geospatial.js index 5f249210f92..3ff8c0bd519 100644 --- a/examples/geospatial/geospatial.js +++ b/examples/geospatial/geospatial.js @@ -1,4 +1,3 @@ - // import async to make control flow simplier var async = require('async'); @@ -11,51 +10,53 @@ var Person = mongoose.model('Person'); // define some dummy data var data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)), - gender: "Male", - likes: ['movies', 'games', 'dogs'], - loc: [0, 0] - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)), - gender: "Female", - likes: ['movies', 'birds', 'cats'], - loc: [1, 1] - }, - { - name: 'bob', - age: 21, - birthday: new Date().setFullYear((new Date().getFullYear() - 21)), - gender: "Male", - likes: ['tv', 'games', 'rabbits'], - loc: [3, 3] - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)), - gender: "Female", - likes: ['books', 'cats', 'dogs'], - loc: [6, 6] - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), - gender: "Male", - likes: ['glasses', 'wine', 'the night'], - loc: [10, 10] - } + { + name: 'bill', + age: 25, + birthday: new Date().setFullYear((new Date().getFullYear() - 25)), + gender: 'Male', + likes: ['movies', 'games', 'dogs'], + loc: [0, 0] + }, + { + name: 'mary', + age: 30, + birthday: new Date().setFullYear((new Date().getFullYear() - 30)), + gender: 'Female', + likes: ['movies', 'birds', 'cats'], + loc: [1, 1] + }, + { + name: 'bob', + age: 21, + birthday: new Date().setFullYear((new Date().getFullYear() - 21)), + gender: 'Male', + likes: ['tv', 'games', 'rabbits'], + loc: [3, 3] + }, + { + name: 'lilly', + age: 26, + birthday: new Date().setFullYear((new Date().getFullYear() - 26)), + gender: 'Female', + likes: ['books', 'cats', 'dogs'], + loc: [6, 6] + }, + { + name: 'alucard', + age: 1000, + birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), + gender: 'Male', + likes: ['glasses', 'wine', 'the night'], + loc: [10, 10] + } ]; mongoose.connect('mongodb://localhost/persons', function(err) { - if (err) throw err; + if (err) { + throw err; + } // create all of the dummy people async.each(data, function(item, cb) { @@ -66,21 +67,25 @@ mongoose.connect('mongodb://localhost/persons', function(err) { } // let's find the closest person to bob - Person.find({ name : 'bob' }, function(err, res) { - if (err) throw err; + Person.find({name: 'bob'}, function(err, res) { + if (err) { + throw err; + } res[0].findClosest(function(err, closest) { - if (err) throw err; + if (err) { + throw err; + } - console.log("%s is closest to %s", res[0].name, closest); + console.log('%s is closest to %s', res[0].name, closest); // we can also just query straight off of the model. For more // information about geospatial queries and indexes, see // http://docs.mongodb.org/manual/applications/geospatial-indexes/ - var coords = [7,7]; - Person.find({ loc : { $nearSphere : coords }}).limit(1).exec(function(err, res) { - console.log("Closest to %s is %s", coords, res); + var coords = [7, 7]; + Person.find({loc: {$nearSphere: coords}}).limit(1).exec(function(err, res) { + console.log('Closest to %s is %s', coords, res); cleanup(); }); }); diff --git a/examples/geospatial/person.js b/examples/geospatial/person.js index d98434efc07..e816637167b 100644 --- a/examples/geospatial/person.js +++ b/examples/geospatial/person.js @@ -1,4 +1,3 @@ - // import the necessary modules var mongoose = require('../../lib'); var Schema = mongoose.Schema; @@ -7,20 +6,20 @@ var Schema = mongoose.Schema; module.exports = function() { // define schema var PersonSchema = new Schema({ - name : String, - age : Number, - birthday : Date, + name: String, + age: Number, + birthday: Date, gender: String, likes: [String], // define the geospatial field - loc: { type : [Number], index: '2d' } + loc: {type: [Number], index: '2d'} }); // define a method to find the closest person PersonSchema.methods.findClosest = function(cb) { return this.model('Person').find({ - loc : { $nearSphere : this.loc }, - name : { $ne : this.name } + loc: {$nearSphere: this.loc}, + name: {$ne: this.name} }).limit(1).exec(cb); }; diff --git a/examples/globalschemas/gs_example.js b/examples/globalschemas/gs_example.js index 744d7bd9972..af9ff11f230 100644 --- a/examples/globalschemas/gs_example.js +++ b/examples/globalschemas/gs_example.js @@ -1,4 +1,3 @@ - var mongoose = require('../../lib'); @@ -6,29 +5,37 @@ var mongoose = require('../../lib'); require('./person.js')(); // grab the person model object -var Person = mongoose.model("Person"); +var Person = mongoose.model('Person'); // connect to a server to do a quick write / read example mongoose.connect('mongodb://localhost/persons', function(err) { - if (err) throw err; + if (err) { + throw err; + } Person.create({ - name : 'bill', - age : 25, - birthday : new Date().setFullYear((new Date().getFullYear() - 25)) + name: 'bill', + age: 25, + birthday: new Date().setFullYear((new Date().getFullYear() - 25)) }, function(err, bill) { - if (err) throw err; - console.log("People added to db: %s", bill.toString()); + if (err) { + throw err; + } + console.log('People added to db: %s', bill.toString()); Person.find({}, function(err, people) { - if (err) throw err; + if (err) { + throw err; + } people.forEach(function(person) { - console.log("People in the db: %s", person.toString()); + console.log('People in the db: %s', person.toString()); }); // make sure to clean things up after we're done - setTimeout(function() { cleanup(); }, 2000); + setTimeout(function() { + cleanup(); + }, 2000); }); }); }); diff --git a/examples/globalschemas/person.js b/examples/globalschemas/person.js index f9c9a27ab40..39ae725dece 100644 --- a/examples/globalschemas/person.js +++ b/examples/globalschemas/person.js @@ -1,4 +1,3 @@ - // import the necessary modules var mongoose = require('../../lib'); var Schema = mongoose.Schema; @@ -7,9 +6,9 @@ var Schema = mongoose.Schema; module.exports = function() { // define schema var PersonSchema = new Schema({ - name : String, - age : Number, - birthday : Date + name: String, + age: Number, + birthday: Date }); mongoose.model('Person', PersonSchema); }; diff --git a/examples/lean/lean.js b/examples/lean/lean.js index 0bf7c4f0ff5..763367f0a07 100644 --- a/examples/lean/lean.js +++ b/examples/lean/lean.js @@ -11,41 +11,41 @@ var Person = mongoose.model('Person'); // define some dummy data var data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)), - gender: "Male", - likes: ['movies', 'games', 'dogs'] - }, - { - name: 'mary', - age: 30, - birthday : new Date().setFullYear((new Date().getFullYear() - 30)), - gender: "Female", - likes: ['movies', 'birds', 'cats'] - }, - { - name: 'bob', - age: 21, - birthday: new Date().setFullYear((new Date().getFullYear() - 21)), - gender: "Male", - likes: ['tv', 'games', 'rabbits'] - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)), - gender: "Female", - likes: ['books', 'cats', 'dogs'] - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), - gender: "Male", - likes: ['glasses', 'wine', 'the night'] - } + { + name: 'bill', + age: 25, + birthday: new Date().setFullYear((new Date().getFullYear() - 25)), + gender: 'Male', + likes: ['movies', 'games', 'dogs'] + }, + { + name: 'mary', + age: 30, + birthday: new Date().setFullYear((new Date().getFullYear() - 30)), + gender: 'Female', + likes: ['movies', 'birds', 'cats'] + }, + { + name: 'bob', + age: 21, + birthday: new Date().setFullYear((new Date().getFullYear() - 21)), + gender: 'Male', + likes: ['tv', 'games', 'rabbits'] + }, + { + name: 'lilly', + age: 26, + birthday: new Date().setFullYear((new Date().getFullYear() - 26)), + gender: 'Female', + likes: ['books', 'cats', 'dogs'] + }, + { + name: 'alucard', + age: 1000, + birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), + gender: 'Male', + likes: ['glasses', 'wine', 'the night'] + } ]; @@ -66,10 +66,10 @@ mongoose.connect('mongodb://localhost/persons', function(err) { // when using .lean() the default is true, but you can explicitly set the // value by passing in a boolean value. IE. .lean(false) - var q = Person.find({ age : { $lt : 1000 }}).sort('age').limit(2).lean(); + var q = Person.find({age: {$lt: 1000}}).sort('age').limit(2).lean(); q.exec(function(err, results) { if (err) throw err; - console.log("Are the results MongooseDocuments?: %s", results[0] instanceof mongoose.Document); + console.log('Are the results MongooseDocuments?: %s', results[0] instanceof mongoose.Document); console.log(results); cleanup(); diff --git a/examples/lean/person.js b/examples/lean/person.js index 5046b1fe1d1..b0bd2ed1f30 100644 --- a/examples/lean/person.js +++ b/examples/lean/person.js @@ -1,4 +1,3 @@ - // import the necessary modules var mongoose = require('../../lib'); var Schema = mongoose.Schema; @@ -7,9 +6,9 @@ var Schema = mongoose.Schema; module.exports = function() { // define schema var PersonSchema = new Schema({ - name : String, - age : Number, - birthday : Date, + name: String, + age: Number, + birthday: Date, gender: String, likes: [String] }); diff --git a/examples/mapreduce/mapreduce.js b/examples/mapreduce/mapreduce.js index 53983faaa9c..6d67fbf4e76 100644 --- a/examples/mapreduce/mapreduce.js +++ b/examples/mapreduce/mapreduce.js @@ -10,36 +10,36 @@ var Person = mongoose.model('Person'); // define some dummy data var data = [ - { - name: 'bill', - age: 25, - birthday: new Date().setFullYear((new Date().getFullYear() - 25)), - gender : "Male" - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)), - gender: "Female" - }, - { - name: 'bob', - age: 21, - birthday : new Date().setFullYear((new Date().getFullYear() - 21)), - gender: "Male" - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)), - gender: "Female" - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), - gender : "Male" - } + { + name: 'bill', + age: 25, + birthday: new Date().setFullYear((new Date().getFullYear() - 25)), + gender: 'Male' + }, + { + name: 'mary', + age: 30, + birthday: new Date().setFullYear((new Date().getFullYear() - 30)), + gender: 'Female' + }, + { + name: 'bob', + age: 21, + birthday: new Date().setFullYear((new Date().getFullYear() - 21)), + gender: 'Male' + }, + { + name: 'lilly', + age: 26, + birthday: new Date().setFullYear((new Date().getFullYear() - 26)), + gender: 'Female' + }, + { + name: 'alucard', + age: 1000, + birthday: new Date().setFullYear((new Date().getFullYear() - 1000)), + gender: 'Male' + } ]; @@ -86,7 +86,7 @@ mongoose.connect('mongodb://localhost/persons', function(err) { // returned, but can also be stored in a new collection // see: http://mongoosejs.com/docs/api.html#model_Model.mapReduce Person.mapReduce(o, function(err, results, stats) { - console.log("map reduce took %d ms", stats.processtime); + console.log('map reduce took %d ms', stats.processtime); console.log(results); cleanup(); }); diff --git a/examples/mapreduce/person.js b/examples/mapreduce/person.js index 6d476ec1a01..9e2f084236e 100644 --- a/examples/mapreduce/person.js +++ b/examples/mapreduce/person.js @@ -7,9 +7,9 @@ var Schema = mongoose.Schema; module.exports = function() { // define schema var PersonSchema = new Schema({ - name : String, - age : Number, - birthday : Date, + name: String, + age: Number, + birthday: Date, gender: String }); mongoose.model('Person', PersonSchema); diff --git a/examples/population/population-across-three-collections.js b/examples/population/population-across-three-collections.js index 5edd01a68f7..fe6de72dbee 100644 --- a/examples/population/population-across-three-collections.js +++ b/examples/population/population-across-three-collections.js @@ -42,7 +42,6 @@ var BlogPost = mongoose.model('BlogPost', blogpost); */ mongoose.connection.on('open', function() { - /** * Generate data */ @@ -99,7 +98,7 @@ mongoose.connection.on('open', function() { */ BlogPost - .find({ tags: 'fun' }) + .find({tags: 'fun'}) .lean() .populate('author') .exec(function(err, docs) { @@ -112,13 +111,13 @@ mongoose.connection.on('open', function() { var opts = { path: 'author.friends', select: 'name', - options: { limit: 2 } + options: {limit: 2} }; BlogPost.populate(docs, opts, function(err, docs) { assert.ifError(err); console.log('populated'); - var s = require('util').inspect(docs, { depth: null }); + var s = require('util').inspect(docs, {depth: null, colors: true}); console.log(s); done(); }); diff --git a/examples/population/population-basic.js b/examples/population/population-basic.js index 8b3c1cfe58d..252cc5374d0 100644 --- a/examples/population/population-basic.js +++ b/examples/population/population-basic.js @@ -77,7 +77,7 @@ function createData() { function example() { Game - .findOne({ name: /^Legend of Zelda/ }) + .findOne({name: /^Legend of Zelda/}) .populate('consoles') .exec(function(err, ocinara) { if (err) return done(err); diff --git a/examples/population/population-of-existing-doc.js b/examples/population/population-of-existing-doc.js index 3abbf0e7559..c7eadfee79f 100644 --- a/examples/population/population-of-existing-doc.js +++ b/examples/population/population-of-existing-doc.js @@ -77,7 +77,7 @@ function createData() { function example() { Game - .findOne({ name: /^Legend of Zelda/ }) + .findOne({name: /^Legend of Zelda/}) .exec(function(err, ocinara) { if (err) return done(err); diff --git a/examples/population/population-of-multiple-existing-docs.js b/examples/population/population-of-multiple-existing-docs.js index 5e1ecfae74b..61b4e85de6f 100644 --- a/examples/population/population-of-multiple-existing-docs.js +++ b/examples/population/population-of-multiple-existing-docs.js @@ -96,7 +96,7 @@ function example() { console.log('found %d games', games.length); - var options = { path: 'consoles', select: 'name released -_id' }; + var options = {path: 'consoles', select: 'name released -_id'}; Game.populate(games, options, function(err, games) { if (err) return done(err); diff --git a/examples/population/population-options.js b/examples/population/population-options.js index 2ca0ce99758..59cfd1e8702 100644 --- a/examples/population/population-options.js +++ b/examples/population/population-options.js @@ -104,9 +104,9 @@ function example() { .find({}) .populate({ path: 'consoles', - match: { manufacturer: 'Nintendo' }, + match: {manufacturer: 'Nintendo'}, select: 'name', - options: { comment: 'population' } + options: {comment: 'population'} }) .exec(function(err, games) { if (err) return done(err); diff --git a/examples/population/population-plain-objects.js b/examples/population/population-plain-objects.js index f8b729c60a4..ba849dc38be 100644 --- a/examples/population/population-plain-objects.js +++ b/examples/population/population-plain-objects.js @@ -79,7 +79,7 @@ function createData() { function example() { Game - .findOne({ name: /^Legend of Zelda/ }) + .findOne({name: /^Legend of Zelda/}) .populate('consoles') .lean() // just return plain objects, not documents wrapped by mongoose .exec(function(err, ocinara) { diff --git a/examples/promises/promise.js b/examples/promises/promise.js index ed188c20962..3f17206a343 100644 --- a/examples/promises/promise.js +++ b/examples/promises/promise.js @@ -1,4 +1,3 @@ - // import async to make control flow simplier var async = require('async'); @@ -11,72 +10,79 @@ var Person = mongoose.model('Person'); // define some dummy data var data = [ - { - name: 'bill', - age: 25, - birthday : new Date().setFullYear((new Date().getFullYear() - 25)) - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)) - }, - { - name: 'bob', - age: 21, - birthday : new Date().setFullYear((new Date().getFullYear() - 21)) - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)) - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)) - } + { + name: 'bill', + age: 25, + birthday: new Date().setFullYear((new Date().getFullYear() - 25)) + }, + { + name: 'mary', + age: 30, + birthday: new Date().setFullYear((new Date().getFullYear() - 30)) + }, + { + name: 'bob', + age: 21, + birthday: new Date().setFullYear((new Date().getFullYear() - 21)) + }, + { + name: 'lilly', + age: 26, + birthday: new Date().setFullYear((new Date().getFullYear() - 26)) + }, + { + name: 'alucard', + age: 1000, + birthday: new Date().setFullYear((new Date().getFullYear() - 1000)) + } ]; mongoose.connect('mongodb://localhost/persons', function(err) { - if (err) throw err; + if (err) { + throw err; + } // create all of the dummy people async.each(data, function(item, cb) { Person.create(item, cb); }, function(err) { if (err) { - // handle error + // handle error } - // create a promise (get one from the query builder) - var prom = Person.find({age : { $lt : 1000 }}).exec(); + // create a promise (get one from the query builder) + var prom = Person.find({age: {$lt: 1000}}).exec(); - // add a callback on the promise. This will be called on both error and - // complete - prom.addBack(function() { console.log("completed"); }); + // add a callback on the promise. This will be called on both error and + // complete + prom.addBack(function() { + console.log('completed'); + }); - // add a callback that is only called on complete (success) events - prom.addCallback(function() { console.log("Successful Completion!"); }); + // add a callback that is only called on complete (success) events + prom.addCallback(function() { + console.log('Successful Completion!'); + }); - // add a callback that is only called on err (rejected) events - prom.addErrback(function() { console.log("Fail Boat"); }); + // add a callback that is only called on err (rejected) events + prom.addErrback(function() { + console.log('Fail Boat'); + }); - // you can chain things just like in the promise/A+ spec - // note: each then() is returning a new promise, so the above methods - // that we defined will all fire after the initial promise is fulfilled + // you can chain things just like in the promise/A+ spec + // note: each then() is returning a new promise, so the above methods + // that we defined will all fire after the initial promise is fulfilled prom.then(function(people) { - - // just getting the stuff for the next query + // just getting the stuff for the next query var ids = people.map(function(p) { return p._id; }); - // return the next promise - return Person.find({ _id : { $nin : ids }}).exec(); + // return the next promise + return Person.find({_id: {$nin: ids}}).exec(); }).then(function(oldest) { - console.log("Oldest person is: %s", oldest); + console.log('Oldest person is: %s', oldest); }).then(cleanup); }); }); diff --git a/examples/querybuilder/querybuilder.js b/examples/querybuilder/querybuilder.js index c455d7042ac..6f8645b5eac 100644 --- a/examples/querybuilder/querybuilder.js +++ b/examples/querybuilder/querybuilder.js @@ -11,31 +11,31 @@ var Person = mongoose.model('Person'); // define some dummy data var data = [ - { - name: 'bill', - age: 25, - birthday : new Date().setFullYear((new Date().getFullYear() - 25)) - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)) - }, - { - name: 'bob', - age: 21, - birthday : new Date().setFullYear((new Date().getFullYear() - 21)) - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)) - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)) - } + { + name: 'bill', + age: 25, + birthday: new Date().setFullYear((new Date().getFullYear() - 25)) + }, + { + name: 'mary', + age: 30, + birthday: new Date().setFullYear((new Date().getFullYear() - 30)) + }, + { + name: 'bob', + age: 21, + birthday: new Date().setFullYear((new Date().getFullYear() - 21)) + }, + { + name: 'lilly', + age: 26, + birthday: new Date().setFullYear((new Date().getFullYear() - 26)) + }, + { + name: 'alucard', + age: 1000, + birthday: new Date().setFullYear((new Date().getFullYear() - 1000)) + } ]; @@ -50,7 +50,7 @@ mongoose.connect('mongodb://localhost/persons', function(err) { // when querying data, instead of providing a callback, you can instead // leave that off and get a query object returned - var query = Person.find({ age : { $lt : 1000 }}); + var query = Person.find({age: {$lt: 1000}}); // this allows you to continue applying modifiers to it query.sort('birthday'); @@ -69,7 +69,6 @@ mongoose.connect('mongodb://localhost/persons', function(err) { cleanup(); }); - }); }); diff --git a/examples/replicasets/replica-sets.js b/examples/replicasets/replica-sets.js index 2ff86213d52..9ebac1b6b3b 100644 --- a/examples/replicasets/replica-sets.js +++ b/examples/replicasets/replica-sets.js @@ -11,38 +11,38 @@ var Person = mongoose.model('Person'); // define some dummy data var data = [ - { - name: 'bill', - age: 25, - birthday : new Date().setFullYear((new Date().getFullYear() - 25)) - }, - { - name: 'mary', - age: 30, - birthday: new Date().setFullYear((new Date().getFullYear() - 30)) - }, - { - name: 'bob', - age: 21, - birthday : new Date().setFullYear((new Date().getFullYear() - 21)) - }, - { - name: 'lilly', - age: 26, - birthday: new Date().setFullYear((new Date().getFullYear() - 26)) - }, - { - name: 'alucard', - age: 1000, - birthday: new Date().setFullYear((new Date().getFullYear() - 1000)) - } + { + name: 'bill', + age: 25, + birthday: new Date().setFullYear((new Date().getFullYear() - 25)) + }, + { + name: 'mary', + age: 30, + birthday: new Date().setFullYear((new Date().getFullYear() - 30)) + }, + { + name: 'bob', + age: 21, + birthday: new Date().setFullYear((new Date().getFullYear() - 21)) + }, + { + name: 'lilly', + age: 26, + birthday: new Date().setFullYear((new Date().getFullYear() - 26)) + }, + { + name: 'alucard', + age: 1000, + birthday: new Date().setFullYear((new Date().getFullYear() - 1000)) + } ]; // to connect to a replica set, pass in the comma delimited uri and optionally // any connection options such as the rs_name. var opts = { - replSet : { rs_name : "rs0" } + replSet: {rs_name: 'rs0'} }; mongoose.connect('mongodb://localhost:27018/persons,localhost:27019,localhost:27020', opts, function(err) { if (err) throw err; @@ -56,10 +56,10 @@ mongoose.connect('mongodb://localhost:27018/persons,localhost:27019,localhost:27 } // create and delete some data - var prom = Person.find({age : { $lt : 1000 }}).exec(); + var prom = Person.find({age: {$lt: 1000}}).exec(); prom.then(function(people) { - console.log("young people: %s", people); + console.log('young people: %s', people); }).then(cleanup); }); }); diff --git a/examples/schema/schema.js b/examples/schema/schema.js index bb906dfe3e9..5bc99ae7635 100644 --- a/examples/schema/schema.js +++ b/examples/schema/schema.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -43,7 +42,7 @@ var BlogPost = new Schema({ var Person = new Schema({ name: { first: String, - last : String + last: String }, email: { type: String, @@ -65,7 +64,7 @@ BlogPost.path('date') return new Date(); }) .set(function(v) { - return v == 'now' ? new Date() : v; + return v === 'now' ? new Date() : v; }); /** @@ -87,7 +86,7 @@ BlogPost.methods.findCreator = function(callback) { }; BlogPost.statics.findByTitle = function(title, callback) { - return this.find({ title: title }, callback); + return this.find({title: title}, callback); }; BlogPost.methods.expressiveQuery = function(creator, date, callback) { diff --git a/examples/schema/storing-schemas-as-json/index.js b/examples/schema/storing-schemas-as-json/index.js index e93cabcbc7d..284b478d4c8 100644 --- a/examples/schema/storing-schemas-as-json/index.js +++ b/examples/schema/storing-schemas-as-json/index.js @@ -16,11 +16,11 @@ var TimeSignature = mongoose.model('TimeSignatures', timeSignatureSchema); var threeFour = new TimeSignature({ count: 3, unit: 4, - description: "3/4", + description: '3/4', additive: false, created: new Date, - links: ["http://en.wikipedia.org/wiki/Time_signature"], - user_id: "518d31a0ef32bbfa853a9814" + links: ['http://en.wikipedia.org/wiki/Time_signature'], + user_id: '518d31a0ef32bbfa853a9814' }); // print its description diff --git a/examples/statics/person.js b/examples/statics/person.js index c0dee737039..a93b8c66cdb 100644 --- a/examples/statics/person.js +++ b/examples/statics/person.js @@ -1,4 +1,3 @@ - // import the necessary modules var mongoose = require('../../lib'); var Schema = mongoose.Schema; @@ -14,7 +13,7 @@ module.exports = function() { // define a static PersonSchema.statics.findPersonByName = function(name, cb) { - this.find({ name : new RegExp(name, 'i') }, cb); + this.find({name: new RegExp(name, 'i')}, cb); }; mongoose.model('Person', PersonSchema); diff --git a/examples/statics/statics.js b/examples/statics/statics.js index a318ca44280..610b2aa4d3c 100644 --- a/examples/statics/statics.js +++ b/examples/statics/statics.js @@ -1,4 +1,3 @@ - var mongoose = require('../../lib'); @@ -6,31 +5,32 @@ var mongoose = require('../../lib'); require('./person.js')(); // grab the person model object -var Person = mongoose.model("Person"); +var Person = mongoose.model('Person'); // connect to a server to do a quick write / read example mongoose.connect('mongodb://localhost/persons', function(err) { - if (err) throw err; - - Person.create( - { - name : 'bill', - age : 25, - birthday : new Date().setFullYear((new Date().getFullYear() - 25)) - }, - function(err, bill) { - if (err) throw err; - console.log("People added to db: %s", bill.toString()); - - // using the static - Person.findPersonByName('bill', function(err, result) { - if (err) throw err; - - console.log(result); - cleanup(); - }); - } + if (err) { + throw err; + } + + Person.create({name: 'bill', age: 25, birthday: new Date().setFullYear((new Date().getFullYear() - 25))}, + function(err, bill) { + if (err) { + throw err; + } + console.log('People added to db: %s', bill.toString()); + + // using the static + Person.findPersonByName('bill', function(err, result) { + if (err) { + throw err; + } + + console.log(result); + cleanup(); + }); + } ); }); diff --git a/format_deps.js b/format_deps.js index 39e5e74322e..206feb77b3d 100644 --- a/format_deps.js +++ b/format_deps.js @@ -1,5 +1,5 @@ var p = require('./package.json'); -var _ = require('underscore'); +var _ = require('lodash'); var result = _.map(p.browserDependencies, function(v, k) { return k + '@' + v; diff --git a/index.html b/index.html index eeb3de3af24..37be0705e32 100644 --- a/index.html +++ b/index.html @@ -1,19 +1,30 @@ -Mongoose ODM v3.6.17Fork me on GitHub

Elegant MongoDB object modeling for Node.js

  • Version 3.6.17

Let's face it, writing MongoDB validation, casting and business logic boilerplate is a drag. That's why we wrote Mongoose.

var mongoose = require('mongoose');
+Mongoose ODM v4.11.9Fork me on GitHub

Elegant MongoDB object modeling for Node.js

  • Version 4.11.9

Let's face it, writing MongoDB validation, casting and business logic boilerplate is a drag. That's why we wrote Mongoose.

var mongoose = require('mongoose');
 mongoose.connect('mongodb://localhost/test');
 
 var Cat = mongoose.model('Cat', { name: String });
 
 var kitty = new Cat({ name: 'Zildjian' });
 kitty.save(function (err) {
-  if (err) // ...
-  console.log('meow');
-});

Mongoose provides a straight-forward, schema-based solution to modeling your application data and includes built-in type casting, validation, query building, business logic hooks and more, out of the box.

Getting Started

Support

Changelog

Unstable

Production - View More

  • Storify
  • LearnBoost
  • GeekList
  • McDonalds
  • ShoeJitsu
  • Bozuko
+KeenAsync.ready(function(){ + // Configure a client instance + var client = new KeenAsync({ + projectId: '59aad9cbc9e77c0001ce1b32', + writeKey: '4B38B0046086885E425D368BFAEAD8FD0D4F2DC2FA2F936FDE058D79508AEFAD9886BC020B96520823BB9C8241D9D9BCFDC0EF52E6033BD89D06E4B24FC13AE955896BF443406269A84DD009CEB5862DCEC944874DB2107FD648DA91ADC1E6DE' + }); + + client.recordEvent('pageView', { + host: window.location.host, + pathname: window.location.pathname, + hash: window.location.hash + }); +}); \ No newline at end of file diff --git a/index.jade b/index.jade index e106075976f..bae6f56650d 100644 --- a/index.jade +++ b/index.jade @@ -25,7 +25,7 @@ html(lang='en') li a(href="docs/guide.html") Read the Docs li - a(href="http://plugins.mongoosejs.com") Discover Plugins + a(href="http://plugins.mongoosejs.io") Discover Plugins #follow ul li @@ -44,14 +44,18 @@ html(lang='en') #example :js var mongoose = require('mongoose'); - mongoose.connect('mongodb://localhost/test'); + mongoose.connect('mongodb://localhost/test', { useMongoClient: true }); + mongoose.Promise = global.Promise; var Cat = mongoose.model('Cat', { name: String }); var kitty = new Cat({ name: 'Zildjian' }); kitty.save(function (err) { - if (err) // ... - console.log('meow'); + if (err) { + console.log(err); + } else { + console.log('meow'); + } }); p.what Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box. #getstarted @@ -96,6 +100,9 @@ html(lang='en') li a(href="https://prezi.com/pa1ub5unrapt/trello-architecture-2013/") img(src="http://upload.wikimedia.org/wikipedia/en/3/3e/Trello_Logo.png", alt="Trello") + li + a(href="http://waffle.io", alt="Waffle.io") + img(src="http://i.imgur.com/nqzTvft.png", alt="Waffle.io") li a(href="http://storify.com/", alt="Storify") img(src="docs/images/apps/h_storify.jpg", alt="Storify") @@ -109,12 +116,16 @@ html(lang='en') a(href="http://www.elevenjames.com") img(src="http://i.imgur.com/vx8KH9Z.jpg", alt="Eleven James") li - a(href="https://bozuko.com/", alt="Bozuko") - img(src="docs/images/apps/h_bozuko.jpg", alt="Bozuko") + a(href="http://www.datafox.com/", alt="DataFox") + img(src="http://i.imgur.com/VxHJfAJ.png", alt="DataFox") + li + a(href="https://sixplus.com") + img(src="https://s3.amazonaws.com/bookalokal-assets/sixplus/sixplus-logo-horizontal-dark.png", alt="SixPlus") li - a(href="http://yourquestions.mcdonalds.ca/") - img#mcds(src="docs/images/apps/h_mcds.png", alt="McDonalds") + a(href="http://www.softbox.com.br/en/produtos/detalhe/cat/dashbox/") + img(src="http://www.softbox.com.br/public/upload/categoriasprodutos/logo/logo-dashbox.png", alt="Dashbox") + p#footer Licensed under MIT. Copyright 2011 LearnBoost. script. document.body.className = 'load'; - include docs/includes/googleanalytics + include docs/includes/keen diff --git a/karma.sauce.conf.js b/karma.sauce.conf.js deleted file mode 100644 index b1380f81ec0..00000000000 --- a/karma.sauce.conf.js +++ /dev/null @@ -1,96 +0,0 @@ -module.exports = function(config) { - var customLaunchers = { - sl_chrome_35: { - base: 'SauceLabs', - browserName: 'chrome', - version: '35' - }, - sl_safari_6: { - base: 'SauceLabs', - browserName: 'safari', - platform: 'OS X 10.8', - version: '6' - }, - /*sl_safari_7: { - base: 'SauceLabs', - browserName: 'safari', - platform: 'OS X 10.9', - version: '7' - },*/ - sl_ie_9: { - base: 'SauceLabs', - browserName: 'internet explorer', - platform: 'Windows 7', - version: '9' - }, - sl_ie_10: { - base: 'SauceLabs', - browserName: 'internet explorer', - platform: 'Windows 7', - version: '10' - }, - sl_ie_11: { - base: 'SauceLabs', - browserName: 'internet explorer', - platform: 'Windows 7', - version: '11' - }, - sl_android_43: { - base: 'SauceLabs', - browserName: 'android', - platform: 'Linux', - version: '4.3', - deviceName: 'Android', - 'device-orientation': 'portrait' - } - }; - - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha', 'chai'], - - // list of files / patterns to load in the browser - files: [ - './bin/mongoose.js', - './test/browser/*.js' - ], - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['dots', 'saucelabs'], - - // web server port - port: 9876, - - // enable / disable colors in the output (reporters and logs) - colors: true, - - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - // Use these custom launchers for starting browsers on Sauce - customLaunchers: customLaunchers, - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: Object.keys(customLaunchers), - - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: true, - - sauceLabs: { - testName: 'Mongoose ' + Date.now() - } - }); -}; diff --git a/lib/ES6Promise.js b/lib/ES6Promise.js index 295adb111e7..13371ac3aca 100644 --- a/lib/ES6Promise.js +++ b/lib/ES6Promise.js @@ -1,5 +1,3 @@ -/* eslint no-unused-vars: 1 */ - /** * ES6 Promise wrapper constructor. * @@ -17,8 +15,8 @@ * @api public */ -function ES6Promise(fn) { - throw 'Can\'t use ES6 promise with mpromise style constructor'; +function ES6Promise() { + throw new Error('Can\'t use ES6 promise with mpromise style constructor'); } ES6Promise.use = function(Promise) { diff --git a/lib/aggregate.js b/lib/aggregate.js index 3686941a3f2..a390176e936 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -1,13 +1,13 @@ -/* eslint no-unused-vars: 1 */ - /*! * Module dependencies */ -var util = require('util'); -var utils = require('./utils'); +var AggregationCursor = require('./cursor/AggregationCursor'); var PromiseProvider = require('./promise_provider'); var Query = require('./query'); +var eachAsync = require('./services/cursor/eachAsync'); +var util = require('util'); +var utils = require('./utils'); var read = Query.prototype.read; /** @@ -44,9 +44,9 @@ var read = Query.prototype.read; function Aggregate() { this._pipeline = []; this._model = undefined; - this.options = undefined; + this.options = {}; - if (1 === arguments.length && util.isArray(arguments[0])) { + if (arguments.length === 1 && util.isArray(arguments[0])) { this.append.apply(this, arguments[0]); } else { this.append.apply(this, arguments); @@ -63,6 +63,16 @@ function Aggregate() { Aggregate.prototype.model = function(model) { this._model = model; + if (model.schema != null) { + if (this.options.readPreference == null && + model.schema.options.read != null) { + this.options.readPreference = model.schema.options.read; + } + if (this.options.collation == null && + model.schema.options.collation != null) { + this.options.collation = model.schema.options.collation; + } + } return this; }; @@ -83,10 +93,12 @@ Aggregate.prototype.model = function(model) { */ Aggregate.prototype.append = function() { - var args = utils.args(arguments); + var args = (arguments.length === 1 && util.isArray(arguments[0])) + ? arguments[0] + : utils.args(arguments); if (!args.every(isOperator)) { - throw new Error("Arguments must be aggregate pipeline operators"); + throw new Error('Arguments must be aggregate pipeline operators'); } this._pipeline = this._pipeline.concat(args); @@ -94,6 +106,41 @@ Aggregate.prototype.append = function() { return this; }; +/** + * Appends a new $addFields operator to this aggregate pipeline. + * Requires MongoDB v3.4+ to work + * + * ####Examples: + * + * // adding new fields based on existing fields + * aggregate.addFields({ + * newField: '$b.nested' + * , plusTen: { $add: ['$val', 10]} + * , sub: { + * name: '$a' + * } + * }) + * + * // etc + * aggregate.addFields({ salary_k: { $divide: [ "$salary", 1000 ] } }); + * + * @param {Object} arg field specification + * @see $addFields https://docs.mongodb.com/manual/reference/operator/aggregation/addFields/ + * @return {Aggregate} + * @api public + */ +Aggregate.prototype.addFields = function(arg) { + var fields = {}; + if (typeof arg === 'object' && !util.isArray(arg)) { + Object.keys(arg).forEach(function(field) { + fields[field] = arg[field]; + }); + } else { + throw new Error('Invalid addFields() argument. Must be an object'); + } + return this.append({$addFields: fields}); +}; + /** * Appends a new $project operator to this aggregate pipeline. * @@ -129,22 +176,26 @@ Aggregate.prototype.append = function() { Aggregate.prototype.project = function(arg) { var fields = {}; - if ('object' === typeof arg && !util.isArray(arg)) { + if (typeof arg === 'object' && !util.isArray(arg)) { Object.keys(arg).forEach(function(field) { fields[field] = arg[field]; }); - } else if (1 === arguments.length && 'string' === typeof arg) { + } else if (arguments.length === 1 && typeof arg === 'string') { arg.split(/\s+/).forEach(function(field) { - if (!field) return; - var include = '-' == field[0] ? 0 : 1; - if (include === 0) field = field.substring(1); + if (!field) { + return; + } + var include = field[0] === '-' ? 0 : 1; + if (include === 0) { + field = field.substring(1); + } fields[field] = include; }); } else { - throw new Error("Invalid project() argument. Must be string or object"); + throw new Error('Invalid project() argument. Must be string or object'); } - return this.append({ $project: fields }); + return this.append({$project: fields}); }; /** @@ -167,7 +218,7 @@ Aggregate.prototype.project = function(arg) { * * ####Examples: * - * aggregate.match({ department: { $in: [ "sales", "engineering" } } }); + * aggregate.match({ department: { $in: [ "sales", "engineering" ] } }); * * @see $match http://docs.mongodb.org/manual/reference/aggregation/match/ * @method match @@ -272,9 +323,91 @@ Aggregate.prototype.near = function(arg) { Aggregate.prototype.unwind = function() { var args = utils.args(arguments); - return this.append.apply(this, args.map(function(arg) { - return { $unwind: (arg && arg.charAt(0) === '$') ? arg : '$' + arg }; - })); + var res = []; + for (var i = 0; i < args.length; ++i) { + var arg = args[i]; + if (arg && typeof arg === 'object') { + res.push({ $unwind: arg }); + } else if (typeof arg === 'string') { + res.push({ + $unwind: (arg && arg.charAt(0) === '$') ? arg : '$' + arg + }); + } else { + throw new Error('Invalid arg "' + arg + '" to unwind(), ' + + 'must be string or object'); + } + } + + return this.append.apply(this, res); +}; + +/** + * Appends new custom $lookup operator(s) to this aggregate pipeline. + * + * ####Examples: + * + * aggregate.lookup({ from: 'users', localField: 'userId', foreignField: '_id', as: 'users' }); + * + * @see $lookup https://docs.mongodb.org/manual/reference/operator/aggregation/lookup/#pipe._S_lookup + * @param {Object} options to $lookup as described in the above link + * @return {Aggregate} + * @api public + */ + +Aggregate.prototype.lookup = function(options) { + return this.append({$lookup: options}); +}; + +/** + * Appends new custom $graphLookup operator(s) to this aggregate pipeline, performing a recursive search on a collection. + * + * Note that graphLookup can only consume at most 100MB of memory, and does not allow disk use even if `{ allowDiskUse: true }` is specified. + * + * #### Examples: + * // Suppose we have a collection of courses, where a document might look like `{ _id: 0, name: 'Calculus', prerequisite: 'Trigonometry'}` and `{ _id: 0, name: 'Trigonometry', prerequisite: 'Algebra' }` + * aggregate.graphLookup({ from: 'courses', startWith: '$prerequisite', connectFromField: 'prerequisite', connectToField: 'name', as: 'prerequisites', maxDepth: 3 }) // this will recursively search the 'courses' collection up to 3 prerequisites + * + * @see $graphLookup https://docs.mongodb.com/manual/reference/operator/aggregation/graphLookup/#pipe._S_graphLookup + * @param {Object} options to $graphLookup as described in the above link + * @return {Aggregate} + * @api public + */ + +Aggregate.prototype.graphLookup = function(options) { + var cloneOptions = {}; + if (options) { + if (!utils.isObject(options)) { + throw new TypeError('Invalid graphLookup() argument. Must be an object.'); + } + + utils.mergeClone(cloneOptions, options); + var startWith = cloneOptions.startWith; + + if (startWith && typeof startWith === 'string') { + cloneOptions.startWith = cloneOptions.startWith.charAt(0) === '$' ? + cloneOptions.startWith : + '$' + cloneOptions.startWith; + } + + } + return this.append({ $graphLookup: cloneOptions }); +}; + +/** + * Appepnds new custom $sample operator(s) to this aggregate pipeline. + * + * ####Examples: + * + * aggregate.sample(3); // Add a pipeline that picks 3 random documents + * + * @see $sample https://docs.mongodb.org/manual/reference/operator/aggregation/sample/#pipe._S_sample + * @param {Number} size number of random documents to pick + * @return {Aggregate} + * @api public + */ + +Aggregate.prototype.sample = function(size) { + return this.append({$sample: {size: size}}); }; /** @@ -301,23 +434,32 @@ Aggregate.prototype.sort = function(arg) { var sort = {}; - if ('Object' === arg.constructor.name) { + if (arg.constructor.name === 'Object') { var desc = ['desc', 'descending', -1]; Object.keys(arg).forEach(function(field) { + // If sorting by text score, skip coercing into 1/-1 + if (arg[field] instanceof Object && arg[field].$meta) { + sort[field] = arg[field]; + return; + } sort[field] = desc.indexOf(arg[field]) === -1 ? 1 : -1; }); - } else if (1 === arguments.length && 'string' == typeof arg) { + } else if (arguments.length === 1 && typeof arg === 'string') { arg.split(/\s+/).forEach(function(field) { - if (!field) return; - var ascend = '-' == field[0] ? -1 : 1; - if (ascend === -1) field = field.substring(1); + if (!field) { + return; + } + var ascend = field[0] === '-' ? -1 : 1; + if (ascend === -1) { + field = field.substring(1); + } sort[field] = ascend; }); } else { throw new TypeError('Invalid sort() argument. Must be a string or object.'); } - return this.append({ $sort: sort }); + return this.append({$sort: sort}); }; /** @@ -333,9 +475,11 @@ Aggregate.prototype.sort = function(arg) { * @see driver http://mongodb.github.com/node-mongodb-native/driver-articles/anintroductionto1_1and2_2.html#read-preferences */ -Aggregate.prototype.read = function(pref) { - if (!this.options) this.options = {}; - read.apply(this, arguments); +Aggregate.prototype.read = function(pref, tags) { + if (!this.options) { + this.options = {}; + } + read.call(this, pref, tags); return this; }; @@ -366,22 +510,22 @@ Aggregate.prototype.explain = function(callback) { prepareDiscriminatorPipeline(_this); _this._model - .collection - .aggregate(_this._pipeline, _this.options || {}) - .explain(function(error, result) { - if (error) { - if (callback) { - callback(error); + .collection + .aggregate(_this._pipeline, _this.options || {}) + .explain(function(error, result) { + if (error) { + if (callback) { + callback(error); + } + reject(error); + return; } - reject(error); - return; - } - if (callback) { - callback(null, result); - } - resolve(result); - }); + if (callback) { + callback(null, result); + } + resolve(result); + }); }); }; @@ -398,11 +542,34 @@ Aggregate.prototype.explain = function(callback) { */ Aggregate.prototype.allowDiskUse = function(value) { - if (!this.options) this.options = {}; this.options.allowDiskUse = value; return this; }; +/** + * Lets you set arbitrary options, for middleware or plugins. + * + * ####Example: + * + * var agg = Model.aggregate(..).option({ allowDiskUse: true }); // Set the `allowDiskUse` option + * agg.options; // `{ allowDiskUse: true }` + * + * @param {Object} options keys to merge into current options + * @param [options.maxTimeMS] number limits the time this aggregation will run, see [MongoDB docs on `maxTimeMS`](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) + * @param [options.allowDiskUse] boolean if true, the MongoDB server will use the hard drive to store data during this aggregation + * @param [options.collation] object see [`Aggregate.prototype.collation()`](./docs/api.html#aggregate_Aggregate-collation) + * @see mongodb http://docs.mongodb.org/manual/reference/command/aggregate/ + * @return {Aggregate} this + * @api public + */ + +Aggregate.prototype.option = function(value) { + for (var key in value) { + this.options[key] = value[key]; + } + return this; +}; + /** * Sets the cursor option option for the aggregation query (ignored for < 2.6.0). * Note the different syntax below: .exec() returns a cursor object, and no callback @@ -410,21 +577,103 @@ Aggregate.prototype.allowDiskUse = function(value) { * * ####Example: * - * var cursor = Model.aggregate(..).cursor({ batchSize: 1000 }).exec(); + * var cursor = Model.aggregate(..).cursor({ batchSize: 1000, useMongooseAggCursor: true }).exec(); * cursor.each(function(error, doc) { * // use doc * }); * - * @param {Object} options set the cursor batch size + * @param {Object} options + * @param {Number} options.batchSize set the cursor batch size + * @param {Boolean} [options.useMongooseAggCursor] use experimental mongoose-specific aggregation cursor (for `eachAsync()` and other query cursor semantics) * @see mongodb http://mongodb.github.io/node-mongodb-native/2.0/api/AggregationCursor.html */ Aggregate.prototype.cursor = function(options) { - if (!this.options) this.options = {}; - this.options.cursor = options; + if (!this.options) { + this.options = {}; + } + this.options.cursor = options || {}; + return this; +}; + +/** + * Adds a [cursor flag](http://mongodb.github.io/node-mongodb-native/2.2/api/Cursor.html#addCursorFlag) + * + * ####Example: + * + * Model.aggregate(..).addCursorFlag('noCursorTimeout', true).exec(); + * + * @param {String} flag + * @param {Boolean} value + * @see mongodb http://mongodb.github.io/node-mongodb-native/2.2/api/Cursor.html#addCursorFlag + */ + +Aggregate.prototype.addCursorFlag = function(flag, value) { + if (!this.options) { + this.options = {}; + } + this.options[flag] = value; return this; }; +/** + * Adds a collation + * + * ####Example: + * + * Model.aggregate(..).collation({ locale: 'en_US', strength: 1 }).exec(); + * + * @param {Object} collation options + * @see mongodb http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#aggregate + */ + +Aggregate.prototype.collation = function(collation) { + if (!this.options) { + this.options = {}; + } + this.options.collation = collation; + return this; +}; + +/** + * Combines multiple aggregation pipelines. + * + * ####Example: + * Model.aggregate(...) + * .facet({ + * books: [{ groupBy: '$author' }], + * price: [{ $bucketAuto: { groupBy: '$price', buckets: 2 } }] + * }) + * .exec(); + * + * // Output: { books: [...], price: [{...}, {...}] } + * + * @param {Object} facet options + * @return {Aggregate} this + * @see $facet https://docs.mongodb.com/v3.4/reference/operator/aggregation/facet/ + * @api public + */ + +Aggregate.prototype.facet = function(options) { + return this.append({$facet: options}); +}; + +/** + * Returns the current pipeline + * + * ####Example: + * + * MyModel.aggregate().match({ test: 1 }).pipeline(); // [{ $match: { test: 1 } }] + * + * @return {Array} + * @api public + */ + + +Aggregate.prototype.pipeline = function() { + return this._pipeline; +}; + /** * Executes the aggregate pipeline on the currently bound Model. * @@ -444,39 +693,46 @@ Aggregate.prototype.cursor = function(options) { Aggregate.prototype.exec = function(callback) { if (!this._model) { - throw new Error("Aggregate not bound to any Model"); + throw new Error('Aggregate not bound to any Model'); } var _this = this; + var model = this._model; var Promise = PromiseProvider.get(); - - if (this.options && this.options.cursor) { - if (this.options.cursor.async) { - return new Promise.ES6(function(resolve, reject) { - if (!_this._model.collection.buffer) { + var options = utils.clone(this.options || {}); + var pipeline = this._pipeline; + var collection = this._model.collection; + + if (options && options.cursor) { + if (options.cursor.async) { + delete options.cursor.async; + return new Promise.ES6(function(resolve) { + if (!collection.buffer) { process.nextTick(function() { - var cursor = _this._model.collection. - aggregate(_this._pipeline, _this.options || {}); - resolve(cursor); - callback && callback(cursor); - }); - return; - } else { - _this._model.collection.emitter.once('queue', function() { - var cursor = _this._model.collection. - aggregate(_this._pipeline, _this.options || {}); + var cursor = collection.aggregate(pipeline, options); + decorateCursor(cursor); resolve(cursor); callback && callback(null, cursor); }); + return; } + collection.emitter.once('queue', function() { + var cursor = collection.aggregate(pipeline, options); + decorateCursor(cursor); + resolve(cursor); + callback && callback(null, cursor); + }); }); - } else { - return this._model.collection. - aggregate(this._pipeline, this.options || {}); + } else if (options.cursor.useMongooseAggCursor) { + delete options.cursor.useMongooseAggCursor; + return new AggregationCursor(this); } + var cursor = collection.aggregate(pipeline, options); + decorateCursor(cursor); + return cursor; } return new Promise.ES6(function(resolve, reject) { - if (!_this._pipeline.length) { + if (!pipeline.length) { var err = new Error('Aggregate has empty pipeline'); if (callback) { callback(err); @@ -487,25 +743,70 @@ Aggregate.prototype.exec = function(callback) { prepareDiscriminatorPipeline(_this); - _this._model - .collection - .aggregate(_this._pipeline, _this.options || {}, function(error, result) { - if (error) { + model.hooks.execPre('aggregate', _this, function(error) { + if (error) { + var _opts = { error: error }; + return model.hooks.execPost('aggregate', _this, [null], _opts, function(error) { if (callback) { callback(error); } reject(error); - return; - } + }); + } - if (callback) { - callback(null, result); - } - resolve(result); + collection.aggregate(pipeline, options, function(error, result) { + var _opts = { error: error }; + model.hooks.execPost('aggregate', _this, [result], _opts, function(error, result) { + if (error) { + if (callback) { + callback(error); + } + reject(error); + return; + } + + if (callback) { + callback(null, result); + } + resolve(result); + }); }); + }); }); }; +/*! + * Add `eachAsync()` to aggregation cursors + */ + +function decorateCursor(cursor) { + cursor.eachAsync = function(fn, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } + opts = opts || {}; + + return eachAsync(function(cb) { return cursor.next(cb); }, fn, opts, callback); + }; +} + +/** + * Provides promise for aggregate. + * + * ####Example: + * + * Model.aggregate(..).then(successCallback, errorCallback); + * + * @see Promise #promise_Promise + * @param {Function} [resolve] successCallback + * @param {Function} [reject] errorCallback + * @return {Promise} + */ +Aggregate.prototype.then = function(resolve, reject) { + return this.exec().then(resolve, reject); +}; + /*! * Helpers */ @@ -521,15 +822,16 @@ Aggregate.prototype.exec = function(callback) { function isOperator(obj) { var k; - if ('object' !== typeof obj) { + if (typeof obj !== 'object') { return false; } k = Object.keys(obj); - return 1 === k.length && k.some(function(key) { - return '$' === key[0]; - }); + return k.length === 1 && k + .some(function(key) { + return key[0] === '$'; + }); } /*! @@ -540,6 +842,8 @@ function isOperator(obj) { * @param {Aggregate} aggregate Aggregate to prepare */ +Aggregate._prepareDiscriminatorPipeline = prepareDiscriminatorPipeline; + function prepareDiscriminatorPipeline(aggregate) { var schema = aggregate._model.schema, discriminatorMapping = schema && schema.discriminatorMapping; @@ -552,24 +856,22 @@ function prepareDiscriminatorPipeline(aggregate) { // If the first pipeline stage is a match and it doesn't specify a `__t` // key, add the discriminator key to it. This allows for potential // aggregation query optimizations not to be disturbed by this feature. - if (originalPipeline[0] && originalPipeline[0].$match && - !originalPipeline[0].$match[discriminatorKey]) { + if (originalPipeline[0] && originalPipeline[0].$match && !originalPipeline[0].$match[discriminatorKey]) { originalPipeline[0].$match[discriminatorKey] = discriminatorValue; // `originalPipeline` is a ref, so there's no need for // aggregate._pipeline = originalPipeline } else if (originalPipeline[0] && originalPipeline[0].$geoNear) { originalPipeline[0].$geoNear.query = - originalPipeline[0].$geoNear.query || {}; + originalPipeline[0].$geoNear.query || {}; originalPipeline[0].$geoNear.query[discriminatorKey] = discriminatorValue; } else { var match = {}; match[discriminatorKey] = discriminatorValue; - aggregate._pipeline = [{ $match: match }].concat(originalPipeline); + aggregate._pipeline.unshift({ $match: match }); } } } - /*! * Exports */ diff --git a/lib/browser.js b/lib/browser.js index d135f66a6a4..53605e516b3 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -1,3 +1,35 @@ +/* eslint-env browser */ + +var DocumentProvider = require('./document_provider.js'); +var PromiseProvider = require('./promise_provider'); + +DocumentProvider.setBrowser(true); + +/** + * The Mongoose [Promise](#promise_Promise) constructor. + * + * @method Promise + * @api public + */ + +Object.defineProperty(exports, 'Promise', { + get: function() { + return PromiseProvider.get(); + }, + set: function(lib) { + PromiseProvider.set(lib); + } +}); + +/** + * Storage layer for mongoose promises + * + * @method PromiseProvider + * @api public + */ + +exports.PromiseProvider = PromiseProvider; + /** * The [MongooseError](#error_MongooseError) constructor. * @@ -85,7 +117,7 @@ exports.utils = require('./utils.js'); * @method Document * @api public */ -exports.Document = require('./document_provider.js')(); +exports.Document = DocumentProvider(); /*! * Module exports. diff --git a/lib/browserDocument.js b/lib/browserDocument.js index ed23af3c40c..9d03294832e 100644 --- a/lib/browserDocument.js +++ b/lib/browserDocument.js @@ -2,14 +2,18 @@ * Module dependencies. */ -var NodeJSDocument = require('./document'), - EventEmitter = require('events').EventEmitter, - MongooseError = require('./error'), - Schema = require('./schema'), - ObjectId = require('./types/objectid'), - utils = require('./utils'), - ValidationError = MongooseError.ValidationError, - InternalCache = require('./internal'); +var NodeJSDocument = require('./document'); +var EventEmitter = require('events').EventEmitter; +var MongooseError = require('./error'); +var Schema = require('./schema'); +var ObjectId = require('./types/objectid'); +var utils = require('./utils'); +var ValidationError = MongooseError.ValidationError; +var InternalCache = require('./internal'); +var PromiseProvider = require('./promise_provider'); +var VersionError = require('./error').VersionError; + +var Embedded; /** * Document constructor. @@ -24,11 +28,12 @@ var NodeJSDocument = require('./document'), */ function Document(obj, schema, fields, skipId, skipInit) { - if ( !(this instanceof Document) ) - return new Document( obj, schema, fields, skipId, skipInit ); + if (!(this instanceof Document)) { + return new Document(obj, schema, fields, skipId, skipInit); + } - if (utils.isObject(schema) && !(schema instanceof Schema)) { + if (utils.isObject(schema) && !schema.instanceOfSchema) { schema = new Schema(schema); } @@ -36,15 +41,15 @@ function Document(obj, schema, fields, skipId, skipInit) { schema = this.schema || schema; // Generate ObjectId if it is missing, but it requires a scheme - if ( !this.schema && schema.options._id ) { + if (!this.schema && schema.options._id) { obj = obj || {}; - if ( obj._id === undefined ) { + if (obj._id === undefined) { obj._id = new ObjectId(); } } - if ( !schema ) { + if (!schema) { throw new MongooseError.MissingSchemaError(); } @@ -55,9 +60,7 @@ function Document(obj, schema, fields, skipId, skipInit) { this.isNew = true; this.errors = undefined; - //var schema = this.schema; - - if ('boolean' === typeof fields) { + if (typeof fields === 'boolean') { this.$__.strictMode = fields; fields = undefined; } else { @@ -73,29 +76,215 @@ function Document(obj, schema, fields, skipId, skipInit) { this.$__.emitter.setMaxListeners(0); this._doc = this.$__buildDoc(obj, fields, skipId); - if ( !skipInit && obj ) { - this.init( obj ); + if (!skipInit && obj) { + this.init(obj); } this.$__registerHooksFromSchema(); // apply methods - for ( var m in schema.methods ) { - this[ m ] = schema.methods[ m ]; + for (var m in schema.methods) { + this[m] = schema.methods[m]; } // apply statics - for ( var s in schema.statics ) { - this[ s ] = schema.statics[ s ]; + for (var s in schema.statics) { + this[s] = schema.statics[s]; } } /*! * Inherit from the NodeJS document */ + Document.prototype = Object.create(NodeJSDocument.prototype); Document.prototype.constructor = Document; +/*! + * Browser doc exposes the event emitter API + */ + +Document.$emitter = new EventEmitter(); + +utils.each( + ['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners', + 'removeAllListeners', 'addListener'], + function(emitterFn) { + Document[emitterFn] = function() { + return Document.$emitter[emitterFn].apply(Document.$emitter, arguments); + }; + }); + +/*! + * Executes methods queued from the Schema definition + * + * @api private + * @method $__registerHooksFromSchema + * @deprecated + * @memberOf Document + */ + +Document.prototype.$__registerHooksFromSchema = function() { + Embedded = Embedded || require('./types/embedded'); + var Promise = PromiseProvider.get(); + + var _this = this; + var q = _this.schema && _this.schema.callQueue; + var toWrapEl; + var len; + var i; + var j; + var pointCut; + var keys; + if (!q.length) { + return _this; + } + + // we are only interested in 'pre' hooks, and group by point-cut + var toWrap = { post: [] }; + var pair; + for (i = 0; i < q.length; ++i) { + pair = q[i]; + if (pair[0] !== 'pre' && pair[0] !== 'post' && pair[0] !== 'on') { + _this[pair[0]].apply(_this, pair[1]); + continue; + } + var args = [].slice.call(pair[1]); + pointCut = pair[0] === 'on' ? 'post' : args[0]; + if (!(pointCut in toWrap)) { + toWrap[pointCut] = {post: [], pre: []}; + } + if (pair[0] === 'post') { + toWrap[pointCut].post.push(args); + } else if (pair[0] === 'on') { + toWrap[pointCut].push(args); + } else { + toWrap[pointCut].pre.push(args); + } + } + + // 'post' hooks are simpler + len = toWrap.post.length; + toWrap.post.forEach(function(args) { + _this.on.apply(_this, args); + }); + delete toWrap.post; + + // 'init' should be synchronous on subdocuments + if (toWrap.init && _this instanceof Embedded) { + if (toWrap.init.pre) { + toWrap.init.pre.forEach(function(args) { + _this.$pre.apply(_this, args); + }); + } + if (toWrap.init.post) { + toWrap.init.post.forEach(function(args) { + _this.$post.apply(_this, args); + }); + } + delete toWrap.init; + } else if (toWrap.set) { + // Set hooks also need to be sync re: gh-3479 + if (toWrap.set.pre) { + toWrap.set.pre.forEach(function(args) { + _this.$pre.apply(_this, args); + }); + } + if (toWrap.set.post) { + toWrap.set.post.forEach(function(args) { + _this.$post.apply(_this, args); + }); + } + delete toWrap.set; + } + + keys = Object.keys(toWrap); + len = keys.length; + for (i = 0; i < len; ++i) { + pointCut = keys[i]; + // this is so we can wrap everything into a promise; + var newName = ('$__original_' + pointCut); + if (!_this[pointCut]) { + continue; + } + if (_this[pointCut].$isWrapped) { + continue; + } + _this[newName] = _this[pointCut]; + _this[pointCut] = (function(_newName) { + return function wrappedPointCut() { + var args = [].slice.call(arguments); + var lastArg = args.pop(); + var fn; + var originalError = new Error(); + var $results; + if (lastArg && typeof lastArg !== 'function') { + args.push(lastArg); + } else { + fn = lastArg; + } + + var promise = new Promise.ES6(function(resolve, reject) { + args.push(function(error) { + if (error) { + // gh-2633: since VersionError is very generic, take the + // stack trace of the original save() function call rather + // than the async trace + if (error instanceof VersionError) { + error.stack = originalError.stack; + } + _this.$__handleReject(error); + reject(error); + return; + } + + // There may be multiple results and promise libs other than + // mpromise don't support passing multiple values to `resolve()` + $results = Array.prototype.slice.call(arguments, 1); + resolve.apply(promise, $results); + }); + + _this[_newName].apply(_this, args); + }); + if (fn) { + if (_this.constructor.$wrapCallback) { + fn = _this.constructor.$wrapCallback(fn); + } + return promise.then( + function() { + process.nextTick(function() { + fn.apply(null, [null].concat($results)); + }); + }, + function(error) { + process.nextTick(function() { + fn(error); + }); + }); + } + return promise; + }; + })(newName); + _this[pointCut].$isWrapped = true; + + toWrapEl = toWrap[pointCut]; + var _len = toWrapEl.pre.length; + args; + for (j = 0; j < _len; ++j) { + args = toWrapEl.pre[j]; + args[0] = newName; + _this.$pre.apply(_this, args); + } + + _len = toWrapEl.post.length; + for (j = 0; j < _len; ++j) { + args = toWrapEl.post[j]; + args[0] = newName; + _this.$post.apply(_this, args); + } + } + return _this; +}; /*! * Module exports. diff --git a/lib/cast.js b/lib/cast.js index d91d675530a..339c031f14d 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -2,57 +2,68 @@ * Module dependencies. */ -var utils = require('./utils'); +var StrictModeError = require('./error/strict'); var Types = require('./schema/index'); +var util = require('util'); +var utils = require('./utils'); + +var ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon']; /** - * Handles internal casting for queries + * Handles internal casting for query filters. * * @param {Schema} schema - * @param {Object obj Object to cast + * @param {Object} obj Object to cast + * @param {Object} options the query options + * @param {Query} context passed to setters * @api private */ +module.exports = function cast(schema, obj, options, context) { + if (Array.isArray(obj)) { + throw new Error('Query filter must be an object, got an array ', util.inspect(obj)); + } -var cast = module.exports = function(schema, obj) { - var paths = Object.keys(obj), - i = paths.length, - any$conditionals, - schematype, - nested, - path, - type, - val; + // bson 1.x has the unfortunate tendency to remove filters that have a top-level + // `_bsontype` property. Should remove this when we upgrade to bson 4.x. See gh-8222 + if (obj.hasOwnProperty('_bsontype')) { + delete obj._bsontype; + } + + var paths = Object.keys(obj); + var i = paths.length; + var _keys; + var any$conditionals; + var schematype; + var nested; + var path; + var type; + var val; while (i--) { path = paths[i]; val = obj[path]; - if ('$or' === path || '$nor' === path || '$and' === path) { + if (path === '$or' || path === '$nor' || path === '$and') { var k = val.length; while (k--) { - val[k] = cast(schema, val[k]); + val[k] = cast(schema, val[k], options, context); } - } else if (path === '$where') { type = typeof val; - if ('string' !== type && 'function' !== type) { - throw new Error("Must have a string or function for $where"); + if (type !== 'string' && type !== 'function') { + throw new Error('Must have a string or function for $where'); } - if ('function' === type) { + if (type === 'function') { obj[path] = val.toString(); } continue; - } else if (path === '$elemMatch') { - - val = cast(schema, val); - + val = cast(schema, val, options, context); } else { - if (!schema) { // no casting for Mixed types continue; @@ -72,7 +83,9 @@ var cast = module.exports = function(schema, obj) { while (j--) { pathFirstHalf = split.slice(0, j).join('.'); schematype = schema.path(pathFirstHalf); - if (schematype) break; + if (schematype) { + break; + } } // If a substring of the input path resolves to an actual real path... @@ -82,7 +95,7 @@ var cast = module.exports = function(schema, obj) { remainingConds = {}; pathLastHalf = split.slice(j).join('.'); remainingConds[pathLastHalf] = val; - obj[path] = cast(schematype.caster.schema, remainingConds)[pathLastHalf]; + obj[path] = cast(schematype.caster.schema, remainingConds, options, context)[pathLastHalf]; } else { obj[path] = val; } @@ -93,84 +106,122 @@ var cast = module.exports = function(schema, obj) { // handle geo schemas that use object notation // { loc: { long: Number, lat: Number } - var geo = val.$near ? '$near' : - val.$nearSphere ? '$nearSphere' : - val.$within ? '$within' : - val.$geoIntersects ? '$geoIntersects' : ''; - - if (!geo) { - continue; + var geo = ''; + if (val.$near) { + geo = '$near'; + } else if (val.$nearSphere) { + geo = '$nearSphere'; + } else if (val.$within) { + geo = '$within'; + } else if (val.$geoIntersects) { + geo = '$geoIntersects'; + } else if (val.$geoWithin) { + geo = '$geoWithin'; } - var numbertype = new Types.Number('__QueryCasting__'); - var value = val[geo]; + if (geo) { + var numbertype = new Types.Number('__QueryCasting__'); + var value = val[geo]; - if (val.$maxDistance) { - val.$maxDistance = numbertype.castForQuery(val.$maxDistance); - } - - if ('$within' == geo) { - var withinType = value.$center - || value.$centerSphere - || value.$box - || value.$polygon; - - if (!withinType) { - throw new Error('Bad $within paramater: ' + JSON.stringify(val)); + if (val.$maxDistance != null) { + val.$maxDistance = numbertype.castForQueryWrapper({ + val: val.$maxDistance, + context: context + }); + } + if (val.$minDistance != null) { + val.$minDistance = numbertype.castForQueryWrapper({ + val: val.$minDistance, + context: context + }); } - value = withinType; - - } else if ('$near' == geo && - 'string' == typeof value.type && Array.isArray(value.coordinates)) { - // geojson; cast the coordinates - value = value.coordinates; + if (geo === '$within') { + var withinType = value.$center + || value.$centerSphere + || value.$box + || value.$polygon; - } else if (('$near' == geo || '$nearSphere' == geo || '$geoIntersects' == geo) && - value.$geometry && 'string' == typeof value.$geometry.type && - Array.isArray(value.$geometry.coordinates)) { - // geojson; cast the coordinates - value = value.$geometry.coordinates; - } + if (!withinType) { + throw new Error('Bad $within paramater: ' + JSON.stringify(val)); + } - (function _cast(val) { - if (Array.isArray(val)) { - val.forEach(function(item, i) { - if (Array.isArray(item) || utils.isObject(item)) { - return _cast(item); + value = withinType; + } else if (geo === '$near' && + typeof value.type === 'string' && Array.isArray(value.coordinates)) { + // geojson; cast the coordinates + value = value.coordinates; + } else if ((geo === '$near' || geo === '$nearSphere' || geo === '$geoIntersects') && + value.$geometry && typeof value.$geometry.type === 'string' && + Array.isArray(value.$geometry.coordinates)) { + if (value.$maxDistance != null) { + value.$maxDistance = numbertype.castForQueryWrapper({ + val: value.$maxDistance, + context: context + }); + } + if (value.$minDistance != null) { + value.$minDistance = numbertype.castForQueryWrapper({ + val: value.$minDistance, + context: context + }); + } + if (utils.isMongooseObject(value.$geometry)) { + value.$geometry = value.$geometry.toObject({ + transform: false, + virtuals: false + }); + } + value = value.$geometry.coordinates; + } else if (geo === '$geoWithin') { + if (value.$geometry) { + if (utils.isMongooseObject(value.$geometry)) { + value.$geometry = value.$geometry.toObject({ virtuals: false }); } - val[i] = numbertype.castForQuery(item); - }); - } else { - var nearKeys = Object.keys(val); - var nearLen = nearKeys.length; - while (nearLen--) { - var nkey = nearKeys[nearLen]; - var item = val[nkey]; - if (Array.isArray(item) || utils.isObject(item)) { - _cast(item); - val[nkey] = item; - } else { - val[nkey] = numbertype.castForQuery(item); + var geoWithinType = value.$geometry.type; + if (ALLOWED_GEOWITHIN_GEOJSON_TYPES.indexOf(geoWithinType) === -1) { + throw new Error('Invalid geoJSON type for $geoWithin "' + + geoWithinType + '", must be "Polygon" or "MultiPolygon"'); + } + value = value.$geometry.coordinates; + } else { + value = value.$box || value.$polygon || value.$center || + value.$centerSphere; + if (utils.isMongooseObject(value)) { + value = value.toObject({ virtuals: false }); } } } - })(value); + + _cast(value, numbertype, context); + continue; + } } + if (options && options.upsert && options.strict && !schema.nested[path]) { + if (options.strict === 'throw') { + throw new StrictModeError(path); + } + throw new StrictModeError(path, 'Path "' + path + '" is not in ' + + 'schema, strict mode is `true`, and upsert is `true`.'); + } else if (options && options.strictQuery === 'throw') { + throw new StrictModeError(path, 'Path "' + path + '" is not in ' + + 'schema and strictQuery is true.'); + } } else if (val === null || val === undefined) { obj[path] = null; continue; - } else if ('Object' === val.constructor.name) { - + } else if (val.constructor.name === 'Object') { any$conditionals = Object.keys(val).some(function(k) { return k.charAt(0) === '$' && k !== '$id' && k !== '$ref'; }); if (!any$conditionals) { - obj[path] = schematype.castForQuery(val); + obj[path] = schematype.castForQueryWrapper({ + val: val, + context: context + }); } else { - var ks = Object.keys(val), $cond; @@ -180,32 +231,78 @@ var cast = module.exports = function(schema, obj) { $cond = ks[k]; nested = val[$cond]; - if ('$exists' === $cond) { - if ('boolean' !== typeof nested) { - throw new Error("$exists parameter must be Boolean"); - } - continue; - } - - if ('$type' === $cond) { - if ('number' !== typeof nested) { - throw new Error("$type parameter must be Number"); + if ($cond === '$not') { + if (nested && schematype && !schematype.caster) { + _keys = Object.keys(nested); + if (_keys.length && _keys[0].charAt(0) === '$') { + for (var key in nested) { + nested[key] = schematype.castForQueryWrapper({ + $conditional: key, + val: nested[key], + context: context + }); + } + } else { + val[$cond] = schematype.castForQueryWrapper({ + $conditional: $cond, + val: nested, + context: context + }); + } + continue; } - continue; - } - - if ('$not' === $cond) { - cast(schema, nested); + cast(schematype.caster ? schematype.caster.schema : schema, nested, options, context); } else { - val[$cond] = schematype.castForQuery($cond, nested); + val[$cond] = schematype.castForQueryWrapper({ + $conditional: $cond, + val: nested, + context: context + }); } } } + } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) { + var casted = []; + for (var valIndex = 0; valIndex < val.length; valIndex++) { + casted.push(schematype.castForQueryWrapper({ + val: val[valIndex], + context: context + })); + } + + obj[path] = { $in: casted }; } else { - obj[path] = schematype.castForQuery(val); + obj[path] = schematype.castForQueryWrapper({ + val: val, + context: context + }); } } } return obj; }; + +function _cast(val, numbertype, context) { + if (Array.isArray(val)) { + val.forEach(function(item, i) { + if (Array.isArray(item) || utils.isObject(item)) { + return _cast(item, numbertype, context); + } + val[i] = numbertype.castForQueryWrapper({ val: item, context: context }); + }); + } else { + var nearKeys = Object.keys(val); + var nearLen = nearKeys.length; + while (nearLen--) { + var nkey = nearKeys[nearLen]; + var item = val[nkey]; + if (Array.isArray(item) || utils.isObject(item)) { + _cast(item, numbertype, context); + val[nkey] = item; + } else { + val[nkey] = numbertype.castForQuery({ val: item, context: context }); + } + } + } +} diff --git a/lib/collection.js b/lib/collection.js index b2d165dabb3..7bc5145ecc8 100644 --- a/lib/collection.js +++ b/lib/collection.js @@ -1,4 +1,3 @@ - /*! * Module dependencies. */ @@ -18,15 +17,19 @@ var STATES = require('./connectionstate'); */ function Collection(name, conn, opts) { - if (undefined === opts) opts = {}; - if (undefined === opts.capped) opts.capped = {}; + if (opts === void 0) { + opts = {}; + } + if (opts.capped === void 0) { + opts.capped = {}; + } opts.bufferCommands = undefined === opts.bufferCommands - ? true - : opts.bufferCommands; + ? true + : opts.bufferCommands; - if ('number' == typeof opts.capped) { - opts.capped = { size: opts.capped }; + if (typeof opts.capped === 'number') { + opts.capped = {size: opts.capped}; } this.opts = opts; @@ -37,7 +40,7 @@ function Collection(name, conn, opts) { this.buffer = this.opts.bufferCommands; this.emitter = new EventEmitter(); - if (STATES.connected == this.conn.readyState) { + if (STATES.connected === this.conn.readyState) { this.onOpen(); } } @@ -76,9 +79,11 @@ Collection.prototype.conn; */ Collection.prototype.onOpen = function() { - var self = this; this.buffer = false; - self.doQueue(); + var _this = this; + setImmediate(function() { + _this.doQueue(); + }); }; /** @@ -87,8 +92,8 @@ Collection.prototype.onOpen = function() { * @api private */ -Collection.prototype.onClose = function() { - if (this.opts.bufferCommands) { +Collection.prototype.onClose = function(force) { + if (this.opts.bufferCommands && !force) { this.buffer = true; } }; @@ -115,7 +120,11 @@ Collection.prototype.addQueue = function(name, args) { Collection.prototype.doQueue = function() { for (var i = 0, l = this.queue.length; i < l; i++) { - this[this.queue[i][0]].apply(this, this.queue[i][1]); + if (typeof this.queue[i][0] === 'function') { + this.queue[i][0].apply(this, this.queue[i][1]); + } else { + this[this.queue[i][0]].apply(this, this.queue[i][1]); + } } this.queue = []; var _this = this; @@ -133,6 +142,14 @@ Collection.prototype.ensureIndex = function() { throw new Error('Collection#ensureIndex unimplemented by driver'); }; +/** + * Abstract method that drivers must implement. + */ + +Collection.prototype.createIndex = function() { + throw new Error('Collection#ensureIndex unimplemented by driver'); +}; + /** * Abstract method that drivers must implement. */ diff --git a/lib/connection.js b/lib/connection.js index b10293deeb0..67c6204a723 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -2,14 +2,17 @@ * Module dependencies. */ -var utils = require('./utils'), - EventEmitter = require('events').EventEmitter, - driver = global.MONGOOSE_DRIVER_PATH || './drivers/node-mongodb-native', - Schema = require('./schema'), - Collection = require(driver + '/collection'), - STATES = require('./connectionstate'), - MongooseError = require('./error'), - muri = require('muri'); +var utils = require('./utils'); +var EventEmitter = require('events').EventEmitter; +var driver = global.MONGOOSE_DRIVER_PATH || './drivers/node-mongodb-native'; +var Schema = require('./schema'); +var Collection = require(driver + '/collection'); +var STATES = require('./connectionstate'); +var MongooseError = require('./error'); +var muri = require('muri'); +var PromiseProvider = require('./promise_provider'); +var mongodb = require('mongodb'); +var util = require('util'); /*! * Protocol prefix regexp. @@ -44,7 +47,8 @@ var authMechanismsWhichDontRequirePassword = [ * @event `close`: Emitted after we `disconnected` and `onClose` executed on all of this connections models. * @event `reconnected`: Emitted after we `connected` and subsequently `disconnected`, followed by successfully another successfull connection. * @event `error`: Emitted when an error occurs on this connection. - * @event `fullsetup`: Emitted in a replica-set scenario, when all nodes specified in the connection string are connected. + * @event `fullsetup`: Emitted in a replica-set scenario, when primary and at least one seconaries specified in the connection string are connected. + * @event `all`: Emitted in a replica-set scenario, when all nodes specified in the connection string are connected. * @api public */ @@ -62,6 +66,7 @@ function Connection(base) { this.name = null; this.options = null; this.otherDbs = []; + this.states = STATES; this._readyState = STATES.disconnected; this._closeCalled = false; this._hasOpened = false; @@ -93,7 +98,9 @@ Connection.prototype.__proto__ = EventEmitter.prototype; */ Object.defineProperty(Connection.prototype, 'readyState', { - get: function() { return this._readyState; }, + get: function() { + return this._readyState; + }, set: function(val) { if (!(val in STATES)) { throw new Error('Invalid connection state: ' + val); @@ -106,8 +113,9 @@ Object.defineProperty(Connection.prototype, 'readyState', { this.otherDbs[i].readyState = val; } - if (STATES.connected === val) + if (STATES.connected === val) { this._hasOpened = true; + } this.emit(STATES[val]); } @@ -133,46 +141,21 @@ Connection.prototype.db; /** * A hash of the global options that are associated with this connection * - * @property global + * @property config */ Connection.prototype.config; -/** - * Opens the connection to MongoDB. - * - * `options` is a hash with the following possible properties: - * - * config - passed to the connection config instance - * db - passed to the connection db instance - * server - passed to the connection server instance(s) - * replset - passed to the connection ReplSet instance - * user - username for authentication - * pass - password for authentication - * auth - options for authentication (see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate) - * - * ####Notes: - * - * Mongoose forces the db option `forceServerObjectId` false and cannot be overridden. - * Mongoose defaults the server `auto_reconnect` options to true which can be overridden. - * See the node-mongodb-native driver instance for options that it understands. - * - * _Options passed take precedence over options included in connection strings._ - * - * @param {String} connection_string mongodb://uri or the host to which you are connecting - * @param {String} [database] database name - * @param {Number} [port] database port - * @param {Object} [options] options - * @param {Function} [callback] - * @see node-mongodb-native https://github.com/mongodb/node-mongodb-native - * @see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate - * @api public +/*! + * ignore */ -Connection.prototype.open = function(host, database, port, options, callback) { +Connection.prototype._handleOpenArgs = function(host, database, port, options, callback) { + var err; + var parsed; - if ('string' === typeof database) { + if (typeof database === 'string') { switch (arguments.length) { case 2: port = 27017; @@ -180,21 +163,26 @@ Connection.prototype.open = function(host, database, port, options, callback) { case 3: switch (typeof port) { case 'function': - callback = port, port = 27017; + callback = port; + port = 27017; break; case 'object': - options = port, port = 27017; + options = port; + port = 27017; break; } break; case 4: - if ('function' === typeof options) - callback = options, options = {}; + if (typeof options === 'function') { + callback = options; + options = {}; + } } } else { switch (typeof database) { case 'function': - callback = database, database = undefined; + callback = database; + database = undefined; break; case 'object': options = database; @@ -209,9 +197,9 @@ Connection.prototype.open = function(host, database, port, options, callback) { try { parsed = muri(host); - } catch (err) { - this.error(err, callback); - return this; + } catch (error) { + this.error(error, callback); + throw error; } database = parsed.db; @@ -223,107 +211,258 @@ Connection.prototype.open = function(host, database, port, options, callback) { // make sure we can open if (STATES.disconnected !== this.readyState) { - var err = new Error('Trying to open unclosed connection.'); + err = new Error('Trying to open unclosed connection.'); err.state = this.readyState; this.error(err, callback); - return this; + throw err; } if (!host) { - this.error(new Error('Missing hostname.'), callback); - return this; + err = new Error('Missing hostname.'); + this.error(err, callback); + throw err; } if (!database) { - this.error(new Error('Missing database name.'), callback); - return this; + err = new Error('Missing database name.'); + this.error(err, callback); + throw err; } // authentication if (this.optionsProvideAuthenticationData(options)) { this.user = options.user; this.pass = options.pass; - } else if (parsed && parsed.auth) { this.user = parsed.auth.user; this.pass = parsed.auth.pass; - // Check hostname for user/pass + // Check hostname for user/pass } else if (/@/.test(host) && /:/.test(host.split('@')[0])) { host = host.split('@'); + if (host.length > 2) { + err = new Error('Username and password must be URI encoded if they ' + + 'contain "@", see http://bit.ly/2nRYRyq'); + throw err; + } var auth = host.shift().split(':'); + if (auth.length > 2) { + err = new Error('Username and password must be URI encoded if they ' + + 'contain ":", see http://bit.ly/2nRYRyq'); + throw err; + } host = host.pop(); this.user = auth[0]; this.pass = auth[1]; - } else { this.user = this.pass = undefined; } // global configuration options if (options && options.config) { - if (options.config.autoIndex === false) { - this.config.autoIndex = false; - } - else { - this.config.autoIndex = true; - } - + this.config.autoIndex = options.config.autoIndex !== false; } this.name = database; this.host = host; this.port = port; - this._open(callback); - return this; + return callback; }; /** - * Opens the connection to a replica set. - * - * ####Example: - * - * var db = mongoose.createConnection(); - * db.openSet("mongodb://user:pwd@localhost:27020/testing,mongodb://example.com:27020,mongodb://localhost:27019"); - * - * The database name and/or auth need only be included in one URI. - * The `options` is a hash which is passed to the internal driver connection object. + * Opens the connection to MongoDB. * - * Valid `options` + * `options` is a hash with the following possible properties: * + * config - passed to the connection config instance * db - passed to the connection db instance * server - passed to the connection server instance(s) - * replset - passed to the connection ReplSetServer instance + * replset - passed to the connection ReplSet instance * user - username for authentication * pass - password for authentication * auth - options for authentication (see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate) - * mongos - Boolean - if true, enables High Availability support for mongos - * - * _Options passed take precedence over options included in connection strings._ * * ####Notes: * - * _If connecting to multiple mongos servers, set the `mongos` option to true._ - * - * conn.open('mongodb://mongosA:27501,mongosB:27501', { mongos: true }, cb); - * * Mongoose forces the db option `forceServerObjectId` false and cannot be overridden. * Mongoose defaults the server `auto_reconnect` options to true which can be overridden. * See the node-mongodb-native driver instance for options that it understands. * * _Options passed take precedence over options included in connection strings._ * - * @param {String} uris comma-separated mongodb:// `URI`s - * @param {String} [database] database name if not included in `uris` - * @param {Object} [options] passed to the internal driver + * @param {String} connection_string mongodb://uri or the host to which you are connecting + * @param {String} [database] database name + * @param {Number} [port] database port + * @param {Object} [options] options * @param {Function} [callback] * @see node-mongodb-native https://github.com/mongodb/node-mongodb-native * @see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate * @api public */ -Connection.prototype.openSet = function(uris, database, options, callback) { +Connection.prototype.open = util.deprecate(function() { + var Promise = PromiseProvider.get(); + var callback; + + try { + callback = this._handleOpenArgs.apply(this, arguments); + } catch (error) { + return new Promise.ES6(function(resolve, reject) { + reject(error); + }); + } + + var _this = this; + var promise = new Promise.ES6(function(resolve, reject) { + _this._open(true, function(error) { + callback && callback(error); + if (error) { + // Error can be on same tick re: christkv/mongodb-core#157 + setImmediate(function() { + reject(error); + if (!callback && !promise.$hasHandler) { + _this.emit('error', error); + } + }); + return; + } + resolve(); + }); + }); + + // Monkey-patch `.then()` so if the promise is handled, we don't emit an + // `error` event. + var _then = promise.then; + promise.then = function(resolve, reject) { + promise.$hasHandler = true; + return _then.call(promise, resolve, reject); + }; + + return promise; +}, '`open()` is deprecated in mongoose >= 4.11.0, use `openUri()` instead, or set the `useMongoClient` option if using `connect()` or `createConnection()`. See http://mongoosejs.com/docs/4.x/docs/connections.html#use-mongo-client'); + +/*! + * ignore + */ + +Connection.prototype._openWithoutPromise = function() { + var callback; + + try { + callback = this._handleOpenArgs.apply(this, arguments); + } catch (error) { + // No need to do anything + } + + var _this = this; + this._open(true, function(error) { + callback && callback(error); + if (error && !callback) { + // Error can be on same tick re: christkv/mongodb-core#157 + setImmediate(function() { + _this.emit('error', error); + }); + return; + } + }); +}; + +/** + * Helper for `createCollection()`. Will explicitly create the given collection + * with specified options. Used to create [capped collections](https://docs.mongodb.com/manual/core/capped-collections/) + * and [views](https://docs.mongodb.com/manual/core/views/) from mongoose. + * + * Options are passed down without modification to the [MongoDB driver's `createCollection()` function](http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#createCollection) + * + * @param {string} collection The collection to delete + * @param {Object} [options] see [MongoDB driver docs](http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#createCollection) + * @param {Function} [callback] + * @return {Promise} + * @api public + */ + +Connection.prototype.createCollection = _wrapConnHelper(function createCollection(collection, options, cb) { + if (typeof options === 'function') { + cb = options; + options = {}; + } + this.db.createCollection(collection, options, cb); +}); + +/** + * Helper for `dropCollection()`. Will delete the given collection, including + * all documents and indexes. + * + * @param {string} collection The collection to delete + * @param {Function} [callback] + * @return {Promise} + * @api public + */ + +Connection.prototype.dropCollection = _wrapConnHelper(function dropCollection(collection, cb) { + this.db.dropCollection(collection, cb); +}); + +/** + * Helper for `dropDatabase()`. Deletes the given database, including all + * collections, documents, and indexes. + * + * @param {Function} [callback] + * @return {Promise} + * @api public + */ + +Connection.prototype.dropDatabase = _wrapConnHelper(function dropDatabase(cb) { + this.db.dropDatabase(cb); +}); + +/*! + * ignore + */ + +function _wrapConnHelper(fn) { + return function() { + var _this = this; + var Promise = PromiseProvider.get(); + var cb = arguments.length > 0 ? arguments[arguments.length - 1] : null; + var argsWithoutCb = typeof cb === 'function' ? + Array.prototype.slice.call(arguments, 0, arguments.length - 1) : + Array.prototype.slice.call(arguments); + var promise = new Promise.ES6(function(resolve, reject) { + if (_this.readyState !== STATES.connected) { + _this.on('open', function() { + fn.apply(_this, argsWithoutCb.concat([function(error) { + if (error) { + reject(error); + } else { + resolve(); + } + }])); + }); + } else { + fn.apply(_this, argsWithoutCb.concat([function(error) { + if (error) { + reject(error); + } else { + resolve(); + } + }])); + } + }); + if (cb) { + promise. + then(function() { cb(); }, function(error) { cb(error); }); + } + return promise; + }; +} + +/*! + * ignore + */ + +Connection.prototype._handleOpenSetArgs = function(uris, database, options, callback) { if (!rgxProtocol.test(uris)) { uris = 'mongodb://' + uris; } @@ -341,7 +480,7 @@ Connection.prototype.openSet = function(uris, database, options, callback) { break; } - if ('function' === typeof options) { + if (typeof options === 'function') { callback = options; options = {}; } @@ -352,20 +491,26 @@ Connection.prototype.openSet = function(uris, database, options, callback) { this.name = database; break; case 'function': - callback = database, database = null; + callback = database; + database = null; break; case 'object': - options = database, database = null; + options = database; + database = null; break; } } + if (typeof database === 'string') { + this.name = database; + } + var parsed; try { parsed = muri(uris); } catch (err) { this.error(err, callback); - return this; + throw err; } if (!this.name) { @@ -377,38 +522,139 @@ Connection.prototype.openSet = function(uris, database, options, callback) { this.replica = true; if (!this.name) { - this.error(new Error('No database name provided for replica set'), callback); - return this; + var err = new Error('No database name provided for replica set'); + this.error(err, callback); + throw err; } // authentication if (this.optionsProvideAuthenticationData(options)) { this.user = options.user; this.pass = options.pass; - } else if (parsed && parsed.auth) { this.user = parsed.auth.user; this.pass = parsed.auth.pass; - } else { this.user = this.pass = undefined; } // global configuration options if (options && options.config) { - if (options.config.autoIndex === false) { - this.config.autoIndex = false; - } - else { - this.config.autoIndex = true; - } + this.config.autoIndex = options.config.autoIndex !== false; + } + + return callback; +}; + +/*! + * ignore + */ +Connection.prototype._openSetWithoutPromise = function(uris, database, options, callback) { + try { + callback = this._handleOpenSetArgs.apply(this, arguments); + } catch (err) { + // Nothing to do, `_handleOpenSetArgs` calls callback if error occurred + return; } - this._open(callback); - return this; + var _this = this; + var emitted = false; + this._open(true, function(error) { + callback && callback(error); + if (error) { + if (!callback && !emitted) { + emitted = true; + _this.emit('error', error); + } + return; + } + }); }; +/** + * Opens the connection to a replica set. + * + * ####Example: + * + * var db = mongoose.createConnection(); + * db.openSet("mongodb://user:pwd@localhost:27020,localhost:27021,localhost:27012/mydb"); + * + * The database name and/or auth need only be included in one URI. + * The `options` is a hash which is passed to the internal driver connection object. + * + * Valid `options` + * + * db - passed to the connection db instance + * server - passed to the connection server instance(s) + * replset - passed to the connection ReplSetServer instance + * user - username for authentication + * pass - password for authentication + * auth - options for authentication (see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate) + * mongos - Boolean - if true, enables High Availability support for mongos + * + * _Options passed take precedence over options included in connection strings._ + * + * ####Notes: + * + * _If connecting to multiple mongos servers, set the `mongos` option to true._ + * + * conn.open('mongodb://mongosA:27501,mongosB:27501', { mongos: true }, cb); + * + * Mongoose forces the db option `forceServerObjectId` false and cannot be overridden. + * Mongoose defaults the server `auto_reconnect` options to true which can be overridden. + * See the node-mongodb-native driver instance for options that it understands. + * + * _Options passed take precedence over options included in connection strings._ + * + * @param {String} uris MongoDB connection string + * @param {String} [database] database name if not included in `uris` + * @param {Object} [options] passed to the internal driver + * @param {Function} [callback] + * @see node-mongodb-native https://github.com/mongodb/node-mongodb-native + * @see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate + * @api public + */ + +Connection.prototype.openSet = util.deprecate(function(uris, database, options, callback) { + var Promise = PromiseProvider.get(); + + try { + callback = this._handleOpenSetArgs.apply(this, arguments); + } catch (err) { + return new Promise.ES6(function(resolve, reject) { + reject(err); + }); + } + + var _this = this; + var emitted = false; + var promise = new Promise.ES6(function(resolve, reject) { + _this._open(true, function(error) { + callback && callback(error); + if (error) { + reject(error); + if (!callback && !promise.$hasHandler && !emitted) { + emitted = true; + _this.emit('error', error); + } + return; + } + resolve(); + }); + }); + + // Monkey-patch `.then()` so if the promise is handled, we don't emit an + // `error` event. + var _then = promise.then; + promise.then = function(resolve, reject) { + promise.$hasHandler = true; + return _then.call(promise, resolve, reject); + }; + + return promise; +}, '`openSet()` is deprecated in mongoose >= 4.11.0, use `openUri()` instead, or set the `useMongoClient` option if using `connect()` or `createConnection()`. See http://mongoosejs.com/docs/4.x/docs/connections.html#use-mongo-client'); + /** * error * @@ -421,7 +667,9 @@ Connection.prototype.openSet = function(uris, database, options, callback) { */ Connection.prototype.error = function(err, callback) { - if (callback) return callback(err); + if (callback) { + return callback(err); + } this.emit('error', err); }; @@ -432,29 +680,31 @@ Connection.prototype.error = function(err, callback) { * @api private */ -Connection.prototype._open = function(callback) { +Connection.prototype._open = function(emit, callback) { this.readyState = STATES.connecting; this._closeCalled = false; - var self = this; + var _this = this; var method = this.replica - ? 'doOpenSet' - : 'doOpen'; + ? 'doOpenSet' + : 'doOpen'; // open connection this[method](function(err) { if (err) { - self.readyState = STATES.disconnected; - if (self._hasOpened) { - if (callback) callback(err); + _this.readyState = STATES.disconnected; + if (_this._hasOpened) { + if (callback) { + callback(err); + } } else { - self.error(err, callback); + _this.error(err, emit && callback); } return; } - self.onOpen(callback); + _this.onOpen(callback); }); }; @@ -465,33 +715,32 @@ Connection.prototype._open = function(callback) { */ Connection.prototype.onOpen = function(callback) { - var self = this; + var _this = this; function open(err, isAuth) { if (err) { - self.readyState = isAuth ? STATES.unauthorized : STATES.disconnected; - if (self._hasOpened) { - if (callback) callback(err); - } else { - self.error(err, callback); - } + _this.readyState = isAuth ? STATES.unauthorized : STATES.disconnected; + _this.error(err, callback); return; } - self.readyState = STATES.connected; + _this.readyState = STATES.connected; // avoid having the collection subscribe to our event emitter // to prevent 0.3 warning - for (var i in self.collections) - self.collections[i].onOpen(); + for (var i in _this.collections) { + if (utils.object.hasOwnProperty(_this.collections, i)) { + _this.collections[i].onOpen(); + } + } callback && callback(); - self.emit('open'); + _this.emit('open'); } - // re-authenticate - if (this.shouldAuthenticate()) { - self.db.authenticate(self.user, self.pass, self.options.auth, function(err) { + // re-authenticate if we're not already connected #3871 + if (this._readyState !== STATES.connected && this.shouldAuthenticate()) { + _this.db.authenticate(_this.user, _this.pass, _this.options.auth, function(err) { open(err, true); }); } else { @@ -499,16 +748,170 @@ Connection.prototype.onOpen = function(callback) { } }; +/** + * Opens the connection with a URI using `MongoClient.connect()`. + * + * @param {String} uri The URI to connect with. + * @param {Object} [options] Passed on to http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html#connect + * @param {Function} [callback] + * @returns {Connection} this + * @api private + */ + +Connection.prototype.openUri = function(uri, options, callback) { + this.readyState = STATES.connecting; + this._closeCalled = false; + + try { + var parsed = muri(uri); + this.name = parsed.db; + this.host = parsed.hosts[0].host || parsed.hosts[0].ipc; + this.port = parsed.hosts[0].port || 27017; + if (parsed.auth) { + this.user = parsed.auth.user; + this.pass = parsed.auth.pass; + } + } catch (error) { + this.error(error, callback); + throw error; + } + + if (typeof options === 'function') { + callback = options; + options = null; + } + + var Promise = PromiseProvider.get(); + var _this = this; + + if (options) { + options = utils.clone(options, { retainKeyOrder: true }); + delete options.useMongoClient; + var autoIndex = options.config && options.config.autoIndex != null ? + options.config.autoIndex : + options.autoIndex; + if (autoIndex != null) { + this.config.autoIndex = autoIndex !== false; + delete options.config; + delete options.autoIndex; + } + + // Backwards compat + if (options.user || options.pass) { + options.auth = options.auth || {}; + options.auth.user = options.user; + options.auth.password = options.pass; + delete options.user; + delete options.pass; + this.user = options.auth.user; + this.pass = options.auth.password; + } + + if (options.bufferCommands != null) { + options.bufferMaxEntries = 0; + this.config.bufferCommands = options.bufferCommands; + delete options.bufferCommands; + } + } + + this._connectionOptions = options; + + var promise = new Promise.ES6(function(resolve, reject) { + mongodb.MongoClient.connect(uri, options, function(error, db) { + if (error) { + _this.readyState = STATES.disconnected; + if (_this.listeners('error').length) { + _this.emit('error', error); + } + callback && callback(error); + return reject(error); + } + // Backwards compat for mongoose 4.x + db.on('reconnect', function() { + _this.readyState = STATES.connected; + _this.emit('reconnect'); + _this.emit('reconnected'); + }); + db.s.topology.on('reconnectFailed', function() { + _this.emit('reconnectFailed'); + }); + db.s.topology.on('close', function() { + // Implicitly emits 'disconnected' + _this.readyState = STATES.disconnected; + }); + db.on('timeout', function() { + _this.emit('timeout'); + }); + + delete _this.then; + delete _this.catch; + + _this.db = db; + _this.readyState = STATES.connected; + + for (var i in _this.collections) { + if (utils.object.hasOwnProperty(_this.collections, i)) { + _this.collections[i].onOpen(); + } + } + + callback && callback(null, _this); + resolve(_this); + _this.emit('open'); + }); + }); + + this.then = function(resolve, reject) { + return promise.then(resolve, reject); + }; + this.catch = function(reject) { + return promise.catch(reject); + }; + + return this; +}; + /** * Closes the connection * + * @param {Boolean} [force] optional * @param {Function} [callback] optional * @return {Connection} self * @api public */ -Connection.prototype.close = function(callback) { - var self = this; +Connection.prototype.close = function(force, callback) { + var _this = this; + var Promise = PromiseProvider.get(); + + if (typeof force === 'function') { + callback = force; + force = false; + } + + this.$wasForceClosed = !!force; + + return new Promise.ES6(function(resolve, reject) { + _this._close(force, function(error) { + callback && callback(error); + if (error) { + reject(error); + return; + } + resolve(); + }); + }); +}; + +/** + * Handles closing the connection + * + * @param {Boolean} force + * @param {Function} callback + * @api private + */ +Connection.prototype._close = function(force, callback) { + var _this = this; this._closeCalled = true; switch (this.readyState) { @@ -519,11 +922,11 @@ Connection.prototype.close = function(callback) { case 1: // connected case 4: // unauthorized this.readyState = STATES.disconnecting; - this.doClose(function(err) { + this.doClose(force, function(err) { if (err) { - self.error(err, callback); + _this.error(err, callback); } else { - self.onClose(); + _this.onClose(force); callback && callback(); } }); @@ -531,12 +934,14 @@ Connection.prototype.close = function(callback) { case 2: // connecting this.once('open', function() { - self.close(callback); + _this.close(callback); }); break; case 3: // disconnecting - if (!callback) break; + if (!callback) { + break; + } this.once('close', function() { callback(); }); @@ -552,15 +957,18 @@ Connection.prototype.close = function(callback) { * @api private */ -Connection.prototype.onClose = function() { +Connection.prototype.onClose = function(force) { this.readyState = STATES.disconnected; // avoid having the collection subscribe to our event emitter // to prevent 0.3 warning - for (var i in this.collections) - this.collections[i].onClose(); + for (var i in this.collections) { + if (utils.object.hasOwnProperty(this.collections, i)) { + this.collections[i].onClose(force); + } + } - this.emit('close'); + this.emit('close', force); }; /** @@ -575,8 +983,11 @@ Connection.prototype.onClose = function() { */ Connection.prototype.collection = function(name, options) { - if (!(name in this.collections)) + options = options ? utils.clone(options, { retainKeyOrder: true }) : {}; + options.$wasForceClosed = this.$wasForceClosed; + if (!(name in this.collections)) { this.collections[name] = new Collection(name, this, options); + } return this.collections[name]; }; @@ -614,27 +1025,31 @@ Connection.prototype.collection = function(name, options) { Connection.prototype.model = function(name, schema, collection) { // collection name discovery - if ('string' == typeof schema) { + if (typeof schema === 'string') { collection = schema; schema = false; } - if (utils.isObject(schema) && !(schema instanceof Schema)) { + if (utils.isObject(schema) && !schema.instanceOfSchema) { schema = new Schema(schema); } + if (schema && !schema.instanceOfSchema) { + throw new Error('The 2nd parameter to `mongoose.model()` should be a ' + + 'schema or a POJO'); + } if (this.models[name] && !collection) { // model exists but we are not subclassing with custom collection - if (schema instanceof Schema && schema != this.models[name].schema) { + if (schema && schema.instanceOfSchema && schema !== this.models[name].schema) { throw new MongooseError.OverwriteModelError(name); } return this.models[name]; } - var opts = { cache: false, connection: this }; + var opts = {cache: false, connection: this}; var model; - if (schema instanceof Schema) { + if (schema && schema.instanceOfSchema) { // compile a model model = this.base.model(name, schema, collection, opts); @@ -664,8 +1079,8 @@ Connection.prototype.model = function(name, schema, collection) { throw new MongooseError.MissingSchemaError(name); } - if (this == model.prototype.db - && (!collection || collection == model.collection.name)) { + if (this === model.prototype.db + && (!collection || collection === model.collection.name)) { // model already uses this connection. // only the first model with this name is cached to allow @@ -676,8 +1091,8 @@ Connection.prototype.model = function(name, schema, collection) { return model; } - - return this.models[name] = model.__subclass(this, schema, collection); + this.models[name] = model.__subclass(this, schema, collection); + return this.models[name]; }; /** @@ -698,8 +1113,8 @@ Connection.prototype.modelNames = function() { * @return {Boolean} true if the connection should be authenticated after it is opened, otherwise false. */ Connection.prototype.shouldAuthenticate = function() { - return (this.user != null) && - ((this.pass != null) || this.authMechanismDoesNotRequirePassword()); + return (this.user !== null && this.user !== void 0) && + ((this.pass !== null || this.pass !== void 0) || this.authMechanismDoesNotRequirePassword()); }; /** @@ -728,8 +1143,8 @@ Connection.prototype.authMechanismDoesNotRequirePassword = function() { */ Connection.prototype.optionsProvideAuthenticationData = function(options) { return (options) && - (options.user) && - ((options.pass) || this.authMechanismDoesNotRequirePassword()); + (options.user) && + ((options.pass) || this.authMechanismDoesNotRequirePassword()); }; /*! diff --git a/lib/cursor/AggregationCursor.js b/lib/cursor/AggregationCursor.js new file mode 100644 index 00000000000..2f360731224 --- /dev/null +++ b/lib/cursor/AggregationCursor.js @@ -0,0 +1,328 @@ +/*! + * Module dependencies. + */ + +var PromiseProvider = require('../promise_provider'); +var Readable = require('stream').Readable; +var util = require('util'); + +/** + * An AggregationCursor is a concurrency primitive for processing aggregation + * results one document at a time. It is analogous to QueryCursor. + * + * An AggregationCursor fulfills the [Node.js streams3 API](https://strongloop.com/strongblog/whats-new-io-js-beta-streams3/), + * in addition to several other mechanisms for loading documents from MongoDB + * one at a time. + * + * Creating an AggregationCursor executes the model's pre aggregate hooks, + * but **not** the model's post aggregate hooks. + * + * Unless you're an advanced user, do **not** instantiate this class directly. + * Use [`Aggregate#cursor()`](/docs/api.html#aggregate_Aggregate-cursor) instead. + * + * @param {Aggregate} agg + * @param {Object} options + * @inherits Readable + * @event `cursor`: Emitted when the cursor is created + * @event `error`: Emitted when an error occurred + * @event `data`: Emitted when the stream is flowing and the next doc is ready + * @event `end`: Emitted when the stream is exhausted + * @api public + */ + +function AggregationCursor(agg) { + Readable.call(this, { objectMode: true }); + + this.cursor = null; + this.agg = agg; + this._transforms = []; + var model = agg._model; + delete agg.options.cursor.useMongooseAggCursor; + + _init(model, this, agg); +} + +util.inherits(AggregationCursor, Readable); + +/*! + * ignore + */ + +function _init(model, c, agg) { + if (!model.collection.buffer) { + model.hooks.execPre('aggregate', agg, function() { + c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {}); + c.emit('cursor', c.cursor); + }); + } else { + model.collection.emitter.once('queue', function() { + model.hooks.execPre('aggregate', agg, function() { + c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {}); + c.emit('cursor', c.cursor); + }); + }); + } +} + +/*! + * Necessary to satisfy the Readable API + */ + +AggregationCursor.prototype._read = function() { + var _this = this; + _next(this, function(error, doc) { + if (error) { + return _this.emit('error', error); + } + if (!doc) { + _this.push(null); + _this.cursor.close(function(error) { + if (error) { + return _this.emit('error', error); + } + setTimeout(function() { + _this.emit('close'); + }, 0); + }); + return; + } + _this.push(doc); + }); +}; + +/** + * Registers a transform function which subsequently maps documents retrieved + * via the streams interface or `.next()` + * + * ####Example + * + * // Map documents returned by `data` events + * Thing. + * find({ name: /^hello/ }). + * cursor(). + * map(function (doc) { + * doc.foo = "bar"; + * return doc; + * }) + * on('data', function(doc) { console.log(doc.foo); }); + * + * // Or map documents returned by `.next()` + * var cursor = Thing.find({ name: /^hello/ }). + * cursor(). + * map(function (doc) { + * doc.foo = "bar"; + * return doc; + * }); + * cursor.next(function(error, doc) { + * console.log(doc.foo); + * }); + * + * @param {Function} fn + * @return {QueryCursor} + * @api public + * @method map + */ + +AggregationCursor.prototype.map = function(fn) { + this._transforms.push(fn); + return this; +}; + +/*! + * Marks this cursor as errored + */ + +AggregationCursor.prototype._markError = function(error) { + this._error = error; + return this; +}; + +/** + * Marks this cursor as closed. Will stop streaming and subsequent calls to + * `next()` will error. + * + * @param {Function} callback + * @return {Promise} + * @api public + * @method close + * @emits close + * @see MongoDB driver cursor#close http://mongodb.github.io/node-mongodb-native/2.1/api/Cursor.html#close + */ + +AggregationCursor.prototype.close = function(callback) { + var Promise = PromiseProvider.get(); + var _this = this; + return new Promise.ES6(function(resolve, reject) { + _this.cursor.close(function(error) { + if (error) { + callback && callback(error); + reject(error); + return _this.listeners('error').length > 0 && + _this.emit('error', error); + } + _this.emit('close'); + resolve(); + callback && callback(); + }); + }); +}; + +/** + * Get the next document from this cursor. Will return `null` when there are + * no documents left. + * + * @param {Function} callback + * @return {Promise} + * @api public + * @method next + */ + +AggregationCursor.prototype.next = function(callback) { + var Promise = PromiseProvider.get(); + var _this = this; + return new Promise.ES6(function(resolve, reject) { + _next(_this, function(error, doc) { + if (error) { + callback && callback(error); + return reject(error); + } + callback && callback(null, doc); + resolve(doc); + }); + }); +}; + +/** + * Execute `fn` for every document in the cursor. If `fn` returns a promise, + * will wait for the promise to resolve before iterating on to the next one. + * Returns a promise that resolves when done. + * + * @param {Function} fn + * @param {Function} [callback] executed when all docs have been processed + * @return {Promise} + * @api public + * @method eachAsync + */ + +AggregationCursor.prototype.eachAsync = function(fn, callback) { + var Promise = PromiseProvider.get(); + var _this = this; + + var handleNextResult = function(doc, callback) { + var promise = fn(doc); + if (promise && typeof promise.then === 'function') { + promise.then( + function() { callback(null); }, + function(error) { callback(error); }); + } else { + callback(null); + } + }; + + var iterate = function(callback) { + return _next(_this, function(error, doc) { + if (error) { + return callback(error); + } + if (!doc) { + return callback(null); + } + handleNextResult(doc, function(error) { + if (error) { + return callback(error); + } + // Make sure to clear the stack re: gh-4697 + setTimeout(function() { + iterate(callback); + }, 0); + }); + }); + }; + + return new Promise.ES6(function(resolve, reject) { + iterate(function(error) { + if (error) { + callback && callback(error); + return reject(error); + } + callback && callback(null); + return resolve(); + }); + }); +}; + +/** + * Adds a [cursor flag](http://mongodb.github.io/node-mongodb-native/2.2/api/Cursor.html#addCursorFlag). + * Useful for setting the `noCursorTimeout` and `tailable` flags. + * + * @param {String} flag + * @param {Boolean} value + * @return {AggregationCursor} this + * @api public + * @method addCursorFlag + */ + +AggregationCursor.prototype.addCursorFlag = function(flag, value) { + var _this = this; + _waitForCursor(this, function() { + _this.cursor.addCursorFlag(flag, value); + }); + return this; +}; + +/*! + * ignore + */ + +function _waitForCursor(ctx, cb) { + if (ctx.cursor) { + return cb(); + } + ctx.once('cursor', function() { + cb(); + }); +} + +/*! + * Get the next doc from the underlying cursor and mongooseify it + * (populate, etc.) + */ + +function _next(ctx, cb) { + var callback = cb; + if (ctx._transforms.length) { + callback = function(err, doc) { + if (err || doc === null) { + return cb(err, doc); + } + cb(err, ctx._transforms.reduce(function(doc, fn) { + return fn(doc); + }, doc)); + }; + } + + if (ctx._error) { + return process.nextTick(function() { + callback(ctx._error); + }); + } + + if (ctx.cursor) { + return ctx.cursor.next(function(error, doc) { + if (error) { + return callback(error); + } + if (!doc) { + return callback(null, null); + } + + callback(null, doc); + }); + } else { + ctx.once('cursor', function() { + _next(ctx, cb); + }); + } +} + +module.exports = AggregationCursor; diff --git a/lib/cursor/QueryCursor.js b/lib/cursor/QueryCursor.js new file mode 100644 index 00000000000..7af480aeb2f --- /dev/null +++ b/lib/cursor/QueryCursor.js @@ -0,0 +1,320 @@ +/*! + * Module dependencies. + */ + +var PromiseProvider = require('../promise_provider'); +var Readable = require('stream').Readable; +var eachAsync = require('../services/cursor/eachAsync'); +var helpers = require('../queryhelpers'); +var util = require('util'); + +/** + * A QueryCursor is a concurrency primitive for processing query results + * one document at a time. A QueryCursor fulfills the [Node.js streams3 API](https://strongloop.com/strongblog/whats-new-io-js-beta-streams3/), + * in addition to several other mechanisms for loading documents from MongoDB + * one at a time. + * + * QueryCursors execute the model's pre find hooks, but **not** the model's + * post find hooks. + * + * Unless you're an advanced user, do **not** instantiate this class directly. + * Use [`Query#cursor()`](/docs/api.html#query_Query-cursor) instead. + * + * @param {Query} query + * @param {Object} options query options passed to `.find()` + * @inherits Readable + * @event `cursor`: Emitted when the cursor is created + * @event `error`: Emitted when an error occurred + * @event `data`: Emitted when the stream is flowing and the next doc is ready + * @event `end`: Emitted when the stream is exhausted + * @api public + */ + +function QueryCursor(query, options) { + Readable.call(this, { objectMode: true }); + + this.cursor = null; + this.query = query; + this._transforms = options.transform ? [options.transform] : []; + var _this = this; + var model = query.model; + model.hooks.execPre('find', query, function() { + model.collection.find(query._conditions, options, function(err, cursor) { + if (_this._error) { + cursor.close(function() {}); + _this.listeners('error').length > 0 && _this.emit('error', _this._error); + } + if (err) { + return _this.emit('error', err); + } + _this.cursor = cursor; + _this.emit('cursor', cursor); + }); + }); +} + +util.inherits(QueryCursor, Readable); + +/*! + * Necessary to satisfy the Readable API + */ + +QueryCursor.prototype._read = function() { + var _this = this; + _next(this, function(error, doc) { + if (error) { + return _this.emit('error', error); + } + if (!doc) { + _this.push(null); + _this.cursor.close(function(error) { + if (error) { + return _this.emit('error', error); + } + setTimeout(function() { + _this.emit('close'); + }, 0); + }); + return; + } + _this.push(doc); + }); +}; + +/** + * Registers a transform function which subsequently maps documents retrieved + * via the streams interface or `.next()` + * + * ####Example + * + * // Map documents returned by `data` events + * Thing. + * find({ name: /^hello/ }). + * cursor(). + * map(function (doc) { + * doc.foo = "bar"; + * return doc; + * }) + * on('data', function(doc) { console.log(doc.foo); }); + * + * // Or map documents returned by `.next()` + * var cursor = Thing.find({ name: /^hello/ }). + * cursor(). + * map(function (doc) { + * doc.foo = "bar"; + * return doc; + * }); + * cursor.next(function(error, doc) { + * console.log(doc.foo); + * }); + * + * @param {Function} fn + * @return {QueryCursor} + * @api public + * @method map + */ + +QueryCursor.prototype.map = function(fn) { + this._transforms.push(fn); + return this; +}; + +/*! + * Marks this cursor as errored + */ + +QueryCursor.prototype._markError = function(error) { + this._error = error; + return this; +}; + +/** + * Marks this cursor as closed. Will stop streaming and subsequent calls to + * `next()` will error. + * + * @param {Function} callback + * @return {Promise} + * @api public + * @method close + * @emits close + * @see MongoDB driver cursor#close http://mongodb.github.io/node-mongodb-native/2.1/api/Cursor.html#close + */ + +QueryCursor.prototype.close = function(callback) { + var Promise = PromiseProvider.get(); + var _this = this; + return new Promise.ES6(function(resolve, reject) { + _this.cursor.close(function(error) { + if (error) { + callback && callback(error); + reject(error); + return _this.listeners('error').length > 0 && + _this.emit('error', error); + } + _this.emit('close'); + resolve(); + callback && callback(); + }); + }); +}; + +/** + * Get the next document from this cursor. Will return `null` when there are + * no documents left. + * + * @param {Function} callback + * @return {Promise} + * @api public + * @method next + */ + +QueryCursor.prototype.next = function(callback) { + var Promise = PromiseProvider.get(); + var _this = this; + return new Promise.ES6(function(resolve, reject) { + _next(_this, function(error, doc) { + if (error) { + callback && callback(error); + return reject(error); + } + callback && callback(null, doc); + resolve(doc); + }); + }); +}; + +/** + * Execute `fn` for every document in the cursor. If `fn` returns a promise, + * will wait for the promise to resolve before iterating on to the next one. + * Returns a promise that resolves when done. + * + * @param {Function} fn + * @param {Object} [options] + * @param {Number} [options.parallel] the number of promises to execute in parallel. Defaults to 1. + * @param {Function} [callback] executed when all docs have been processed + * @return {Promise} + * @api public + * @method eachAsync + */ + +QueryCursor.prototype.eachAsync = function(fn, opts, callback) { + var _this = this; + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } + opts = opts || {}; + + return eachAsync(function(cb) { return _next(_this, cb); }, fn, opts, callback); +}; + +/** + * Adds a [cursor flag](http://mongodb.github.io/node-mongodb-native/2.2/api/Cursor.html#addCursorFlag). + * Useful for setting the `noCursorTimeout` and `tailable` flags. + * + * @param {String} flag + * @param {Boolean} value + * @return {AggregationCursor} this + * @api public + * @method addCursorFlag + */ + +QueryCursor.prototype.addCursorFlag = function(flag, value) { + var _this = this; + _waitForCursor(this, function() { + _this.cursor.addCursorFlag(flag, value); + }); + return this; +}; + +/*! + * Get the next doc from the underlying cursor and mongooseify it + * (populate, etc.) + */ + +function _next(ctx, cb) { + var callback = cb; + if (ctx._transforms.length) { + callback = function(err, doc) { + if (err || doc === null) { + return cb(err, doc); + } + cb(err, ctx._transforms.reduce(function(doc, fn) { + return fn(doc); + }, doc)); + }; + } + + if (ctx._error) { + return process.nextTick(function() { + callback(ctx._error); + }); + } + + if (ctx.cursor) { + return ctx.cursor.next(function(error, doc) { + if (error) { + return callback(error); + } + if (!doc) { + return callback(null, null); + } + + var opts = ctx.query._mongooseOptions; + if (!opts.populate) { + return opts.lean === true ? + callback(null, doc) : + _create(ctx, doc, null, callback); + } + + var pop = helpers.preparePopulationOptionsMQ(ctx.query, + ctx.query._mongooseOptions); + pop.__noPromise = true; + ctx.query.model.populate(doc, pop, function(err, doc) { + if (err) { + return callback(err); + } + return opts.lean === true ? + callback(null, doc) : + _create(ctx, doc, pop, callback); + }); + }); + } else { + ctx.once('cursor', function() { + _next(ctx, cb); + }); + } +} + +/*! + * ignore + */ + +function _waitForCursor(ctx, cb) { + if (ctx.cursor) { + return cb(); + } + ctx.once('cursor', function() { + cb(); + }); +} + +/*! + * Convert a raw doc into a full mongoose doc. + */ + +function _create(ctx, doc, populatedIds, cb) { + var instance = helpers.createModel(ctx.query.model, doc, ctx.query._fields); + var opts = populatedIds ? + { populated: populatedIds } : + undefined; + + instance.init(doc, opts, function(err) { + if (err) { + return cb(err); + } + cb(null, instance); + }); +} + +module.exports = QueryCursor; diff --git a/lib/document.js b/lib/document.js index 03372be3d42..fe514a93bed 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1,26 +1,37 @@ -/* eslint no-unused-vars: 1 */ - /*! * Module dependencies. */ -var EventEmitter = require('events').EventEmitter, - MongooseError = require('./error'), - MixedSchema = require('./schema/mixed'), - Schema = require('./schema'), - ValidatorError = require('./schematype').ValidatorError, - utils = require('./utils'), - clone = utils.clone, - isMongooseObject = utils.isMongooseObject, - inspect = require('util').inspect, - ValidationError = MongooseError.ValidationError, - InternalCache = require('./internal'), - deepEqual = utils.deepEqual, - hooks = require('hooks-fixed'), - PromiseProvider = require('./promise_provider'), - DocumentArray, - MongooseArray, - Embedded; +var EventEmitter = require('events').EventEmitter; +var MongooseError = require('./error'); +var MixedSchema = require('./schema/mixed'); +var Schema = require('./schema'); +var ObjectExpectedError = require('./error/objectExpected'); +var ObjectParameterError = require('./error/objectParameter'); +var StrictModeError = require('./error/strict'); +var ValidatorError = require('./schematype').ValidatorError; +var VirtualType = require('./virtualtype'); +var utils = require('./utils'); +var clone = utils.clone; +var isDefiningProjection = require('./services/projection/isDefiningProjection'); +var isMongooseObject = utils.isMongooseObject; +var inspect = require('util').inspect; +var ValidationError = MongooseError.ValidationError; +var InternalCache = require('./internal'); +var cleanModifiedSubpaths = require('./services/document/cleanModifiedSubpaths'); +var compile = require('./services/document/compile').compile; +var deepEqual = utils.deepEqual; +var defineKey = require('./services/document/compile').defineKey; +var hooks = require('hooks-fixed'); +var PromiseProvider = require('./promise_provider'); +var DocumentArray; +var MongooseArray; +var Embedded; +var flatten = require('./services/common').flatten; +var mpath = require('mpath'); +var idGetter = require('./plugins/idGetter'); + +var specialProperties = ['__proto__', 'constructor', 'prototype']; /** * Document constructor. @@ -34,15 +45,20 @@ var EventEmitter = require('events').EventEmitter, * @api private */ -function Document(obj, fields, skipId) { +function Document(obj, fields, skipId, options) { this.$__ = new InternalCache; this.$__.emitter = new EventEmitter(); this.isNew = true; this.errors = undefined; + this.$__.$options = options || {}; + + if (obj != null && typeof obj !== 'object') { + throw new ObjectParameterError(obj, 'obj', 'Document'); + } var schema = this.schema; - if ('boolean' === typeof fields) { + if (typeof fields === 'boolean') { this.$__.strictMode = fields; fields = undefined; } else { @@ -59,21 +75,31 @@ function Document(obj, fields, skipId) { this._doc = this.$__buildDoc(obj, fields, skipId); if (obj) { - this.set(obj, undefined, true); + if (obj instanceof Document) { + this.isNew = obj.isNew; + } + // Skip set hooks + if (this.$__original_set) { + this.$__original_set(obj, undefined, true); + } else { + this.$set(obj, undefined, true); + } } + this.$__._id = this._id; + if (!schema.options.strict && obj) { - var self = this, + var _this = this, keys = Object.keys(this._doc); keys.forEach(function(key) { if (!(key in schema.tree)) { - defineKey(key, null, self); + defineKey(key, null, _this); } }); } - this.$__registerHooksFromSchema(); + applyQueue(this); } /*! @@ -81,13 +107,13 @@ function Document(obj, fields, skipId) { * `on`, `once`, etc. */ utils.each( - ['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners', - 'removeAllListeners', 'addListener'], - function(emitterFn) { - Document.prototype[emitterFn] = function() { - return this.$__.emitter[emitterFn].apply(this.$__.emitter, arguments); - }; - }); + ['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners', + 'removeAllListeners', 'addListener'], + function(emitterFn) { + Document.prototype[emitterFn] = function() { + return this.$__.emitter[emitterFn].apply(this.$__.emitter, arguments); + }; + }); Document.prototype.constructor = Document; @@ -151,11 +177,12 @@ Document.prototype.$__buildDoc = function(obj, fields, skipId) { var exclude = null; var keys; var ki; - var self = this; + var _this = this; // determine if this doc is a result of a query with // excluded fields - if (fields && 'Object' === utils.getFunctionName(fields.constructor)) { + + if (fields && utils.getFunctionName(fields.constructor) === 'Object') { keys = Object.keys(fields); ki = keys.length; @@ -163,8 +190,9 @@ Document.prototype.$__buildDoc = function(obj, fields, skipId) { exclude = !!fields[keys[ki]]; } else { while (ki--) { - if (keys[ki] !== '_id' && - (!fields[keys[ki]] || typeof fields[keys[ki]] !== 'object')) { + // Does this projection explicitly define inclusion/exclusion? + // Explicitly avoid `$meta` and `$slice` + if (keys[ki] !== '_id' && isDefiningProjection(fields[keys[ki]])) { exclude = !fields[keys[ki]]; break; } @@ -172,16 +200,33 @@ Document.prototype.$__buildDoc = function(obj, fields, skipId) { } } - var paths = Object.keys(this.schema.paths), - plen = paths.length, - ii = 0; + var paths = Object.keys(this.schema.paths); + var plen = paths.length; + var ii = 0; + + var hasIncludedChildren = {}; + if (exclude === false && fields) { + keys = Object.keys(fields); + for (var j = 0; j < keys.length; ++j) { + var parts = keys[j].split('.'); + var c = []; + for (var k = 0; k < parts.length; ++k) { + c.push(parts[k]); + hasIncludedChildren[c.join('.')] = 1; + } + } + } for (; ii < plen; ++ii) { var p = paths[ii]; - if ('_id' == p) { - if (skipId) continue; - if (obj && '_id' in obj) continue; + if (p === '_id') { + if (skipId) { + continue; + } + if (obj && '_id' in obj) { + continue; + } } var type = this.schema.paths[p]; @@ -197,45 +242,51 @@ Document.prototype.$__buildDoc = function(obj, fields, skipId) { var piece = path[i], def; - curPath += piece; + curPath += (!curPath.length ? '' : '.') + piece; // support excluding intermediary levels if (exclude === true) { - if (curPath in fields) break; - } else if (fields && curPath in fields) { - included = true; + if (curPath in fields) { + break; + } + } else if (exclude === false && fields && !included) { + if (curPath in fields) { + included = true; + } else if (!hasIncludedChildren[curPath]) { + break; + } } if (i === last) { if (fields && exclude !== null) { if (exclude === true) { // apply defaults to all non-excluded fields - if (p in fields) continue; + if (p in fields) { + continue; + } - def = type.getDefault(self, true); - if ('undefined' !== typeof def) { + def = type.getDefault(_this, false); + if (typeof def !== 'undefined') { doc_[piece] = def; - self.$__.activePaths.default(p); + _this.$__.activePaths.default(p); } - } else if (included) { // selected field - def = type.getDefault(self, true); - if ('undefined' !== typeof def) { + def = type.getDefault(_this, false); + if (typeof def !== 'undefined') { doc_[piece] = def; - self.$__.activePaths.default(p); + _this.$__.activePaths.default(p); } } } else { - def = type.getDefault(self, true); - if ('undefined' !== typeof def) { + def = type.getDefault(_this, false); + if (typeof def !== 'undefined') { doc_[piece] = def; - self.$__.activePaths.default(p); + _this.$__.activePaths.default(p); } } } else { doc_ = doc_[piece] || (doc_[piece] = {}); - curPath += '.'; } } } @@ -243,6 +294,20 @@ Document.prototype.$__buildDoc = function(obj, fields, skipId) { return doc; }; +/*! + * Converts to POJO when you use the document for querying + */ + +Document.prototype.toBSON = function() { + return this.toObject({ + transform: false, + virtuals: false, + _skipDepopulateTopLevel: true, + depopulate: true, + flattenDecimals: false + }); +}; + /** * Initializes the document without setters or marking anything modified. * @@ -250,35 +315,46 @@ Document.prototype.$__buildDoc = function(obj, fields, skipId) { * * @param {Object} doc document returned by mongo * @param {Function} fn callback - * @api private + * @api public */ Document.prototype.init = function(doc, opts, fn) { // do not prefix this method with $__ since its // used by public hooks - if ('function' == typeof opts) { + if (typeof opts === 'function') { fn = opts; opts = null; } this.isNew = false; + this.$init = true; // handle docs with populated paths // If doc._id is not null or undefined - if (doc._id != null && opts && opts.populated && opts.populated.length) { + if (doc._id !== null && doc._id !== undefined && + opts && opts.populated && opts.populated.length) { var id = String(doc._id); for (var i = 0; i < opts.populated.length; ++i) { var item = opts.populated[i]; - this.populated(item.path, item._docs[id], item); + if (item.isVirtual) { + this.populated(item.path, utils.getValue(item.path, doc), item); + } else { + this.populated(item.path, item._docs[id], item); + } } } init(this, doc, this._doc); - this.$__storeShard(); this.emit('init', this); - if (fn) fn(null); + this.constructor.emit('init', this); + + this.$__._id = this._id; + + if (fn) { + fn(null); + } return this; }; @@ -294,22 +370,44 @@ Document.prototype.init = function(doc, opts, fn) { function init(self, obj, doc, prefix) { prefix = prefix || ''; - var keys = Object.keys(obj), - len = keys.length, - schema, - path, - i; + var keys = Object.keys(obj); + var len = keys.length; + var schema; + var path; + var i; + var index = 0; + + if (self.schema.options.retainKeyOrder) { + while (index < len) { + _init(index++); + } + } else { + while (len--) { + _init(len); + } + } - while (len--) { - i = keys[len]; + function _init(index) { + i = keys[index]; path = prefix + i; schema = self.schema.path(path); + // Should still work if not a model-level discriminator, but should not be + // necessary. This is *only* to catch the case where we queried using the + // base model and the discriminated model has a projection + if (self.schema.$isRootDiscriminator && !self.isSelected(path)) { + return; + } + if (!schema && utils.isObject(obj[i]) && - (!obj[i].constructor || 'Object' == utils.getFunctionName(obj[i].constructor))) { + (!obj[i].constructor || utils.getFunctionName(obj[i].constructor) === 'Object')) { // assume nested object - if (!doc[i]) doc[i] = {}; + if (!doc[i]) { + doc[i] = {}; + } init(self, obj[i], doc[i], path + '.'); + } else if (!schema) { + doc[i] = obj[i]; } else { if (obj[i] === null) { doc[i] = null; @@ -337,49 +435,26 @@ function init(self, obj, doc, prefix) { } } -/** - * Stores the current values of the shard keys. - * - * ####Note: - * - * _Shard key values do not / are not allowed to change._ - * - * @api private - * @method $__storeShard - * @memberOf Document - */ - -Document.prototype.$__storeShard = function() { - // backwards compat - var key = this.schema.options.shardKey || this.schema.options.shardkey; - if (!(key && 'Object' == utils.getFunctionName(key.constructor))) return; - - var orig = this.$__.shardval = {}, - paths = Object.keys(key), - len = paths.length, - val; - - for (var i = 0; i < len; ++i) { - val = this.getValue(paths[i]); - if (isMongooseObject(val)) { - orig[paths[i]] = val.toObject({ depopulate: true }); - } else if (null != val && - val.valueOf && - // Explicitly don't take value of dates - (!val.constructor || utils.getFunctionName(val.constructor) !== 'Date')) { - orig[paths[i]] = val.valueOf(); - } else { - orig[paths[i]] = val; - } - } -}; - /*! * Set up middleware support */ for (var k in hooks) { - Document.prototype[k] = Document[k] = hooks[k]; + if (k === 'post') { + Document.prototype['$' + k] = Document['$' + k] = hooks[k]; + } else if (k === 'pre') { + Document.prototype.$pre = Document.$pre = function mongoosePreWrapper() { + if (arguments[0] === 'set') { + // Make set hooks also work for `$set` + var $setArgs = Array.prototype.slice.call(arguments); + $setArgs[0] = '$set'; + hooks.pre.apply(this, $setArgs); + } + return hooks.pre.apply(this, arguments); + }; + } else { + Document.prototype[k] = Document[k] = hooks[k]; + } } /** @@ -408,69 +483,48 @@ Document.prototype.update = function update() { }; /** - * Sets the value of a path, or many paths. - * - * ####Example: - * - * // path, value - * doc.set(path, value) - * - * // object - * doc.set({ - * path : value - * , path2 : { - * path : value - * } - * }) - * - * // on-the-fly cast to number - * doc.set(path, value, Number) - * - * // on-the-fly cast to string - * doc.set(path, value, String) - * - * // changing strict mode behavior - * doc.set(path, value, { strict: false }); + * Alias for `set()`, used internally to avoid conflicts * * @param {String|Object} path path or object of key/vals to set * @param {Any} val the value to set - * @param {Schema|String|Number|Buffer|etc..} [type] optionally specify a type for "on-the-fly" attributes + * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for "on-the-fly" attributes * @param {Object} [options] optionally specify options that modify the behavior of the set + * @method $set * @api public */ -Document.prototype.set = function(path, val, type, options) { - if (type && 'Object' == utils.getFunctionName(type.constructor)) { +Document.prototype.$set = function $set(path, val, type, options) { + if (type && utils.getFunctionName(type.constructor) === 'Object') { options = type; type = undefined; } - var merge = options && options.merge, - adhoc = type && true !== type, - constructing = true === type, - adhocs; + options = options || {}; + var merge = options.merge; + var adhoc = type && type !== true; + var constructing = type === true; + var adhocs; - var strict = options && 'strict' in options - ? options.strict - : this.$__.strictMode; + var strict = 'strict' in options + ? options.strict + : this.$__.strictMode; if (adhoc) { adhocs = this.$__.adhocPaths || (this.$__.adhocPaths = {}); adhocs[path] = Schema.interpretAsType(path, type, this.schema.options); } - if ('string' !== typeof path) { + if (typeof path !== 'string') { // new Document({ key: val }) - if (null === path || undefined === path) { + if (path === null || path === void 0) { var _ = path; path = val; val = _; - } else { var prefix = val - ? val + '.' - : ''; + ? val + '.' + : ''; if (path instanceof Document) { if (path.$__isNested) { @@ -480,38 +534,26 @@ Document.prototype.set = function(path, val, type, options) { } } - var keys = Object.keys(path), - i = keys.length, - pathtype, - key; - - while (i--) { - key = keys[i]; - var pathName = prefix + key; - pathtype = this.schema.pathType(pathName); - - if (null != path[key] - // need to know if plain object - no Buffer, ObjectId, ref, etc - && utils.isObject(path[key]) - && (!path[key].constructor || 'Object' == utils.getFunctionName(path[key].constructor)) - && 'virtual' !== pathtype - && 'real' !== pathtype - && !(this.$__path(pathName) instanceof MixedSchema) - && !(this.schema.paths[pathName] && - this.schema.paths[pathName].options && - this.schema.paths[pathName].options.ref)) { - this.set(path[key], prefix + key, constructing); - } else if (strict) { - if ('real' === pathtype || 'virtual' === pathtype) { - this.set(prefix + key, path[key], constructing); - } else if (pathtype === 'nested' && path[key] instanceof Document) { - this.set(prefix + key, - path[key].toObject({ virtuals: false }), constructing); - } else if ('throw' == strict) { - throw new Error('Field `' + key + '` is not in schema.'); - } - } else if (undefined !== path[key]) { - this.set(prefix + key, path[key], constructing); + var keys = Object.keys(path); + var len = keys.length; + var i = 0; + var pathtype; + var key; + + if (len === 0 && !this.schema.options.minimize) { + if (val) { + this.$set(val, {}); + } + return this; + } + + if (this.schema.options.retainKeyOrder) { + while (i < len) { + _handleIndex.call(this, i++); + } + } else { + while (len--) { + _handleIndex.call(this, len); } } @@ -519,15 +561,70 @@ Document.prototype.set = function(path, val, type, options) { } } - // ensure _strict is honored for obj props - // docschema = new Schema({ path: { nest: 'string' }}) - // doc.set('path', obj); + function _handleIndex(i) { + key = keys[i]; + var pathName = prefix + key; + pathtype = this.schema.pathType(pathName); + + if (path[key] !== null + && path[key] !== void 0 + // need to know if plain object - no Buffer, ObjectId, ref, etc + && utils.isObject(path[key]) + && (!path[key].constructor || utils.getFunctionName(path[key].constructor) === 'Object') + && pathtype !== 'virtual' + && pathtype !== 'real' + && !(this.$__path(pathName) instanceof MixedSchema) + && !(this.schema.paths[pathName] && + this.schema.paths[pathName].options && + this.schema.paths[pathName].options.ref)) { + this.$set(path[key], prefix + key, constructing); + } else if (strict) { + // Don't overwrite defaults with undefined keys (gh-3981) + if (constructing && path[key] === void 0 && + this.get(key) !== void 0) { + return; + } + + if (pathtype === 'real' || pathtype === 'virtual') { + // Check for setting single embedded schema to document (gh-3535) + var p = path[key]; + if (this.schema.paths[pathName] && + this.schema.paths[pathName].$isSingleNested && + path[key] instanceof Document) { + p = p.toObject({ virtuals: false, transform: false }); + } + this.$set(prefix + key, p, constructing); + } else if (pathtype === 'nested' && path[key] instanceof Document) { + this.$set(prefix + key, + path[key].toObject({transform: false}), constructing); + } else if (strict === 'throw') { + if (pathtype === 'nested') { + throw new ObjectExpectedError(key, path[key]); + } else { + throw new StrictModeError(key); + } + } + } else if (path[key] !== void 0) { + this.$set(prefix + key, path[key], constructing); + } + } + var pathType = this.schema.pathType(path); - if ('nested' == pathType && val) { + if (pathType === 'nested' && val) { if (utils.isObject(val) && - (!val.constructor || 'Object' == utils.getFunctionName(val.constructor))) { - if (!merge) this.setValue(path, null); - this.set(val, path, constructing); + (!val.constructor || utils.getFunctionName(val.constructor) === 'Object')) { + if (!merge) { + this.setValue(path, null); + cleanModifiedSubpaths(this, path); + } + + if (Object.keys(val).length === 0) { + this.setValue(path, {}); + this.markModified(path); + cleanModifiedSubpaths(this, path); + } else { + this.$set(val, path, constructing); + } return this; } this.invalidate(path, new MongooseError.CastError('Object', val, path)); @@ -537,8 +634,7 @@ Document.prototype.set = function(path, val, type, options) { var schema; var parts = path.split('.'); - if ('adhocOrUndefined' == pathType && strict) { - + if (pathType === 'adhocOrUndefined' && strict) { // check for roots that are Mixed types var mixed; @@ -550,16 +646,21 @@ Document.prototype.set = function(path, val, type, options) { mixed = true; break; } + + // If path is underneath a virtual, bypass everything and just set it. + if (i + 1 < parts.length && this.schema.pathType(subpath) === 'virtual') { + mpath.set(path, val, this); + return this; + } } if (!mixed) { - if ('throw' == strict) { - throw new Error("Field `" + path + "` is not in schema."); + if (strict === 'throw') { + throw new StrictModeError(path); } return this; } - - } else if ('virtual' == pathType) { + } else if (pathType === 'virtual') { schema = this.schema.virtualpath(path); schema.applySetters(val, this); return this; @@ -567,6 +668,26 @@ Document.prototype.set = function(path, val, type, options) { schema = this.$__path(path); } + // gh-4578, if setting a deeply nested path that doesn't exist yet, create it + var cur = this._doc; + var curPath = ''; + for (i = 0; i < parts.length - 1; ++i) { + cur = cur[parts[i]]; + curPath += (curPath.length > 0 ? '.' : '') + parts[i]; + if (!cur) { + this.$set(curPath, {}); + // Hack re: gh-5800. If nested field is not selected, it probably exists + // so `MongoError: cannot use the part (nested of nested.num) to + // traverse the element ({nested: null})` is not likely. If user gets + // that error, its their fault for now. We should reconsider disallowing + // modifying not selected paths for v5. + if (!this.isSelected(curPath)) { + this.unmarkModified(curPath); + } + cur = this.getValue(curPath); + } + } + var pathToMark; // When using the $set operator the path to the field must already exist. @@ -578,20 +699,22 @@ Document.prototype.set = function(path, val, type, options) { for (i = 0; i < parts.length; ++i) { subpath = parts.slice(0, i + 1).join('.'); if (this.isDirectModified(subpath) // earlier prefixes that are already - // marked as dirty have precedence + // marked as dirty have precedence || this.get(subpath) === null) { pathToMark = subpath; break; } } - if (!pathToMark) pathToMark = path; + if (!pathToMark) { + pathToMark = path; + } } // if this doc is being constructed we should not trigger getters - var priorVal = constructing - ? undefined - : this.getValue(path); + var priorVal = constructing ? + undefined : + this.getValue(path); if (!schema) { this.$__set(pathToMark, path, constructing, parts, schema, val, priorVal); @@ -606,11 +729,41 @@ Document.prototype.set = function(path, val, type, options) { if (schema.options && schema.options.ref && val instanceof Document && - schema.options.ref === val.constructor.modelName) { - this.populated(path, val._id); + (schema.options.ref === val.constructor.modelName || schema.options.ref === val.constructor.baseModelName)) { + if (this.ownerDocument) { + this.ownerDocument().populated(this.$__fullPath(path), + val._id, {model: val.constructor}); + } else { + this.populated(path, val._id, {model: val.constructor}); + } + didPopulate = true; + } + + var popOpts; + if (schema.options && + Array.isArray(schema.options[this.schema.options.typeKey]) && + schema.options[this.schema.options.typeKey].length && + schema.options[this.schema.options.typeKey][0].ref && + Array.isArray(val) && + val.length > 0 && + val[0] instanceof Document && + val[0].constructor.modelName && + (schema.options[this.schema.options.typeKey][0].ref === val[0].constructor.baseModelName || schema.options[this.schema.options.typeKey][0].ref === val[0].constructor.modelName)) { + if (this.ownerDocument) { + popOpts = { model: val[0].constructor }; + this.ownerDocument().populated(this.$__fullPath(path), + val.map(function(v) { return v._id; }), popOpts); + } else { + popOpts = { model: val[0].constructor }; + this.populated(path, val.map(function(v) { return v._id; }), popOpts); + } didPopulate = true; } - val = schema.applySetters(val, this, false, priorVal); + + var setterContext = constructing && this.$__.$options.priorDoc ? + this.$__.$options.priorDoc : + this; + val = schema.applySetters(val, setterContext, false, priorVal); if (!didPopulate && this.$__.populated) { delete this.$__.populated[path]; @@ -618,12 +771,8 @@ Document.prototype.set = function(path, val, type, options) { this.$markValid(path); } catch (e) { - var reason; - if (!(e instanceof MongooseError.CastError)) { - reason = e; - } this.invalidate(path, - new MongooseError.CastError(schema.instance, val, path, reason)); + new MongooseError.CastError(schema.instance, val, path, e)); shouldSet = false; } @@ -631,9 +780,47 @@ Document.prototype.set = function(path, val, type, options) { this.$__set(pathToMark, path, constructing, parts, schema, val, priorVal); } + if (schema.$isSingleNested && (this.isDirectModified(path) || val == null)) { + cleanModifiedSubpaths(this, path); + } + return this; }; +/** + * Sets the value of a path, or many paths. + * + * ####Example: + * + * // path, value + * doc.set(path, value) + * + * // object + * doc.set({ + * path : value + * , path2 : { + * path : value + * } + * }) + * + * // on-the-fly cast to number + * doc.set(path, value, Number) + * + * // on-the-fly cast to string + * doc.set(path, value, String) + * + * // changing strict mode behavior + * doc.set(path, value, { strict: false }); + * + * @param {String|Object} path path or object of key/vals to set + * @param {Any} val the value to set + * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for "on-the-fly" attributes + * @param {Object} [options] optionally specify options that modify the behavior of the set + * @api public + */ + +Document.prototype.set = Document.prototype.$set; + /** * Determine if we should mark this change as modified. * @@ -643,10 +830,10 @@ Document.prototype.set = function(path, val, type, options) { * @memberOf Document */ -Document.prototype.$__shouldModify = function( - pathToMark, path, constructing, parts, schema, val, priorVal) { - - if (this.isNew) return true; +Document.prototype.$__shouldModify = function(pathToMark, path, constructing, parts, schema, val, priorVal) { + if (this.isNew) { + return true; + } if (undefined === val && !this.isSelected(path)) { // when a path is not selected in a query, its initial @@ -659,12 +846,21 @@ Document.prototype.$__shouldModify = function( return false; } + // gh-3992: if setting a populated field to a doc, don't mark modified + // if they have the same _id + if (this.populated(path) && + val instanceof Document && + deepEqual(val._id, priorVal)) { + return false; + } + if (!deepEqual(val, priorVal || this.get(path))) { return true; } if (!constructing && - null != val && + val !== null && + val !== undefined && path in this.$__.activePaths.states.default && deepEqual(val, schema.getDefault(this, constructing))) { // a path with a default was $unset on the server @@ -682,21 +878,28 @@ Document.prototype.$__shouldModify = function( * @memberOf Document */ -Document.prototype.$__set = function( - pathToMark, path, constructing, parts, schema, val, priorVal) { +Document.prototype.$__set = function(pathToMark, path, constructing, parts, schema, val, priorVal) { Embedded = Embedded || require('./types/embedded'); - var shouldModify = this.$__shouldModify.apply(this, arguments); + var shouldModify = this.$__shouldModify(pathToMark, path, constructing, parts, + schema, val, priorVal); var _this = this; if (shouldModify) { - this.markModified(pathToMark, val); + this.markModified(pathToMark); // handle directly setting arrays (gh-1126) MongooseArray || (MongooseArray = require('./types/array')); if (val && val.isMongooseArray) { val._registerAtomic('$set', val); + // Update embedded document parent references (gh-5189) + if (val.isMongooseDocumentArray) { + val.forEach(function(item) { + item && item.__parentArray && (item.__parentArray = val); + }); + } + // Small hack for gh-1638: if we're overwriting the entire array, ignore // paths that were modified before the array overwrite this.$__.activePaths.forEach(function(modifiedPath) { @@ -707,25 +910,33 @@ Document.prototype.$__set = function( } } - var obj = this._doc, - i = 0, - l = parts.length; + var obj = this._doc; + var i = 0; + var l = parts.length; + var cur = ''; for (; i < l; i++) { - var next = i + 1, - last = next === l; + var next = i + 1; + var last = next === l; + cur += (cur ? '.' + parts[i] : parts[i]); + if (specialProperties.indexOf(parts[i]) !== -1) { + return; + } if (last) { obj[parts[i]] = val; } else { - if (obj[parts[i]] && 'Object' === utils.getFunctionName(obj[parts[i]].constructor)) { + if (obj[parts[i]] && utils.getFunctionName(obj[parts[i]].constructor) === 'Object') { obj = obj[parts[i]]; } else if (obj[parts[i]] && obj[parts[i]] instanceof Embedded) { obj = obj[parts[i]]; + } else if (obj[parts[i]] && obj[parts[i]].$isSingleNested) { + obj = obj[parts[i]]; } else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) { obj = obj[parts[i]]; } else { - obj = obj[parts[i]] = {}; + obj[parts[i]] = obj[parts[i]] || {}; + obj = obj[parts[i]]; } } } @@ -767,7 +978,7 @@ Document.prototype.setValue = function(path, val) { * doc.get('age', String) // "47" * * @param {String} path - * @param {Schema|String|Number|Buffer|etc..} [type] optionally specify a type for on-the-fly attributes + * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for on-the-fly attributes * @api public */ @@ -777,23 +988,28 @@ Document.prototype.get = function(path, type) { adhoc = Schema.interpretAsType(path, type, this.schema.options); } - var schema = this.$__path(path) || this.schema.virtualpath(path), - pieces = path.split('.'), - obj = this._doc; + var schema = this.$__path(path) || this.schema.virtualpath(path); + var pieces = path.split('.'); + var obj = this._doc; + + if (schema instanceof VirtualType) { + if (schema.getters.length === 0) { + return void 0; + } + return schema.applyGetters(null, this); + } for (var i = 0, l = pieces.length; i < l; i++) { - obj = undefined === obj || null === obj - ? undefined - : obj[pieces[i]]; + obj = obj === null || obj === void 0 + ? undefined + : obj[pieces[i]]; } if (adhoc) { obj = adhoc.cast(obj); } - // Check if this path is populated - don't apply getters if it is, - // because otherwise its a nested object. See gh-3357 - if (schema && !this.populated(path)) { + if (schema) { obj = schema.applyGetters(obj, this); } @@ -815,9 +1031,8 @@ Document.prototype.$__path = function(path) { if (adhocType) { return adhocType; - } else { - return this.schema.path(path); } + return this.schema.path(path); }; /** @@ -832,11 +1047,52 @@ Document.prototype.$__path = function(path) { * doc.save() // changes to mixed.type are now persisted * * @param {String} path the path to mark modified + * @param {Document} [scope] the scope to run validators with * @api public */ -Document.prototype.markModified = function(path) { +Document.prototype.markModified = function(path, scope) { this.$__.activePaths.modify(path); + if (scope != null && !this.ownerDocument) { + this.$__.pathsToScopes[path] = scope; + } +}; + +/** + * Clears the modified state on the specified path. + * + * ####Example: + * + * doc.foo = 'bar'; + * doc.unmarkModified('foo'); + * doc.save() // changes to foo will not be persisted + * + * @param {String} path the path to unmark modified + * @api public + */ + +Document.prototype.unmarkModified = function(path) { + this.$__.activePaths.init(path); + delete this.$__.pathsToScopes[path]; +}; + +/** + * Don't run validation on this path or persist changes to this path. + * + * ####Example: + * + * doc.foo = null; + * doc.$ignore('foo'); + * doc.save() // changes to foo will not be persisted and validators won't be run + * + * @memberOf Document + * @method $ignore + * @param {String} path the path to ignore + * @api public + */ + +Document.prototype.$ignore = function(path) { + this.$__.activePaths.ignore(path); }; /** @@ -848,12 +1104,13 @@ Document.prototype.markModified = function(path) { Document.prototype.modifiedPaths = function() { var directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); - return directModifiedPaths.reduce(function(list, path) { var parts = path.split('.'); return list.concat(parts.reduce(function(chains, part, i) { return chains.concat(parts.slice(0, i).concat(part).join('.')); - }, [])); + }, []).filter(function(chain) { + return (list.indexOf(chain) === -1); + })); }, []); }; @@ -865,20 +1122,34 @@ Document.prototype.modifiedPaths = function() { * ####Example * * doc.set('documents.0.title', 'changed'); - * doc.isModified() // true - * doc.isModified('documents') // true - * doc.isModified('documents.0.title') // true - * doc.isDirectModified('documents') // false + * doc.isModified() // true + * doc.isModified('documents') // true + * doc.isModified('documents.0.title') // true + * doc.isModified('documents otherProp') // true + * doc.isDirectModified('documents') // false * * @param {String} [path] optional * @return {Boolean} * @api public */ -Document.prototype.isModified = function(path) { - return path - ? !!~this.modifiedPaths().indexOf(path) - : this.$__.activePaths.some('modify'); +Document.prototype.isModified = function(paths) { + if (paths) { + if (!Array.isArray(paths)) { + paths = paths.split(' '); + } + var modified = this.modifiedPaths(); + var directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); + var isModifiedChild = paths.some(function(path) { + return !!~modified.indexOf(path); + }); + return isModifiedChild || paths.some(function(path) { + return directModifiedPaths.some(function(mod) { + return mod === path || path.indexOf(mod + '.') === 0; + }); + }); + } + return this.$__.activePaths.some('modify'); }; /** @@ -888,11 +1159,12 @@ Document.prototype.isModified = function(path) { * * MyModel = mongoose.model('test', { name: { type: String, default: 'Val '} }); * var m = new MyModel(); - * m.$isDefault('name'); // true + * m.$isDefault('name'); // true * + * @memberOf Document + * @method $isDefault * @param {String} [path] * @return {Boolean} - * @method $isDefault * @api public */ @@ -900,6 +1172,34 @@ Document.prototype.$isDefault = function(path) { return (path in this.$__.activePaths.states.default); }; +/** + * Getter/setter, determines whether the document was removed or not. + * + * ####Example: + * product.remove(function (err, product) { + * product.isDeleted(); // true + * product.remove(); // no-op, doesn't send anything to the db + * + * product.isDeleted(false); + * product.isDeleted(); // false + * product.remove(); // will execute a remove against the db + * }) + * + * @param {Boolean} [val] optional, overrides whether mongoose thinks the doc is deleted + * @return {Boolean} whether mongoose thinks this doc is deleted. + * @method $isDeleted + * @api public + */ + +Document.prototype.$isDeleted = function(val) { + if (arguments.length === 0) { + return !!this.$__.isDeleted; + } + + this.$__.isDeleted = !!val; + return this; +}; + /** * Returns true if `path` was directly set and modified, else false. * @@ -945,48 +1245,117 @@ Document.prototype.isInit = function(path) { * @api public */ -Document.prototype.isSelected = function isSelected(path) { +Document.prototype.isSelected = function isSelected(path) { + if (this.$__.selected) { + if (path === '_id') { + return this.$__.selected._id !== 0; + } + + var paths = Object.keys(this.$__.selected); + var i = paths.length; + var inclusive = null; + var cur; + + if (i === 1 && paths[0] === '_id') { + // only _id was selected. + return this.$__.selected._id === 0; + } + + while (i--) { + cur = paths[i]; + if (cur === '_id') { + continue; + } + if (!isDefiningProjection(this.$__.selected[cur])) { + continue; + } + inclusive = !!this.$__.selected[cur]; + break; + } + + if (inclusive === null) { + return true; + } + + if (path in this.$__.selected) { + return inclusive; + } + + i = paths.length; + var pathDot = path + '.'; + + while (i--) { + cur = paths[i]; + if (cur === '_id') { + continue; + } + + if (cur.indexOf(pathDot) === 0) { + return inclusive || cur !== pathDot; + } + + if (pathDot.indexOf(cur + '.') === 0) { + return inclusive; + } + } + + return !inclusive; + } + + return true; +}; + +/** + * Checks if `path` was explicitly selected. If no projection, always returns + * true. + * + * ####Example + * + * Thing.findOne().select('nested.name').exec(function (err, doc) { + * doc.isDirectSelected('nested.name') // true + * doc.isDirectSelected('nested.otherName') // false + * doc.isDirectSelected('nested') // false + * }) + * + * @param {String} path + * @return {Boolean} + * @api public + */ + +Document.prototype.isDirectSelected = function isDirectSelected(path) { if (this.$__.selected) { - - if ('_id' === path) { - return 0 !== this.$__.selected._id; + if (path === '_id') { + return this.$__.selected._id !== 0; } - var paths = Object.keys(this.$__.selected), - i = paths.length, - inclusive = false, - cur; + var paths = Object.keys(this.$__.selected); + var i = paths.length; + var inclusive = null; + var cur; - if (1 === i && '_id' === paths[0]) { + if (i === 1 && paths[0] === '_id') { // only _id was selected. - return 0 === this.$__.selected._id; + return this.$__.selected._id === 0; } while (i--) { cur = paths[i]; - if ('_id' == cur) continue; + if (cur === '_id') { + continue; + } + if (!isDefiningProjection(this.$__.selected[cur])) { + continue; + } inclusive = !!this.$__.selected[cur]; break; } - if (path in this.$__.selected) { - return inclusive; + if (inclusive === null) { + return true; } - i = paths.length; - var pathDot = path + '.'; - - while (i--) { - cur = paths[i]; - if ('_id' == cur) continue; - - if (0 === cur.indexOf(pathDot)) { - return inclusive; - } - - if (0 === pathDot.indexOf(cur + '.')) { - return inclusive; - } + if (path in this.$__.selected) { + return inclusive; } return !inclusive; @@ -1010,136 +1379,190 @@ Document.prototype.isSelected = function isSelected(path) { * }); * * @param {Object} optional options internal options - * @param {Function} optional callback called after validation completes, passing an error if one occurred + * @param {Function} callback optional callback called after validation completes, passing an error if one occurred * @return {Promise} Promise * @api public */ Document.prototype.validate = function(options, callback) { - var _this = this; - var Promise = PromiseProvider.get(); if (typeof options === 'function') { callback = options; options = null; } - if (options && options.__noPromise) { - this.$__validate(callback); - return; + this.$__validate(callback || function() {}); +}; + +/*! + * ignore + */ + +function _getPathsToValidate(doc) { + var i; + var len; + + // only validate required fields when necessary + var paths = Object.keys(doc.$__.activePaths.states.require).filter(function(path) { + if (!doc.isSelected(path) && !doc.isModified(path)) { + return false; + } + var p = doc.schema.path(path); + if (typeof p.originalRequiredValue === 'function') { + return p.originalRequiredValue.call(doc); + } + return true; + }); + + paths = paths.concat(Object.keys(doc.$__.activePaths.states.init)); + paths = paths.concat(Object.keys(doc.$__.activePaths.states.modify)); + paths = paths.concat(Object.keys(doc.$__.activePaths.states.default)); + + if (!doc.ownerDocument) { + var subdocs = doc.$__getAllSubdocs(); + var subdoc; + len = subdocs.length; + for (i = 0; i < len; ++i) { + subdoc = subdocs[i]; + if (doc.isModified(subdoc.$basePath) && + !doc.isDirectModified(subdoc.$basePath)) { + // Remove child paths for now, because we'll be validating the whole + // subdoc + paths = paths.filter(function(p) { + return p != null && p.indexOf(subdoc.$basePath + '.') !== 0; + }); + paths.push(subdoc.$basePath); + } + } } - return new Promise.ES6(function(resolve, reject) { - _this.$__validate(function(error) { - callback && callback(error); - if (error) { - reject(error); - return; + // gh-661: if a whole array is modified, make sure to run validation on all + // the children as well + len = paths.length; + for (i = 0; i < len; ++i) { + var path = paths[i]; + + var _pathType = doc.schema.path(path); + if (!_pathType || !_pathType.$isMongooseArray || _pathType.$isMongooseDocumentArray) { + continue; + } + + var val = doc.getValue(path); + if (val) { + var numElements = val.length; + for (var j = 0; j < numElements; ++j) { + paths.push(path + '.' + j); } - resolve(); - }); - }); -}; + } + } + + var flattenOptions = { skipArrays: true }; + len = paths.length; + for (i = 0; i < len; ++i) { + var pathToCheck = paths[i]; + if (doc.schema.nested[pathToCheck]) { + var _v = doc.getValue(pathToCheck); + if (isMongooseObject(_v)) { + _v = _v.toObject({ transform: false }); + } + var flat = flatten(_v, '', flattenOptions); + var _subpaths = Object.keys(flat).map(function(p) { + return pathToCheck + '.' + p; + }); + paths = paths.concat(_subpaths); + } + } + + return paths; +} /*! * ignore */ Document.prototype.$__validate = function(callback) { - var self = this; + var _this = this; var _complete = function() { - var err = self.$__.validationError; - self.$__.validationError = undefined; - self.emit('validate', self); + var err = _this.$__.validationError; + _this.$__.validationError = undefined; + _this.emit('validate', _this); + _this.constructor.emit('validate', _this); if (err) { for (var key in err.errors) { // Make sure cast errors persist - if (!self.__parent && err.errors[key] instanceof MongooseError.CastError) { - self.invalidate(key, err.errors[key]); + if (!_this.__parent && err.errors[key] instanceof MongooseError.CastError) { + _this.invalidate(key, err.errors[key]); } } return err; - } else { - return; } }; // only validate required fields when necessary - var paths = Object.keys(this.$__.activePaths.states.require).filter(function(path) { - if (!self.isSelected(path) && !self.isModified(path)) return false; - return true; - }); + var paths = _getPathsToValidate(this); - paths = paths.concat(Object.keys(this.$__.activePaths.states.init)); - paths = paths.concat(Object.keys(this.$__.activePaths.states.modify)); - paths = paths.concat(Object.keys(this.$__.activePaths.states.default)); - - if (0 === paths.length) { - process.nextTick(function() { - var err = _complete(); - if (err) { - callback(err); - return; + if (paths.length === 0) { + return process.nextTick(function() { + var error = _complete(); + if (error) { + return _this.schema.s.hooks.execPost('validate:error', _this, [ _this], { error: error }, function(error) { + callback(error); + }); } callback(); }); } - var validating = {}, - total = 0; - - // gh-661: if a whole array is modified, make sure to run validation on all - // the children as well - for (var i = 0; i < paths.length; ++i) { - var path = paths[i]; - var val = self.getValue(path); - if (val && val.isMongooseArray && !Buffer.isBuffer(val) && - !val.isMongooseDocumentArray) { - var numElements = val.length; - for (var j = 0; j < numElements; ++j) { - paths.push(path + '.' + j); - } - } - } + var validated = {}; + var total = 0; var complete = function() { - var err = _complete(); - if (err) { - callback(err); - return; + var error = _complete(); + if (error) { + return _this.schema.s.hooks.execPost('validate:error', _this, [ _this], { error: error }, function(error) { + callback(error); + }); } callback(); }; var validatePath = function(path) { - if (validating[path]) return; + if (path == null || validated[path]) { + return; + } - validating[path] = true; + validated[path] = true; total++; process.nextTick(function() { - var p = self.schema.path(path); + var p = _this.schema.path(path); if (!p) { return --total || complete(); } // If user marked as invalid or there was a cast error, don't validate - if (!self.$isValid(path)) { + if (!_this.$isValid(path)) { --total || complete(); return; } - var val = self.getValue(path); + var val = _this.getValue(path); + var scope = path in _this.$__.pathsToScopes ? + _this.$__.pathsToScopes[path] : + _this; p.doValidate(val, function(err) { if (err) { - self.invalidate(path, err, undefined, true); + _this.invalidate(path, err, undefined, true); } --total || complete(); - }, self); + }, scope); }); }; - paths.forEach(validatePath); + var numPaths = paths.length; + for (var i = 0; i < numPaths; ++i) { + validatePath(paths[i]); + } }; /** @@ -1164,21 +1587,14 @@ Document.prototype.$__validate = function(callback) { */ Document.prototype.validateSync = function(pathsToValidate) { - var self = this; + var _this = this; if (typeof pathsToValidate === 'string') { pathsToValidate = pathsToValidate.split(' '); } // only validate required fields when necessary - var paths = Object.keys(this.$__.activePaths.states.require).filter(function(path) { - if (!self.isSelected(path) && !self.isModified(path)) return false; - return true; - }); - - paths = paths.concat(Object.keys(this.$__.activePaths.states.init)); - paths = paths.concat(Object.keys(this.$__.activePaths.states.modify)); - paths = paths.concat(Object.keys(this.$__.activePaths.states.default)); + var paths = _getPathsToValidate(this); if (pathsToValidate && pathsToValidate.length) { var tmp = []; @@ -1193,32 +1609,37 @@ Document.prototype.validateSync = function(pathsToValidate) { var validating = {}; paths.forEach(function(path) { - if (validating[path]) return; + if (validating[path]) { + return; + } validating[path] = true; - var p = self.schema.path(path); - if (!p) return; - if (!self.$isValid(path)) { + var p = _this.schema.path(path); + if (!p) { + return; + } + if (!_this.$isValid(path)) { return; } - var val = self.getValue(path); - var err = p.doValidateSync(val, self); + var val = _this.getValue(path); + var err = p.doValidateSync(val, _this); if (err) { - self.invalidate(path, err, undefined, true); + _this.invalidate(path, err, undefined, true); } }); - var err = self.$__.validationError; - self.$__.validationError = undefined; - self.emit('validate', self); + var err = _this.$__.validationError; + _this.$__.validationError = undefined; + _this.emit('validate', _this); + _this.constructor.emit('validate', _this); if (err) { for (var key in err.errors) { // Make sure cast errors persist if (err.errors[key] instanceof MongooseError.CastError) { - self.invalidate(key, err.errors[key]); + _this.invalidate(key, err.errors[key]); } } } @@ -1252,35 +1673,42 @@ Document.prototype.validateSync = function(pathsToValidate) { * @param {String} path the field to invalidate * @param {String|Error} errorMsg the error which states the reason `path` was invalid * @param {Object|String|Number|any} value optional invalid value + * @param {String} [kind] optional `kind` property for the error + * @return {ValidationError} the current ValidationError, with all currently invalidated paths * @api public */ -Document.prototype.invalidate = function(path, err, val) { +Document.prototype.invalidate = function(path, err, val, kind) { if (!this.$__.validationError) { this.$__.validationError = new ValidationError(this); } - if (this.$__.validationError.errors[path]) return; + if (this.$__.validationError.errors[path]) { + return; + } - if (!err || 'string' === typeof err) { + if (!err || typeof err === 'string') { err = new ValidatorError({ path: path, message: err, - type: 'user defined', + type: kind || 'user defined', value: val }); } - if (this.$__.validationError == err) return; + if (this.$__.validationError === err) { + return this.$__.validationError; + } - this.$__.validationError.errors[path] = err; + this.$__.validationError.addError(path, err); + return this.$__.validationError; }; /** * Marks a path as valid, removing existing validation errors. * * @param {String} path the field to mark as valid - * @api private + * @api public * @method $markValid * @receiver Document */ @@ -1296,6 +1724,44 @@ Document.prototype.$markValid = function(path) { } }; +/** + * Saves this document. + * + * ####Example: + * + * product.sold = Date.now(); + * product.save(function (err, product, numAffected) { + * if (err) .. + * }) + * + * The callback will receive three parameters + * + * 1. `err` if an error occurred + * 2. `product` which is the saved `product` + * 3. `numAffected` will be 1 when the document was successfully persisted to MongoDB, otherwise 0. Unless you tweak mongoose's internals, you don't need to worry about checking this parameter for errors - checking `err` is sufficient to make sure your document was properly saved. + * + * As an extra measure of flow control, save will return a Promise. + * ####Example: + * product.save().then(function(product) { + * ... + * }); + * + * For legacy reasons, mongoose stores object keys in reverse order on initial + * save. That is, `{ a: 1, b: 2 }` will be saved as `{ b: 2, a: 1 }` in + * MongoDB. To override this behavior, set + * [the `toObject.retainKeyOrder` option](http://mongoosejs.com/docs/api.html#document_Document-toObject) + * to true on your schema. + * + * @param {Object} [options] options optional options + * @param {Object} [options.safe] overrides [schema's safe option](http://mongoosejs.com//docs/guide.html#safe) + * @param {Boolean} [options.validateBeforeSave] set to false to save without validating. + * @param {Function} [fn] optional callback + * @method save + * @return {Promise} Promise + * @api public + * @see middleware http://mongoosejs.com/docs/middleware.html + */ + /** * Checks if a path is invalid * @@ -1319,12 +1785,12 @@ Document.prototype.$isValid = function(path) { */ Document.prototype.$__reset = function reset() { - var self = this; + var _this = this; DocumentArray || (DocumentArray = require('./types/documentarray')); this.$__.activePaths .map('init', 'modify', function(i) { - return self.getValue(i); + return _this.getValue(i); }) .filter(function(val) { return val && val instanceof Array && val.isMongooseDocumentArray && val.length; @@ -1333,11 +1799,24 @@ Document.prototype.$__reset = function reset() { var i = array.length; while (i--) { var doc = array[i]; - if (!doc) continue; + if (!doc) { + continue; + } doc.$__reset(); } }); + this.$__.activePaths. + map('init', 'modify', function(i) { + return _this.getValue(i); + }). + filter(function(val) { + return val && val.$isSingleNested; + }). + forEach(function(doc) { + doc.$__reset(); + }); + // clear atomics this.$__dirty().forEach(function(dirt) { var type = dirt.value; @@ -1351,9 +1830,9 @@ Document.prototype.$__reset = function reset() { this.$__.activePaths.clear('default'); this.$__.validationError = undefined; this.errors = undefined; - self = this; + _this = this; this.schema.requiredPaths().forEach(function(path) { - self.$__.activePaths.require(path); + _this.$__.activePaths.require(path); }); return this; @@ -1368,26 +1847,26 @@ Document.prototype.$__reset = function reset() { */ Document.prototype.$__dirty = function() { - var self = this; + var _this = this; var all = this.$__.activePaths.map('modify', function(path) { return { path: path, - value: self.getValue(path), - schema: self.$__path(path) + value: _this.getValue(path), + schema: _this.$__path(path) }; }); // gh-2558: if we had to set a default and the value is not undefined, // we have to save as well all = all.concat(this.$__.activePaths.map('default', function(path) { - if (path === '_id' || !self.getValue(path)) { + if (path === '_id' || !_this.getValue(path)) { return; } return { path: path, - value: self.getValue(path), - schema: self.$__path(path) + value: _this.getValue(path), + schema: _this.$__path(path) }; })); @@ -1401,7 +1880,7 @@ Document.prototype.$__dirty = function() { lastPath, top; - all.forEach(function(item, i) { + all.forEach(function(item) { if (!item) { return; } @@ -1425,115 +1904,6 @@ Document.prototype.$__dirty = function() { return minimal; }; -/*! - * Compiles schemas. - */ - -function compile(tree, proto, prefix, options) { - var keys = Object.keys(tree), - i = keys.length, - limb, - key; - - while (i--) { - key = keys[i]; - limb = tree[key]; - - defineKey(key - , (('Object' === utils.getFunctionName(limb.constructor) - && Object.keys(limb).length) - && (!limb[options.typeKey] || (options.typeKey === 'type' && limb.type.type)) - ? limb - : null) - , proto - , prefix - , keys - , options); - } -} - -// gets descriptors for all properties of `object` -// makes all properties non-enumerable to match previous behavior to #2211 -function getOwnPropertyDescriptors(object) { - var result = {}; - - Object.getOwnPropertyNames(object).forEach(function(key) { - result[key] = Object.getOwnPropertyDescriptor(object, key); - result[key].enumerable = true; - }); - - return result; -} - -/*! - * Defines the accessor named prop on the incoming prototype. - */ - -function defineKey(prop, subprops, prototype, prefix, keys, options) { - var path = (prefix ? prefix + '.' : '') + prop; - prefix = prefix || ''; - - if (subprops) { - - Object.defineProperty(prototype, prop, { - enumerable: true, - configurable: true, - get: function() { - var _self = this; - if (!this.$__.getters) - this.$__.getters = {}; - - if (!this.$__.getters[path]) { - var nested = Object.create(Object.getPrototypeOf(this), getOwnPropertyDescriptors(this)); - - // save scope for nested getters/setters - if (!prefix) nested.$__.scope = this; - - // shadow inherited getters from sub-objects so - // thing.nested.nested.nested... doesn't occur (gh-366) - var i = 0, - len = keys.length; - - for (; i < len; ++i) { - // over-write the parents getter without triggering it - Object.defineProperty(nested, keys[i], { - enumerable: false, // It doesn't show up. - writable: true, // We can set it later. - configurable: true, // We can Object.defineProperty again. - value: undefined // It shadows its parent. - }); - } - - nested.toObject = function() { - return _self.get(path); - }; - - nested.toJSON = nested.toObject; - - nested.$__isNested = true; - - compile(subprops, nested, path, options); - this.$__.getters[path] = nested; - } - - return this.$__.getters[path]; - }, - set: function(v) { - if (v instanceof Document) v = v.toObject(); - return (this.$__.scope || this).set(path, v); - } - }); - - } else { - Object.defineProperty(prototype, prop, { - enumerable: true, - configurable: true, - get: function() { return this.get.call(this.$__.scope || this, path); }, - set: function(v) { return this.set.call(this.$__.scope || this, path, v); } - }); - } -} - /** * Assigns/compiles `schema` into this documents prototype. * @@ -1544,6 +1914,7 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) { */ Document.prototype.$__setSchema = function(schema) { + schema.plugin(idGetter, { deduplicate: true }); compile(schema.tree, this, undefined, schema.options); this.schema = schema; }; @@ -1562,15 +1933,17 @@ Document.prototype.$__getArrayPathsToValidate = function() { // validate all document arrays. return this.$__.activePaths - .map('init', 'modify', function(i) { - return this.getValue(i); - }.bind(this)) - .filter(function(val) { - return val && val instanceof Array && val.isMongooseDocumentArray && val.length; - }).reduce(function(seed, array) { - return seed.concat(array); - }, []) - .filter(function(doc) { return doc;}); + .map('init', 'modify', function(i) { + return this.getValue(i); + }.bind(this)) + .filter(function(val) { + return val && val instanceof Array && val.isMongooseDocumentArray && val.length; + }).reduce(function(seed, array) { + return seed.concat(array); + }, []) + .filter(function(doc) { + return doc; + }); }; @@ -1586,147 +1959,66 @@ Document.prototype.$__getAllSubdocs = function() { DocumentArray || (DocumentArray = require('./types/documentarray')); Embedded = Embedded || require('./types/embedded'); - function docReducer(seed, path) { - var val = this[path]; + function docReducer(doc, seed, path) { + var val = doc[path]; - if (val instanceof Embedded) seed.push(val); + if (val instanceof Embedded) { + seed.push(val); + } if (val && val.$isSingleNested) { + seed = Object.keys(val._doc).reduce(function(seed, path) { + return docReducer(val._doc, seed, path); + }, seed); seed.push(val); } if (val && val.isMongooseDocumentArray) { val.forEach(function _docReduce(doc) { - if (!doc || !doc._doc) return; - if (doc instanceof Embedded) seed.push(doc); - seed = Object.keys(doc._doc).reduce(docReducer.bind(doc._doc), seed); + if (!doc || !doc._doc) { + return; + } + if (doc instanceof Embedded) { + seed.push(doc); + } + seed = Object.keys(doc._doc).reduce(function(seed, path) { + return docReducer(doc._doc, seed, path); + }, seed); }); } else if (val instanceof Document && val.$__isNested) { - val = val.toObject(); if (val) { - seed = Object.keys(val).reduce(docReducer.bind(val), seed); + seed = Object.keys(val).reduce(function(seed, path) { + return docReducer(val, seed, path); + }, seed); } } return seed; } - var subDocs = Object.keys(this._doc).reduce(docReducer.bind(this), []); + var _this = this; + var subDocs = Object.keys(this._doc).reduce(function(seed, path) { + return docReducer(_this, seed, path); + }, []); return subDocs; }; -/** - * Executes methods queued from the Schema definition - * - * @api private - * @method $__registerHooksFromSchema - * @memberOf Document +/*! + * Runs queued functions */ -Document.prototype.$__registerHooksFromSchema = function() { - Embedded = Embedded || require('./types/embedded'); - var Promise = PromiseProvider.get(); - - var self = this; - var q = self.schema && self.schema.callQueue; - if (!q.length) return self; +function applyQueue(doc) { + var q = doc.schema && doc.schema.callQueue; + if (!q.length) { + return; + } + var pair; - // we are only interested in 'pre' hooks, and group by point-cut - var toWrap = q.reduce(function(seed, pair) { + for (var i = 0; i < q.length; ++i) { + pair = q[i]; if (pair[0] !== 'pre' && pair[0] !== 'post' && pair[0] !== 'on') { - self[pair[0]].apply(self, pair[1]); - return seed; - } - var args = [].slice.call(pair[1]); - var pointCut = pair[0] === 'on' ? 'post' : args[0]; - if (!(pointCut in seed)) seed[pointCut] = { post: [], pre: [] }; - if (pair[0] === 'post') { - seed[pointCut].post.push(args); - } else if (pair[0] === 'on') { - seed[pointCut].push(args); - } else { - seed[pointCut].pre.push(args); - } - return seed; - }, {post: []}); - - // 'post' hooks are simpler - toWrap.post.forEach(function(args) { - self.on.apply(self, args); - }); - delete toWrap.post; - - // 'init' should be synchronous on subdocuments - if (toWrap.init && self instanceof Embedded) { - if (toWrap.init.pre) { - toWrap.init.pre.forEach(function(args) { - self.pre.apply(self, args); - }); - } - if (toWrap.init.post) { - toWrap.init.post.forEach(function(args) { - self.post.apply(self, args); - }); - } - delete toWrap.init; - } else if (toWrap.set) { - // Set hooks also need to be sync re: gh-3479 - if (toWrap.set.pre) { - toWrap.set.pre.forEach(function(args) { - self.pre.apply(self, args); - }); + doc[pair[0]].apply(doc, pair[1]); } - if (toWrap.set.post) { - toWrap.set.post.forEach(function(args) { - self.post.apply(self, args); - }); - } - delete toWrap.set; } - - Object.keys(toWrap).forEach(function(pointCut) { - // this is so we can wrap everything into a promise; - var newName = ('$__original_' + pointCut); - if (!self[pointCut]) { - return; - } - self[newName] = self[pointCut]; - self[pointCut] = function wrappedPointCut() { - var args = [].slice.call(arguments); - var lastArg = args.pop(); - var fn; - - return new Promise.ES6(function(resolve, reject) { - if (lastArg && typeof lastArg !== 'function') { - args.push(lastArg); - } else { - fn = lastArg; - } - args.push(function(error, result) { - if (error) { - self.$__handleReject(error); - fn && fn(error); - reject(error); - return; - } - - fn && fn.apply(null, [null].concat(Array.prototype.slice.call(arguments, 1))); - resolve(result); - }); - - self[newName].apply(self, args); - }); - }; - - toWrap[pointCut].pre.forEach(function(args) { - args[0] = newName; - self.pre.apply(self, args); - }); - toWrap[pointCut].post.forEach(function(args) { - args[0] = newName; - self.post.apply(self, args); - }); - }); - return self; -}; +} Document.prototype.$__handleReject = function handleReject(err) { // emit on the Model if listening @@ -1748,22 +2040,23 @@ Document.prototype.$__handleReject = function handleReject(err) { */ Document.prototype.$toObject = function(options, json) { - var defaultOptions = { transform: true, json: json }; + var defaultOptions = { + transform: true, + json: json, + retainKeyOrder: this.schema.options.retainKeyOrder, + flattenDecimals: true + }; - if (options && options.depopulate && !options._skipDepopulateTopLevel && this.$__.wasPopulated) { + // _isNested will only be true if this is not the top level document, we + // should never depopulate + if (options && options.depopulate && options._isNested && this.$__.wasPopulated) { // populated paths that we set to a document return clone(this._id, options); } - // If we're calling toObject on a populated doc, we may want to skip - // depopulated on the top level - if (options && options._skipDepopulateTopLevel) { - options._skipDepopulateTopLevel = false; - } - // When internally saving this document we always pass options, // bypassing the custom schema options. - if (!(options && 'Object' == utils.getFunctionName(options.constructor)) || + if (!(options && utils.getFunctionName(options.constructor) === 'Object') || (options && options._useSchemaOptions)) { if (json) { options = this.schema.options.toJSON ? @@ -1792,11 +2085,9 @@ Document.prototype.$toObject = function(options, json) { // to save it from being overwritten by sub-transform functions var originalTransform = options.transform; - var ret = clone(this._doc, options) || {}; + options._isNested = true; - if (options.virtuals || options.getters && false !== options.virtuals) { - applyGetters(this, ret, 'virtuals', options); - } + var ret = clone(this._doc, options) || {}; if (options.getters) { applyGetters(this, ret, 'paths', options); @@ -1807,6 +2098,10 @@ Document.prototype.$toObject = function(options, json) { } } + if (options.virtuals || options.getters && options.virtuals !== false) { + applyGetters(this, ret, 'virtuals', options); + } + if (options.versionKey === false && this.schema.options.versionKey) { delete ret[this.schema.options.versionKey]; } @@ -1818,9 +2113,8 @@ Document.prototype.$toObject = function(options, json) { // child schema has a transform (this.schema.options.toObject) In this case, // we need to adjust options.transform to be the child schema's transform and // not the parent schema's - if (true === transform || + if (transform === true || (this.schema.options.toObject && transform)) { - var opts = options.json ? this.schema.options.toJSON : this.schema.options.toObject; if (opts) { @@ -1830,9 +2124,11 @@ Document.prototype.$toObject = function(options, json) { options.transform = originalTransform; } - if ('function' == typeof transform) { + if (typeof transform === 'function') { var xformed = transform(this, ret, options); - if ('undefined' != typeof xformed) ret = xformed; + if (typeof xformed !== 'undefined') { + ret = xformed; + } } return ret; @@ -1890,6 +2186,7 @@ Document.prototype.$toObject = function(options, json) { * schema.options.toObject.transform = function (doc, ret, options) { * // remove the _id of every document before returning the result * delete ret._id; + * return ret; * } * * // without the transformation in the schema @@ -1922,7 +2219,7 @@ Document.prototype.$toObject = function(options, json) { * // pass the transform as an inline option * doc.toObject({ transform: xform }); // { inline: 'Wreck-it Ralph', custom: true } * - * _Note: if you call `toObject` and pass any options, the transform declared in your schema options will __not__ be applied. To force its application pass `transform: true`_ + * If you want to skip transformations, use `transform: false`: * * if (!schema.options.toObject) schema.options.toObject = {}; * schema.options.toObject.hide = '_id'; @@ -1932,11 +2229,12 @@ Document.prototype.$toObject = function(options, json) { * delete ret[prop]; * }); * } + * return ret; * } * * var doc = new Doc({ _id: 'anId', secret: 47, name: 'Wreck-it Ralph' }); * doc.toObject(); // { secret: 47, name: 'Wreck-it Ralph' } - * doc.toObject({ hide: 'secret _id' }); // { _id: 'anId', secret: 47, name: 'Wreck-it Ralph' } + * doc.toObject({ hide: 'secret _id', transform: false });// { _id: 'anId', secret: 47, name: 'Wreck-it Ralph' } * doc.toObject({ hide: 'secret _id', transform: true }); // { name: 'Wreck-it Ralph' } * * Transforms are applied _only to the document and are not applied to sub-documents_. @@ -1975,7 +2273,7 @@ function minimize(obj) { key = keys[i]; val = obj[key]; - if (utils.isObject(val)) { + if (utils.isObject(val) && !Buffer.isBuffer(val)) { obj[key] = minimize(val); } @@ -1988,8 +2286,8 @@ function minimize(obj) { } return hasKeys - ? obj - : undefined; + ? obj + : undefined; } /*! @@ -2002,27 +2300,62 @@ function minimize(obj) { */ function applyGetters(self, json, type, options) { - var schema = self.schema, - paths = Object.keys(schema[type]), - i = paths.length, - path; + var schema = self.schema; + var paths = Object.keys(schema[type]); + var i = paths.length; + var numPaths = i; + var path; + var cur = self._doc; + var v; + + if (!cur) { + return json; + } + + if (type === 'virtuals') { + for (i = 0; i < numPaths; ++i) { + path = paths[i]; + parts = path.split('.'); + v = clone(self.get(path), options); + if (v === void 0) { + continue; + } + plen = parts.length; + cur = json; + for (var j = 0; j < plen - 1; ++j) { + cur[parts[j]] = cur[parts[j]] || {}; + cur = cur[parts[j]]; + } + cur[parts[plen - 1]] = v; + } + + return json; + } while (i--) { path = paths[i]; - var parts = path.split('.'), - plen = parts.length, - last = plen - 1, - branch = json, - part; + var parts = path.split('.'); + var plen = parts.length; + var last = plen - 1; + var branch = json; + var part; + cur = self._doc; for (var ii = 0; ii < plen; ++ii) { part = parts[ii]; + v = cur[part]; if (ii === last) { branch[part] = clone(self.get(path), options); + } else if (v == null) { + if (part in cur) { + branch[part] = v; + } + break; } else { branch = branch[part] || (branch[part] = {}); } + cur = v; } } @@ -2055,10 +2388,15 @@ Document.prototype.toJSON = function(options) { */ Document.prototype.inspect = function(options) { - var opts = options && 'Object' == utils.getFunctionName(options.constructor) ? options : {}; - opts.minimize = false; - opts.retainKeyOrder = true; - return inspect(this.toObject(opts)); + var isPOJO = options && + utils.getFunctionName(options.constructor) === 'Object'; + var opts; + if (isPOJO) { + opts = options; + opts.minimize = false; + opts.retainKeyOrder = true; + } + return this.toObject(opts); }; /** @@ -2068,7 +2406,9 @@ Document.prototype.inspect = function(options) { * @method toString */ -Document.prototype.toString = Document.prototype.inspect; +Document.prototype.toString = function() { + return inspect(this.inspect()); +}; /** * Returns true if the Document stores the same data as doc. @@ -2083,18 +2423,24 @@ Document.prototype.toString = Document.prototype.inspect; */ Document.prototype.equals = function(doc) { + if (!doc) { + return false; + } + var tid = this.get('_id'); var docid = doc.get ? doc.get('_id') : doc; if (!tid && !docid) { return deepEqual(this, doc); } return tid && tid.equals - ? tid.equals(docid) - : tid === docid; + ? tid.equals(docid) + : tid === docid; }; /** * Populates document references, executing the `callback` when complete. + * If you want to use promises instead, use this function with + * [`execPopulate()`](#document_Document-execPopulate) * * ####Example: * @@ -2107,15 +2453,16 @@ Document.prototype.equals = function(doc) { * model: 'modelName' * options: opts * }, function (err, user) { - * assert(doc._id == user._id) // the document itself is passed + * assert(doc._id === user._id) // the document itself is passed * }) * * // summary - * doc.populate(path) // not executed - * doc.populate(options); // not executed - * doc.populate(path, callback) // executed - * doc.populate(options, callback); // executed - * doc.populate(callback); // executed + * doc.populate(path) // not executed + * doc.populate(options); // not executed + * doc.populate(path, callback) // executed + * doc.populate(options, callback); // executed + * doc.populate(callback); // executed + * doc.populate(options).execPopulate() // executed, returns promise * * * ####NOTE: @@ -2126,6 +2473,7 @@ Document.prototype.equals = function(doc) { * See [Model.populate()](#model_Model.populate) for explaination of options. * * @see Model.populate #model_Model.populate + * @see Document.execPopulate #document_Document-execPopulate * @param {String|Object} [path] The path to populate or an options object * @param {Function} [callback] When passed, population is invoked * @api public @@ -2133,13 +2481,15 @@ Document.prototype.equals = function(doc) { */ Document.prototype.populate = function populate() { - if (0 === arguments.length) return this; + if (arguments.length === 0) { + return this; + } var pop = this.$__.populate || (this.$__.populate = {}); var args = utils.args(arguments); var fn; - if ('function' == typeof args[args.length - 1]) { + if (typeof args[args.length - 1] === 'function') { fn = args.pop(); } @@ -2155,14 +2505,23 @@ Document.prototype.populate = function populate() { if (fn) { var paths = utils.object.vals(pop); this.$__.populate = undefined; - this.constructor.populate(this, paths, fn); + paths.__noPromise = true; + var topLevelModel = this.constructor; + if (this.$__isNested) { + topLevelModel = this.$__.scope.constructor; + var nestedPath = this.$__.nestedPath; + paths.forEach(function(populateOptions) { + populateOptions.path = nestedPath + '.' + populateOptions.path; + }); + } + topLevelModel.populate(this, paths, fn); } return this; }; /** - * Explicitly executes population and returns a promise. Useful for ES6 + * Explicitly executes population and returns a promise. Useful for ES2015 * integration. * * ####Example: @@ -2179,16 +2538,10 @@ Document.prototype.populate = function populate() { * execPopulate(); * * // summary - * doc.execPopulate() + * doc.execPopulate().then(resolve, reject); * * - * ####NOTE: - * - * Population does not occur unless a `callback` is passed. - * Passing the same path a second time will overwrite the previous path options. - * See [Model.populate()](#model_Model.populate) for explaination of options. - * - * @see Document.populate #Document_model.populate + * @see Document.populate #document_Document-populate * @api public * @return {Promise} promise that resolves to the document when population is done */ @@ -2197,11 +2550,12 @@ Document.prototype.execPopulate = function() { var Promise = PromiseProvider.get(); var _this = this; return new Promise.ES6(function(resolve, reject) { - _this.populate(function(error) { + _this.populate(function(error, res) { if (error) { - return reject(error); + reject(error); + } else { + resolve(res); } - resolve(_this); }); }); }; @@ -2226,22 +2580,28 @@ Document.prototype.execPopulate = function() { Document.prototype.populated = function(path, val, options) { // val and options are internal - if (val == null) { - if (!this.$__.populated) return undefined; + if (val === null || val === void 0) { + if (!this.$__.populated) { + return undefined; + } var v = this.$__.populated[path]; - if (v) return v.value; + if (v) { + return v.value; + } return undefined; } // internal - if (true === val) { - if (!this.$__.populated) return undefined; + if (val === true) { + if (!this.$__.populated) { + return undefined; + } return this.$__.populated[path]; } this.$__.populated || (this.$__.populated = {}); - this.$__.populated[path] = { value: val, options: options }; + this.$__.populated[path] = {value: val, options: options}; return val; }; @@ -2259,16 +2619,24 @@ Document.prototype.populated = function(path, val, options) { * If the path was not populated, this is a no-op. * * @param {String} path + * @return {Document} this + * @see Document.populate #document_Document-populate * @api public */ Document.prototype.depopulate = function(path) { - var populatedIds = this.populated(path); - if (!populatedIds) { - return; + if (typeof path === 'string') { + path = path.split(' '); } - delete this.$__.populated[path]; - this.set(path, populatedIds); + for (var i = 0; i < path.length; i++) { + var populatedIds = this.populated(path[i]); + if (!populatedIds) { + continue; + } + delete this.$__.populated[path[i]]; + this.$set(path[i], populatedIds); + } + return this; }; diff --git a/lib/document_provider.js b/lib/document_provider.js index f7ad8fac6c8..738b7d7cf56 100644 --- a/lib/document_provider.js +++ b/lib/document_provider.js @@ -1,20 +1,30 @@ 'use strict'; +/* eslint-env browser */ + /*! * Module dependencies. */ var Document = require('./document.js'); var BrowserDocument = require('./browserDocument.js'); +var isBrowser = false; + /** * Returns the Document constructor for the current context * * @api private */ module.exports = function() { - if (typeof window !== 'undefined' && typeof document !== 'undefined' && document === window.document) { + if (isBrowser) { return BrowserDocument; - } else { - return Document; } -}; \ No newline at end of file + return Document; +}; + +/*! + * ignore + */ +module.exports.setBrowser = function(flag) { + isBrowser = flag; +}; diff --git a/lib/document_provider.web.js b/lib/document_provider.web.js new file mode 100644 index 00000000000..4223fc39750 --- /dev/null +++ b/lib/document_provider.web.js @@ -0,0 +1,17 @@ +'use strict'; + +/* eslint-env browser */ + +/*! + * Module dependencies. + */ +var BrowserDocument = require('./browserDocument.js'); + +/** + * Returns the Document constructor for the current context + * + * @api private + */ +module.exports = function() { + return BrowserDocument; +}; diff --git a/lib/drivers/browser/decimal128.js b/lib/drivers/browser/decimal128.js new file mode 100644 index 00000000000..ecc1ae3b9be --- /dev/null +++ b/lib/drivers/browser/decimal128.js @@ -0,0 +1,5 @@ +/*! + * ignore + */ + +module.exports = require('bson').Decimal128; diff --git a/lib/drivers/browser/index.js b/lib/drivers/browser/index.js index fa5dbb422d0..cc794a79379 100644 --- a/lib/drivers/browser/index.js +++ b/lib/drivers/browser/index.js @@ -3,5 +3,6 @@ */ exports.Binary = require('./binary'); +exports.Decimal128 = require('./decimal128'); exports.ObjectId = require('./objectid'); exports.ReadPreference = require('./ReadPreference'); diff --git a/lib/drivers/index.js b/lib/drivers/index.js index 5a842baea3b..a6965824978 100644 --- a/lib/drivers/index.js +++ b/lib/drivers/index.js @@ -5,7 +5,10 @@ var driver; if (typeof window === 'undefined') { - driver = require(global.MONGOOSE_DRIVER_PATH || './node-mongodb-native'); + driver = require('./node-mongodb-native'); + if (global.MONGOOSE_DRIVER_PATH) { + driver = require(global.MONGOOSE_DRIVER_PATH); + } } else { driver = require('./browser'); } diff --git a/lib/drivers/index.web.js b/lib/drivers/index.web.js new file mode 100644 index 00000000000..f833eb223fd --- /dev/null +++ b/lib/drivers/index.web.js @@ -0,0 +1,5 @@ +/*! + * ignore + */ + +module.exports = require('./browser'); diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js index 815e8bd53b3..a5378a72e09 100644 --- a/lib/drivers/node-mongodb-native/collection.js +++ b/lib/drivers/node-mongodb-native/collection.js @@ -1,11 +1,10 @@ - /*! * Module dependencies. */ -var MongooseCollection = require('../../collection'), - Collection = require('mongodb').Collection, - utils = require('../../utils'); +var MongooseCollection = require('../../collection'); +var Collection = require('mongodb').Collection; +var utils = require('../../utils'); /** * A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) collection implementation. @@ -34,22 +33,23 @@ NativeCollection.prototype.__proto__ = MongooseCollection.prototype; */ NativeCollection.prototype.onOpen = function() { - var self = this; + var _this = this; // always get a new collection in case the user changed host:port // of parent db instance when re-opening the connection. - if (!self.opts.capped.size) { + if (!_this.opts.capped.size) { // non-capped - return self.conn.db.collection(self.name, callback); + callback(null, _this.conn.db.collection(_this.name)); + return _this.collection; } // capped - return self.conn.db.collection(self.name, function(err, c) { + return _this.conn.db.collection(_this.name, function(err, c) { if (err) return callback(err); // discover if this collection exists and if it is capped - self.conn.db.listCollections({ name: self.name }).toArray(function(err, docs) { + _this.conn.db.listCollections({name: _this.name}).toArray(function(err, docs) { if (err) { return callback(err); } @@ -60,18 +60,18 @@ NativeCollection.prototype.onOpen = function() { if (doc.options && doc.options.capped) { callback(null, c); } else { - var msg = 'A non-capped collection exists with the name: ' + self.name + '\n\n' - + ' To use this collection as a capped collection, please ' - + 'first convert it.\n' - + ' http://www.mongodb.org/display/DOCS/Capped+Collections#CappedCollections-Convertingacollectiontocapped'; + var msg = 'A non-capped collection exists with the name: ' + _this.name + '\n\n' + + ' To use this collection as a capped collection, please ' + + 'first convert it.\n' + + ' http://www.mongodb.org/display/DOCS/Capped+Collections#CappedCollections-Convertingacollectiontocapped'; err = new Error(msg); callback(err); } } else { // create - var opts = utils.clone(self.opts.capped); + var opts = utils.clone(_this.opts.capped); opts.capped = true; - self.conn.db.createCollection(self.name, opts, callback); + _this.conn.db.createCollection(_this.name, opts, callback); } }); }); @@ -79,10 +79,10 @@ NativeCollection.prototype.onOpen = function() { function callback(err, collection) { if (err) { // likely a strict mode error - self.conn.emit('error', err); + _this.conn.emit('error', err); } else { - self.collection = collection; - MongooseCollection.prototype.onOpen.call(self); + _this.collection = collection; + MongooseCollection.prototype.onOpen.call(_this); } } }; @@ -93,14 +93,54 @@ NativeCollection.prototype.onOpen = function() { * @api private */ -NativeCollection.prototype.onClose = function() { - MongooseCollection.prototype.onClose.call(this); +NativeCollection.prototype.onClose = function(force) { + MongooseCollection.prototype.onClose.call(this, force); }; /*! * Copy the collection methods and make them subject to queues */ +function iter(i) { + NativeCollection.prototype[i] = function() { + // If user force closed, queueing will hang forever. See #5664 + if (this.opts.$wasForceClosed) { + return this.conn.db.collection(this.name)[i].apply(collection, args); + } + if (this.buffer) { + this.addQueue(i, arguments); + return; + } + + var collection = this.collection; + var args = arguments; + var _this = this; + var debug = _this.conn.base.options.debug; + + if (debug) { + if (typeof debug === 'function') { + debug.apply(_this, + [_this.name, i].concat(utils.args(args, 0, args.length - 1))); + } else { + this.$print(_this.name, i, args); + } + } + + try { + return collection[i].apply(collection, args); + } catch (error) { + // Collection operation may throw because of max bson size, catch it here + // See gh-3906 + if (args.length > 0 && + typeof args[args.length - 1] === 'function') { + args[args.length - 1](error); + } else { + throw error; + } + } + }; +} + for (var i in Collection.prototype) { // Janky hack to work around gh-3005 until we can get rid of the mongoose // collection abstraction @@ -112,58 +152,40 @@ for (var i in Collection.prototype) { continue; } - (function(i) { - NativeCollection.prototype[i] = function() { - if (this.buffer) { - this.addQueue(i, arguments); - return; - } - - var collection = this.collection, - args = arguments, - self = this, - debug = self.conn.base.options.debug; - - if (debug) { - if ('function' === typeof debug) { - debug.apply(debug - , [self.name, i].concat(utils.args(args, 0, args.length - 1))); - } else { - this.$print(self.name, i, args); - } - } - - return collection[i].apply(collection, args); - }; - })(i); + iter(i); } /** * Debug print helper * * @api public + * @method $print */ NativeCollection.prototype.$print = function(name, i, args) { - console.error( - '\x1B[0;36mMongoose:\x1B[0m %s.%s(%s) %s %s %s', - name, - i, - this.$format(args[0]), - this.$format(args[1]), - this.$format(args[2]), - this.$format(args[3])); + var moduleName = '\x1B[0;36mMongoose:\x1B[0m '; + var functionCall = [name, i].join('.'); + var _args = []; + for (var j = args.length - 1; j >= 0; --j) { + if (this.$format(args[j]) || _args.length) { + _args.unshift(this.$format(args[j])); + } + } + var params = '(' + _args.join(', ') + ')'; + + console.error(moduleName + functionCall + params); }; /** * Formatter for debug print args * * @api public + * @method $format */ NativeCollection.prototype.$format = function(arg) { var type = typeof arg; - if ('function' === type || 'undefined' === type) return ''; + if (type === 'function' || type === 'undefined') return ''; return format(arg); }; @@ -171,43 +193,54 @@ NativeCollection.prototype.$format = function(arg) { * Debug print helper */ +function map(o) { + return format(o, true); +} +function formatObjectId(x, key) { + var representation = 'ObjectId("' + x[key].toHexString() + '")'; + x[key] = {inspect: function() { return representation; }}; +} +function formatDate(x, key) { + var representation = 'new Date("' + x[key].toUTCString() + '")'; + x[key] = {inspect: function() { return representation; }}; +} function format(obj, sub) { - var x = utils.clone(obj, { retainKeyOrder: 1 }); + if (obj && typeof obj.toBSON === 'function') { + obj = obj.toBSON(); + } + var x = utils.clone(obj, {retainKeyOrder: 1, transform: false}); var representation; - if (x) { - if ('Binary' === x.constructor.name) { - x = '[object Buffer]'; - } else if ('ObjectID' === x.constructor.name) { + + if (x != null) { + if (x.constructor.name === 'Binary') { + x = 'BinData(' + x.sub_type + ', "' + x.toString('base64') + '")'; + } else if (x.constructor.name === 'ObjectID') { representation = 'ObjectId("' + x.toHexString() + '")'; - x = { inspect: function() { return representation; } }; - } else if ('Date' === x.constructor.name) { + x = {inspect: function() { return representation; }}; + } else if (x.constructor.name === 'Date') { representation = 'new Date("' + x.toUTCString() + '")'; - x = { inspect: function() { return representation; } }; - } else if ('Object' === x.constructor.name) { + x = {inspect: function() { return representation; }}; + } else if (x.constructor.name === 'Object') { var keys = Object.keys(x); var numKeys = keys.length; var key; for (var i = 0; i < numKeys; ++i) { key = keys[i]; if (x[key]) { - if ('Binary' === x[key].constructor.name) { - x[key] = '[object Buffer]'; - } else if ('Object' === x[key].constructor.name) { + if (typeof x[key].toBSON === 'function') { + x[key] = x[key].toBSON(); + } + if (x[key].constructor.name === 'Binary') { + x[key] = 'BinData(' + x[key].sub_type + ', "' + + x[key].buffer.toString('base64') + '")'; + } else if (x[key].constructor.name === 'Object') { x[key] = format(x[key], true); - } else if ('ObjectID' === x[key].constructor.name) { - (function(x) { - var representation = 'ObjectId("' + x[key].toHexString() + '")'; - x[key] = { inspect: function() { return representation; } }; - })(x); - } else if ('Date' === x[key].constructor.name) { - (function(x) { - var representation = 'new Date("' + x[key].toUTCString() + '")'; - x[key] = { inspect: function() { return representation; } }; - })(x); + } else if (x[key].constructor.name === 'ObjectID') { + formatObjectId(x, key); + } else if (x[key].constructor.name === 'Date') { + formatDate(x, key); } else if (Array.isArray(x[key])) { - x[key] = x[key].map(function(o) { - return format(o, true); - }); + x[key] = x[key].map(map); } } } @@ -216,9 +249,9 @@ function format(obj, sub) { } return require('util') - .inspect(x, false, 10, true) - .replace(/\n/g, '') - .replace(/\s{2,}/g, ' '); + .inspect(x, false, 10, true) + .replace(/\n/g, '') + .replace(/\s{2,}/g, ' '); } /** diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 617045543ec..b92d8254716 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -2,13 +2,14 @@ * Module dependencies. */ -var MongooseConnection = require('../../connection'), - mongo = require('mongodb'), - Db = mongo.Db, - Server = mongo.Server, - Mongos = mongo.Mongos, - STATES = require('../../connectionstate'), - ReplSetServers = mongo.ReplSet; +var MongooseConnection = require('../../connection'); +var mongo = require('mongodb'); +var Db = mongo.Db; +var Server = mongo.Server; +var Mongos = mongo.Mongos; +var STATES = require('../../connectionstate'); +var ReplSetServers = mongo.ReplSet; +var DisconnectedError = require('../../error/disconnected'); /** * A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation. @@ -44,13 +45,29 @@ NativeConnection.prototype.__proto__ = MongooseConnection.prototype; */ NativeConnection.prototype.doOpen = function(fn) { + var _this = this; var server = new Server(this.host, this.port, this.options.server); - this.db = new Db(this.name, server, this.options.db); - var self = this; + if (this.options && this.options.mongos) { + var mongos = new Mongos([server], this.options.mongos); + this.db = new Db(this.name, mongos, this.options.db); + } else { + this.db = new Db(this.name, server, this.options.db); + } + this.db.open(function(err) { + listen(_this); + + if (!mongos) { + server.s.server.on('error', function(error) { + if (/after \d+ attempts/.test(error.message)) { + _this.emit('error', new DisconnectedError(server.s.server.name)); + } + }); + } + if (err) return fn(err); - listen(self); + fn(); }); @@ -93,7 +110,7 @@ NativeConnection.prototype.useDb = function(name) { // the 'open' event of the connection before doing the rest of the setup // the 'connected' event is the first time we'll have access to the db object - var self = this; + var _this = this; if (this.db && this._readyState === STATES.connected) { wireup(); @@ -102,7 +119,7 @@ NativeConnection.prototype.useDb = function(name) { } function wireup() { - newConn.db = self.db.db(name); + newConn.db = _this.db.db(name); newConn.onOpen(); // setup the events appropriately listen(newConn); @@ -122,10 +139,12 @@ NativeConnection.prototype.useDb = function(name) { */ function listen(conn) { - if (conn._listening) return; - conn._listening = true; + if (conn.db._listening) { + return; + } + conn.db._listening = true; - conn.db.on('close', function() { + conn.db.on('close', function(force) { if (conn._closeCalled) return; // the driver never emits an `open` event. auto_reconnect still @@ -136,22 +155,24 @@ function listen(conn) { conn.emit('close'); return; } - conn.onClose(); + conn.onClose(force); }); conn.db.on('error', function(err) { conn.emit('error', err); }); conn.db.on('reconnect', function() { conn.readyState = STATES.connected; + conn.emit('reconnect'); conn.emit('reconnected'); + conn.onOpen(); }); conn.db.on('timeout', function(err) { - var error = new Error(err && err.err || 'connection timeout'); - conn.emit('error', error); + conn.emit('timeout', err); }); conn.db.on('open', function(err, db) { if (STATES.disconnected === conn.readyState && db && db.databaseName) { conn.readyState = STATES.connected; + conn.emit('reconnect'); conn.emit('reconnected'); } }); @@ -172,27 +193,39 @@ function listen(conn) { NativeConnection.prototype.doOpenSet = function(fn) { var servers = [], - self = this; + _this = this; this.hosts.forEach(function(server) { var host = server.host || server.ipc; var port = server.port || 27017; - servers.push(new Server(host, port, self.options.server)); + servers.push(new Server(host, port, _this.options.server)); }); var server = this.options.mongos ? new Mongos(servers, this.options.mongos) - : new ReplSetServers(servers, this.options.replset); + : new ReplSetServers(servers, this.options.replset || this.options.replSet); this.db = new Db(this.name, server, this.options.db); + this.db.s.topology.on('left', function(data) { + _this.emit('left', data); + }); + + this.db.s.topology.on('joined', function(data) { + _this.emit('joined', data); + }); + this.db.on('fullsetup', function() { - self.emit('fullsetup'); + _this.emit('fullsetup'); + }); + + this.db.on('all', function() { + _this.emit('all'); }); this.db.open(function(err) { if (err) return fn(err); fn(); - listen(self); + listen(_this); }); return this; @@ -201,14 +234,14 @@ NativeConnection.prototype.doOpenSet = function(fn) { /** * Closes the connection * - * @param {Function} fn + * @param {Boolean} [force] + * @param {Function} [fn] * @return {Connection} this * @api private */ -NativeConnection.prototype.doClose = function(fn) { - this.db.close(); - if (fn) fn(); +NativeConnection.prototype.doClose = function(force, fn) { + this.db.close(force, fn); return this; }; @@ -223,68 +256,80 @@ NativeConnection.prototype.doClose = function(fn) { */ NativeConnection.prototype.parseOptions = function(passed, connStrOpts) { - var o = passed || {}; + var o = passed ? require('../../utils').clone(passed) : {}; + o.db || (o.db = {}); o.auth || (o.auth = {}); o.server || (o.server = {}); - o.replset || (o.replset = {}); + o.replset || (o.replset = o.replSet) || (o.replset = {}); o.server.socketOptions || (o.server.socketOptions = {}); o.replset.socketOptions || (o.replset.socketOptions = {}); + o.mongos || (o.mongos = (connStrOpts && connStrOpts.mongos)); + (o.mongos === true) && (o.mongos = {}); var opts = connStrOpts || {}; Object.keys(opts).forEach(function(name) { switch (name) { case 'ssl': + o.server.ssl = opts.ssl; + o.replset.ssl = opts.ssl; + o.mongos && (o.mongos.ssl = opts.ssl); + break; case 'poolSize': - if ('undefined' == typeof o.server[name]) { + if (typeof o.server[name] === 'undefined') { o.server[name] = o.replset[name] = opts[name]; } break; case 'slaveOk': - if ('undefined' == typeof o.server.slave_ok) { + if (typeof o.server.slave_ok === 'undefined') { o.server.slave_ok = opts[name]; } break; case 'autoReconnect': - if ('undefined' == typeof o.server.auto_reconnect) { + if (typeof o.server.auto_reconnect === 'undefined') { o.server.auto_reconnect = opts[name]; } break; case 'socketTimeoutMS': case 'connectTimeoutMS': - if ('undefined' == typeof o.server.socketOptions[name]) { + if (typeof o.server.socketOptions[name] === 'undefined') { o.server.socketOptions[name] = o.replset.socketOptions[name] = opts[name]; } break; case 'authdb': - if ('undefined' == typeof o.auth.authdb) { + if (typeof o.auth.authdb === 'undefined') { o.auth.authdb = opts[name]; } break; case 'authSource': - if ('undefined' == typeof o.auth.authSource) { + if (typeof o.auth.authSource === 'undefined') { o.auth.authSource = opts[name]; } break; + case 'authMechanism': + if (typeof o.auth.authMechanism === 'undefined') { + o.auth.authMechanism = opts[name]; + } + break; case 'retries': case 'reconnectWait': case 'rs_name': - if ('undefined' == typeof o.replset[name]) { + if (typeof o.replset[name] === 'undefined') { o.replset[name] = opts[name]; } break; case 'replicaSet': - if ('undefined' == typeof o.replset.rs_name) { + if (typeof o.replset.rs_name === 'undefined') { o.replset.rs_name = opts[name]; } break; case 'readSecondary': - if ('undefined' == typeof o.replset.read_secondary) { + if (typeof o.replset.read_secondary === 'undefined') { o.replset.read_secondary = opts[name]; } break; case 'nativeParser': - if ('undefined' == typeof o.db.native_parser) { + if (typeof o.db.native_parser === 'undefined') { o.db.native_parser = opts[name]; } break; @@ -293,20 +338,24 @@ NativeConnection.prototype.parseOptions = function(passed, connStrOpts) { case 'fsync': case 'journal': case 'wtimeoutMS': - if ('undefined' == typeof o.db[name]) { + if (typeof o.db[name] === 'undefined') { o.db[name] = opts[name]; } break; case 'readPreference': - if ('undefined' == typeof o.db.read_preference) { - o.db.read_preference = opts[name]; + if (typeof o.db.readPreference === 'undefined') { + o.db.readPreference = opts[name]; } break; case 'readPreferenceTags': - if ('undefined' == typeof o.db.read_preference_tags) { + if (typeof o.db.read_preference_tags === 'undefined') { o.db.read_preference_tags = opts[name]; } break; + case 'sslValidate': + o.server.sslValidate = opts.sslValidate; + o.replset.sslValidate = opts.sslValidate; + o.mongos && (o.mongos.sslValidate = opts.sslValidate); } }); @@ -314,11 +363,6 @@ NativeConnection.prototype.parseOptions = function(passed, connStrOpts) { o.server.auto_reconnect = true; } - if (!o.db.read_preference) { - // read from primaries by default - o.db.read_preference = 'primary'; - } - // mongoose creates its own ObjectIds o.db.forceServerObjectId = false; @@ -328,6 +372,10 @@ NativeConnection.prototype.parseOptions = function(passed, connStrOpts) { o.db.w = 1; } + if (o.promiseLibrary) { + o.db.promiseLibrary = o.promiseLibrary; + } + validate(o); return o; }; @@ -339,7 +387,7 @@ NativeConnection.prototype.parseOptions = function(passed, connStrOpts) { */ function validate(o) { - if (-1 === o.db.w || 0 === o.db.w) { + if (o.db.w === -1 || o.db.w === 0) { if (o.db.journal || o.db.fsync || o.db.safe) { throw new Error( 'Invalid writeConcern: ' diff --git a/lib/drivers/node-mongodb-native/decimal128.js b/lib/drivers/node-mongodb-native/decimal128.js new file mode 100644 index 00000000000..d1f04de9658 --- /dev/null +++ b/lib/drivers/node-mongodb-native/decimal128.js @@ -0,0 +1,5 @@ +/*! + * ignore + */ + +module.exports = require('mongodb').Decimal128; diff --git a/lib/drivers/node-mongodb-native/index.js b/lib/drivers/node-mongodb-native/index.js index fa5dbb422d0..cc794a79379 100644 --- a/lib/drivers/node-mongodb-native/index.js +++ b/lib/drivers/node-mongodb-native/index.js @@ -3,5 +3,6 @@ */ exports.Binary = require('./binary'); +exports.Decimal128 = require('./decimal128'); exports.ObjectId = require('./objectid'); exports.ReadPreference = require('./ReadPreference'); diff --git a/lib/error.js b/lib/error.js deleted file mode 100644 index d08980eab19..00000000000 --- a/lib/error.js +++ /dev/null @@ -1,51 +0,0 @@ - -/** - * MongooseError constructor - * - * @param {String} msg Error message - * @inherits Error https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error - */ - -function MongooseError(msg) { - Error.call(this); - this.stack = new Error().stack; - this.message = msg; - this.name = 'MongooseError'; -} - -/*! - * Inherits from Error. - */ - -MongooseError.prototype = Object.create(Error.prototype); -MongooseError.prototype.constructor = Error; - -/*! - * Module exports. - */ - -module.exports = exports = MongooseError; - -/** - * The default built-in validator error messages. - * - * @see Error.messages #error_messages_MongooseError-messages - * @api public - */ - -MongooseError.messages = require('./error/messages'); - -// backward compat -MongooseError.Messages = MongooseError.messages; - -/*! - * Expose subclasses - */ - -MongooseError.CastError = require('./error/cast'); -MongooseError.ValidationError = require('./error/validation'); -MongooseError.ValidatorError = require('./error/validator'); -MongooseError.VersionError = require('./error/version'); -MongooseError.OverwriteModelError = require('./error/overwriteModel'); -MongooseError.MissingSchemaError = require('./error/missingSchema'); -MongooseError.DivergentArrayError = require('./error/divergentArray'); diff --git a/lib/error/browserMissingSchema.js b/lib/error/browserMissingSchema.js index 8a67f1ae2ea..24140f58a59 100644 --- a/lib/error/browserMissingSchema.js +++ b/lib/error/browserMissingSchema.js @@ -1,9 +1,8 @@ -/* eslint no-unused-vars: 1 */ /*! * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /*! * MissingSchema Error constructor. @@ -11,12 +10,16 @@ var MongooseError = require('../error.js'); * @inherits MongooseError */ -function MissingSchemaError(name) { +function MissingSchemaError() { var msg = 'Schema hasn\'t been registered for document.\n' + 'Use mongoose.Document(name, schema)'; MongooseError.call(this, msg); - Error.captureStackTrace && Error.captureStackTrace(this, arguments.callee); this.name = 'MissingSchemaError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } } /*! diff --git a/lib/error/cast.js b/lib/error/cast.js index 65da189232a..9bd9bc91160 100644 --- a/lib/error/cast.js +++ b/lib/error/cast.js @@ -2,7 +2,8 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); +var util = require('util'); /** * Casting Error constructor. @@ -14,9 +15,20 @@ var MongooseError = require('../error.js'); */ function CastError(type, value, path, reason) { - MongooseError.call(this, 'Cast to ' + type + ' failed for value "' + value + '" at path "' + path + '"'); - this.stack = new Error().stack; + var stringValue = util.inspect(value); + stringValue = stringValue.replace(/^'/, '"').replace(/'$/, '"'); + if (stringValue.charAt(0) !== '"') { + stringValue = '"' + stringValue + '"'; + } + MongooseError.call(this, 'Cast to ' + type + ' failed for value ' + + stringValue + ' at path "' + path + '"'); this.name = 'CastError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } + this.stringValue = stringValue; this.kind = type; this.value = value; this.path = path; @@ -30,6 +42,16 @@ function CastError(type, value, path, reason) { CastError.prototype = Object.create(MongooseError.prototype); CastError.prototype.constructor = MongooseError; +/*! + * ignore + */ + +CastError.prototype.setModel = function(model) { + this.model = model; + this.message = 'Cast to ' + this.kind + ' failed for value ' + + this.stringValue + ' at path "' + this.path + '"' + ' for model "' + + model.modelName + '"'; +}; /*! * exports diff --git a/lib/error/disconnected.js b/lib/error/disconnected.js new file mode 100644 index 00000000000..e17f71ce0e6 --- /dev/null +++ b/lib/error/disconnected.js @@ -0,0 +1,40 @@ +/*! + * Module dependencies. + */ + +var MongooseError = require('./'); + +/** + * Casting Error constructor. + * + * @param {String} type + * @param {String} value + * @inherits MongooseError + * @api private + */ + +function DisconnectedError(connectionString) { + MongooseError.call(this, 'Ran out of retries trying to reconnect to "' + + connectionString + '". Try setting `server.reconnectTries` and ' + + '`server.reconnectInterval` to something higher.'); + this.name = 'DisconnectedError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } +} + +/*! + * Inherits from MongooseError. + */ + +DisconnectedError.prototype = Object.create(MongooseError.prototype); +DisconnectedError.prototype.constructor = MongooseError; + + +/*! + * exports + */ + +module.exports = DisconnectedError; diff --git a/lib/error/divergentArray.js b/lib/error/divergentArray.js index 1cbaa25f95e..de949cb0206 100644 --- a/lib/error/divergentArray.js +++ b/lib/error/divergentArray.js @@ -3,7 +3,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /*! * DivergentArrayError constructor. @@ -23,8 +23,12 @@ function DivergentArrayError(paths) { // TODO write up a docs page (FAQ) and link to it MongooseError.call(this, msg); - Error.captureStackTrace && Error.captureStackTrace(this, arguments.callee); this.name = 'DivergentArrayError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } } /*! diff --git a/lib/error/index.js b/lib/error/index.js new file mode 100644 index 00000000000..2dbf4963880 --- /dev/null +++ b/lib/error/index.js @@ -0,0 +1,66 @@ + +/** + * MongooseError constructor + * + * @param {String} msg Error message + * @inherits Error https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error + */ + +function MongooseError(msg) { + Error.call(this); + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } + this.message = msg; + this.name = 'MongooseError'; +} + +/*! + * Inherits from Error. + */ + +MongooseError.prototype = Object.create(Error.prototype); +MongooseError.prototype.constructor = Error; + +/*! + * Module exports. + */ + +module.exports = exports = MongooseError; + +/** + * The default built-in validator error messages. + * + * @see Error.messages #error_messages_MongooseError-messages + * @api public + */ + +MongooseError.messages = require('./messages'); + +// backward compat +MongooseError.Messages = MongooseError.messages; + +/** + * This error will be called when `save()` fails because the underlying + * document was not found. The constructor takes one parameter, the + * conditions that mongoose passed to `update()` when trying to update + * the document. + * + * @api public + */ + +MongooseError.DocumentNotFoundError = require('./notFound'); + +/*! + * Expose subclasses + */ + +MongooseError.CastError = require('./cast'); +MongooseError.ValidationError = require('./validation'); +MongooseError.ValidatorError = require('./validator'); +MongooseError.VersionError = require('./version'); +MongooseError.OverwriteModelError = require('./overwriteModel'); +MongooseError.MissingSchemaError = require('./missingSchema'); +MongooseError.DivergentArrayError = require('./divergentArray'); diff --git a/lib/error/messages.js b/lib/error/messages.js index 75fc2355c83..56950eb3cd1 100644 --- a/lib/error/messages.js +++ b/lib/error/messages.js @@ -16,28 +16,29 @@ * * Click the "show code" link below to see all defaults. * - * @property messages + * @static messages * @receiver MongooseError * @api public */ var msg = module.exports = exports = {}; +msg.DocumentNotFoundError = null; + msg.general = {}; -msg.general.default = "Validator failed for path `{PATH}` with value `{VALUE}`"; -msg.general.required = "Path `{PATH}` is required."; +msg.general.default = 'Validator failed for path `{PATH}` with value `{VALUE}`'; +msg.general.required = 'Path `{PATH}` is required.'; msg.Number = {}; -msg.Number.min = "Path `{PATH}` ({VALUE}) is less than minimum allowed value ({MIN})."; -msg.Number.max = "Path `{PATH}` ({VALUE}) is more than maximum allowed value ({MAX})."; +msg.Number.min = 'Path `{PATH}` ({VALUE}) is less than minimum allowed value ({MIN}).'; +msg.Number.max = 'Path `{PATH}` ({VALUE}) is more than maximum allowed value ({MAX}).'; msg.Date = {}; -msg.Date.min = "Path `{PATH}` ({VALUE}) is before minimum allowed value ({MIN})."; -msg.Date.max = "Path `{PATH}` ({VALUE}) is after maximum allowed value ({MAX})."; +msg.Date.min = 'Path `{PATH}` ({VALUE}) is before minimum allowed value ({MIN}).'; +msg.Date.max = 'Path `{PATH}` ({VALUE}) is after maximum allowed value ({MAX}).'; msg.String = {}; -msg.String.enum = "`{VALUE}` is not a valid enum value for path `{PATH}`."; -msg.String.match = "Path `{PATH}` is invalid ({VALUE})."; -msg.String.minlength = "Path `{PATH}` (`{VALUE}`) is shorter than the minimum allowed length ({MINLENGTH})."; -msg.String.maxlength = "Path `{PATH}` (`{VALUE}`) is longer than the maximum allowed length ({MAXLENGTH})."; - +msg.String.enum = '`{VALUE}` is not a valid enum value for path `{PATH}`.'; +msg.String.match = 'Path `{PATH}` is invalid ({VALUE}).'; +msg.String.minlength = 'Path `{PATH}` (`{VALUE}`) is shorter than the minimum allowed length ({MINLENGTH}).'; +msg.String.maxlength = 'Path `{PATH}` (`{VALUE}`) is longer than the maximum allowed length ({MAXLENGTH}).'; diff --git a/lib/error/missingSchema.js b/lib/error/missingSchema.js index 25eabfa5289..dacfb1c7abe 100644 --- a/lib/error/missingSchema.js +++ b/lib/error/missingSchema.js @@ -3,7 +3,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /*! * MissingSchema Error constructor. @@ -15,8 +15,12 @@ function MissingSchemaError(name) { var msg = 'Schema hasn\'t been registered for model "' + name + '".\n' + 'Use mongoose.model(name, schema)'; MongooseError.call(this, msg); - Error.captureStackTrace && Error.captureStackTrace(this, arguments.callee); this.name = 'MissingSchemaError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } } /*! diff --git a/lib/error/notFound.js b/lib/error/notFound.js new file mode 100644 index 00000000000..fecbd8c584a --- /dev/null +++ b/lib/error/notFound.js @@ -0,0 +1,50 @@ +'use strict'; + +/*! + * Module dependencies. + */ + +var MongooseError = require('./'); +var util = require('util'); + +/*! + * OverwriteModel Error constructor. + * + * @inherits MongooseError + */ + +function DocumentNotFoundError(query) { + var msg; + var messages = MongooseError.messages; + if (messages.DocumentNotFoundError != null) { + msg = typeof messages.DocumentNotFoundError === 'function' ? + messages.DocumentNotFoundError(query) : + messages.DocumentNotFoundError; + } else { + msg = 'No document found for query "' + util.inspect(query) + '"'; + } + + MongooseError.call(this, msg); + + this.name = 'DocumentNotFoundError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } + + this.query = query; +} + +/*! + * Inherits from MongooseError. + */ + +DocumentNotFoundError.prototype = Object.create(MongooseError.prototype); +DocumentNotFoundError.prototype.constructor = MongooseError; + +/*! + * exports + */ + +module.exports = DocumentNotFoundError; diff --git a/lib/error/objectExpected.js b/lib/error/objectExpected.js new file mode 100644 index 00000000000..5a64c9debee --- /dev/null +++ b/lib/error/objectExpected.js @@ -0,0 +1,35 @@ +/*! + * Module dependencies. + */ + +var MongooseError = require('./'); + +/** + * Strict mode error constructor + * + * @param {String} type + * @param {String} value + * @inherits MongooseError + * @api private + */ + +function ObjectExpectedError(path, val) { + MongooseError.call(this, 'Tried to set nested object field `' + path + + '` to primitive value `' + val + '` and strict mode is set to throw.'); + this.name = 'ObjectExpectedError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } + this.path = path; +} + +/*! + * Inherits from MongooseError. + */ + +ObjectExpectedError.prototype = Object.create(MongooseError.prototype); +ObjectExpectedError.prototype.constructor = MongooseError; + +module.exports = ObjectExpectedError; diff --git a/lib/error/objectParameter.js b/lib/error/objectParameter.js new file mode 100644 index 00000000000..450771b671d --- /dev/null +++ b/lib/error/objectParameter.js @@ -0,0 +1,36 @@ +/*! + * Module dependencies. + */ + +var MongooseError = require('./'); + +/** + * Constructor for errors that happen when a parameter that's expected to be + * an object isn't an object + * + * @param {Any} value + * @param {String} paramName + * @param {String} fnName + * @inherits MongooseError + * @api private + */ + +function ObjectParameterError(value, paramName, fnName) { + MongooseError.call(this, 'Parameter "' + paramName + '" to ' + fnName + + '() must be an object, got ' + value.toString()); + this.name = 'ObjectParameterError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } +} + +/*! + * Inherits from MongooseError. + */ + +ObjectParameterError.prototype = Object.create(MongooseError.prototype); +ObjectParameterError.prototype.constructor = MongooseError; + +module.exports = ObjectParameterError; diff --git a/lib/error/overwriteModel.js b/lib/error/overwriteModel.js index c14ae7ffc36..27ce7fe7069 100644 --- a/lib/error/overwriteModel.js +++ b/lib/error/overwriteModel.js @@ -3,7 +3,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /*! * OverwriteModel Error constructor. @@ -13,8 +13,12 @@ var MongooseError = require('../error.js'); function OverwriteModelError(name) { MongooseError.call(this, 'Cannot overwrite `' + name + '` model once compiled.'); - Error.captureStackTrace && Error.captureStackTrace(this, arguments.callee); this.name = 'OverwriteModelError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } } /*! diff --git a/lib/error/strict.js b/lib/error/strict.js new file mode 100644 index 00000000000..2d0c108fda9 --- /dev/null +++ b/lib/error/strict.js @@ -0,0 +1,36 @@ +/*! + * Module dependencies. + */ + +var MongooseError = require('./'); + +/** + * Strict mode error constructor + * + * @param {String} type + * @param {String} value + * @inherits MongooseError + * @api private + */ + +function StrictModeError(path, msg) { + msg = msg || 'Field `' + path + '` is not in schema and strict ' + + 'mode is set to throw.'; + MongooseError.call(this, msg); + this.name = 'StrictModeError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } + this.path = path; +} + +/*! + * Inherits from MongooseError. + */ + +StrictModeError.prototype = Object.create(MongooseError.prototype); +StrictModeError.prototype.constructor = MongooseError; + +module.exports = StrictModeError; diff --git a/lib/error/validation.js b/lib/error/validation.js index b574455ac95..2202d82a97b 100644 --- a/lib/error/validation.js +++ b/lib/error/validation.js @@ -1,9 +1,9 @@ - /*! * Module requirements */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); +var utils = require('../utils'); /** * Document Validation Error @@ -14,14 +14,21 @@ var MongooseError = require('../error.js'); */ function ValidationError(instance) { + this.errors = {}; + this._message = ''; if (instance && instance.constructor.name === 'model') { - MongooseError.call(this, instance.constructor.modelName + " validation failed"); + this._message = instance.constructor.modelName + ' validation failed'; + MongooseError.call(this, this._message); } else { - MongooseError.call(this, "Validation failed"); + this._message = 'Validation failed'; + MongooseError.call(this, this._message); } - this.stack = new Error().stack; this.name = 'ValidationError'; - this.errors = {}; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } if (instance) { instance.errors = this.errors; } @@ -34,23 +41,60 @@ function ValidationError(instance) { ValidationError.prototype = Object.create(MongooseError.prototype); ValidationError.prototype.constructor = MongooseError; - /** * Console.log helper */ ValidationError.prototype.toString = function() { - var ret = this.name + ': '; - var msgs = []; + return this.name + ': ' + _generateMessage(this); +}; - Object.keys(this.errors).forEach(function(key) { - if (this == this.errors[key]) return; - msgs.push(String(this.errors[key])); - }, this); +/*! + * inspect helper + */ + +ValidationError.prototype.inspect = function() { + return utils.assign(new Error(this.message), this); +}; + +/*! + * Helper for JSON.stringify + */ - return ret + msgs.join(', '); +ValidationError.prototype.toJSON = function() { + return utils.assign({}, this, { message: this.message }); }; +/*! + * add message + */ + +ValidationError.prototype.addError = function(path, error) { + this.errors[path] = error; + this.message = this._message + ': ' + _generateMessage(this); +}; + +/*! + * ignore + */ + +function _generateMessage(err) { + var keys = Object.keys(err.errors || {}); + var len = keys.length; + var msgs = []; + var key; + + for (var i = 0; i < len; ++i) { + key = keys[i]; + if (err === err.errors[key]) { + continue; + } + msgs.push(key + ': ' + err.errors[key].message); + } + + return msgs.join(', '); +} + /*! * Module exports */ diff --git a/lib/error/validator.js b/lib/error/validator.js index 3f1c67f9126..d934b524a14 100644 --- a/lib/error/validator.js +++ b/lib/error/validator.js @@ -2,8 +2,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); -var errorMessages = MongooseError.messages; +var MongooseError = require('./'); /** * Schema validator error @@ -16,17 +15,22 @@ var errorMessages = MongooseError.messages; function ValidatorError(properties) { var msg = properties.message; if (!msg) { - msg = errorMessages.general.default; + msg = MongooseError.messages.general.default; } - this.properties = properties; var message = this.formatMessage(msg, properties); MongooseError.call(this, message); - this.stack = new Error().stack; this.name = 'ValidatorError'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } else { + this.stack = new Error().stack; + } + this.properties = properties; this.kind = properties.type; this.path = properties.path; this.value = properties.value; + this.reason = properties.reason; } /*! @@ -36,6 +40,17 @@ function ValidatorError(properties) { ValidatorError.prototype = Object.create(MongooseError.prototype); ValidatorError.prototype.constructor = MongooseError; +/*! + * The object used to define this validator. Not enumerable to hide + * it from `require('util').inspect()` output re: gh-3925 + */ + +Object.defineProperty(ValidatorError.prototype, 'properties', { + enumerable: false, + writable: true, + value: null +}); + /*! * Formats error messages */ diff --git a/lib/error/version.js b/lib/error/version.js index 815b1bc31da..eed37fd5888 100644 --- a/lib/error/version.js +++ b/lib/error/version.js @@ -1,9 +1,10 @@ +'use strict'; /*! * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /** * Version Error constructor. @@ -12,10 +13,13 @@ var MongooseError = require('../error.js'); * @api private */ -function VersionError() { - MongooseError.call(this, 'No matching document found.'); - Error.captureStackTrace && Error.captureStackTrace(this, arguments.callee); +function VersionError(doc, currentVersion, modifiedPaths) { + var modifiedPathsStr = modifiedPaths.join(', '); + MongooseError.call(this, 'No matching document found for id "' + doc._id + + '" version ' + currentVersion + ' modifiedPaths "' + modifiedPathsStr + '"'); this.name = 'VersionError'; + this.version = currentVersion; + this.modifiedPaths = modifiedPaths; } /*! diff --git a/lib/index.js b/lib/index.js index b3b219c0812..5fbbdc00578 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,22 +4,25 @@ * Module dependencies. */ -var Schema = require('./schema'), - SchemaType = require('./schematype'), - VirtualType = require('./virtualtype'), - STATES = require('./connectionstate'), - Types = require('./types'), - Query = require('./query'), - Model = require('./model'), - Document = require('./document'), - utils = require('./utils'), - format = utils.toCollectionName, - pkg = require('../package.json'); +var Schema = require('./schema'); +var SchemaType = require('./schematype'); +var VirtualType = require('./virtualtype'); +var STATES = require('./connectionstate'); +var Types = require('./types'); +var Query = require('./query'); +var Model = require('./model'); +var Document = require('./document'); +var utils = require('./utils'); +var format = utils.toCollectionName; +var pkg = require('../package.json'); var querystring = require('querystring'); +var saveSubdocs = require('./plugins/saveSubdocs'); +var validateBeforeSave = require('./plugins/validateBeforeSave'); var Aggregate = require('./aggregate'); var PromiseProvider = require('./promise_provider'); +var shardingPlugin = require('./plugins/sharding'); /** * Mongoose constructor. @@ -32,7 +35,6 @@ var PromiseProvider = require('./promise_provider'); function Mongoose() { this.connections = []; - this.plugins = []; this.models = {}; this.modelSchemas = {}; // default global options @@ -41,6 +43,17 @@ function Mongoose() { }; var conn = this.createConnection(); // default connection conn.models = this.models; + + Object.defineProperty(this, 'plugins', { + configurable: false, + enumerable: true, + writable: false, + value: [ + [saveSubdocs, { deduplicate: true }], + [validateBeforeSave, { deduplicate: true }], + [shardingPlugin, { deduplicate: true }] + ] + }); } /** @@ -58,19 +71,22 @@ Mongoose.prototype.STATES = STATES; * * mongoose.set('debug', true) // enable logging collection methods + arguments to the console * + * mongoose.set('debug', function(collectionName, methodName, arg1, arg2...) {}); // use custom function to log collection methods + arguments + * * @param {String} key - * @param {String} value + * @param {String|Function|Boolean} value * @api public */ Mongoose.prototype.set = function(key, value) { - if (arguments.length == 1) { + if (arguments.length === 1) { return this.options[key]; } this.options[key] = value; return this; }; +Mongoose.prototype.set.$hasSideEffects = true; /** * Gets mongoose options @@ -163,34 +179,46 @@ var checkReplicaSetInUri = function(uri) { * * @param {String} [uri] a mongodb:// URI * @param {Object} [options] options to pass to the driver + * @param {Object} [options.config] mongoose-specific options + * @param {Boolean} [options.config.autoIndex] set to false to disable automatic index creation for all models associated with this connection. + * @param {Boolean} [options.useMongoClient] false by default, set to true to use new mongoose connection logic * @see Connection#open #connection_Connection-open * @see Connection#openSet #connection_Connection-openSet - * @return {Connection} the created Connection object + * @return {Connection|Promise} the created Connection object, or promise that resolves to the connection if `useMongoClient` option specified. * @api public */ -Mongoose.prototype.createConnection = function(uri, options) { +Mongoose.prototype.createConnection = function(uri, options, callback) { var conn = new Connection(this); this.connections.push(conn); + var rsOption = options && (options.replset || options.replSet); + + if (options && options.useMongoClient) { + return conn.openUri(uri, options, callback); + } + if (arguments.length) { if (rgxReplSet.test(arguments[0]) || checkReplicaSetInUri(arguments[0])) { - conn.openSet.apply(conn, arguments); - } else if (options && options.replset && - (options.replset.replicaSet || options.replset.rs_name)) { - conn.openSet.apply(conn, arguments); + conn._openSetWithoutPromise.apply(conn, arguments); + } else if (rsOption && + (rsOption.replicaSet || rsOption.rs_name)) { + conn._openSetWithoutPromise.apply(conn, arguments); } else { - conn.open.apply(conn, arguments); + conn._openWithoutPromise.apply(conn, arguments); } } return conn; }; +Mongoose.prototype.createConnection.$hasSideEffects = true; /** * Opens the default mongoose connection. * - * If arguments are passed, they are proxied to either [Connection#open](#connection_Connection-open) or [Connection#openSet](#connection_Connection-openSet) appropriately. + * If arguments are passed, they are proxied to either + * [Connection#open](#connection_Connection-open) or + * [Connection#openSet](#connection_Connection-openSet) appropriately. * * _Options passed take precedence over options included in connection strings._ * @@ -199,7 +227,7 @@ Mongoose.prototype.createConnection = function(uri, options) { * mongoose.connect('mongodb://user:pass@localhost:port/database'); * * // replica sets - * var uri = 'mongodb://user:pass@localhost:port/database,mongodb://anotherhost:port,mongodb://yetanother:port'; + * var uri = 'mongodb://user:pass@localhost:port,anotherhost:port,yetanother:port/mydatabase'; * mongoose.connect(uri); * * // with options @@ -210,54 +238,72 @@ Mongoose.prototype.createConnection = function(uri, options) { * var opts = { mongos: true }; * mongoose.connect(uri, opts); * + * // optional callback that gets fired when initial connection completed + * var uri = 'mongodb://nonexistent.domain:27000'; + * mongoose.connect(uri, function(error) { + * // if error is truthy, the initial connection failed. + * }) + * * @param {String} uri(s) * @param {Object} [options] + * @param {Boolean} [options.useMongoClient] false by default, set to true to use new mongoose connection logic * @param {Function} [callback] * @see Mongoose#createConnection #index_Mongoose-createConnection * @api public - * @return {Mongoose} this + * @return {MongooseThenable} pseudo-promise wrapper around this */ Mongoose.prototype.connect = function() { var conn = this.connection; - + if ((arguments.length === 2 || arguments.length === 3) && + typeof arguments[0] === 'string' && + typeof arguments[1] === 'object' && + arguments[1].useMongoClient === true) { + return conn.openUri(arguments[0], arguments[1], arguments[2]); + } if (rgxReplSet.test(arguments[0]) || checkReplicaSetInUri(arguments[0])) { - conn.openSet.apply(conn, arguments); - } else { - conn.open.apply(conn, arguments); + return new MongooseThenable(this, conn.openSet.apply(conn, arguments)); } - return this; + return new MongooseThenable(this, conn.open.apply(conn, arguments)); }; +Mongoose.prototype.connect.$hasSideEffects = true; /** * Disconnects all connections. * * @param {Function} [fn] called after all connection close. - * @return {Mongoose} this + * @return {MongooseThenable} pseudo-promise wrapper around this * @api public */ Mongoose.prototype.disconnect = function(fn) { - var count = this.connections.length, - error; - - this.connections.forEach(function(conn) { - conn.close(function(err) { - if (error) return; - - if (err) { - error = err; - if (fn) return fn(err); - throw err; - } - - if (fn) - --count || fn(); + var _this = this; + + var Promise = PromiseProvider.get(); + return new MongooseThenable(this, new Promise.ES6(function(resolve, reject) { + var remaining = _this.connections.length; + if (remaining <= 0) { + fn && fn(); + resolve(); + return; + } + _this.connections.forEach(function(conn) { + conn.close(function(error) { + if (error) { + fn && fn(error); + reject(error); + return; + } + if (!--remaining) { + fn && fn(); + resolve(); + } + }); }); - }); - return this; + })); }; +Mongoose.prototype.disconnect.$hasSideEffects = true; /** * Defines a model or retrieves it. @@ -292,24 +338,37 @@ Mongoose.prototype.disconnect = function(fn) { * var collectionName = 'actor' * var M = mongoose.model('Actor', schema, collectionName) * - * @param {String} name model name + * @param {String|Function} name model name or class extending Model * @param {Schema} [schema] - * @param {String} [collection] name (optional, induced from model name) + * @param {String} [collection] name (optional, inferred from model name) * @param {Boolean} [skipInit] whether to skip initialization (defaults to false) * @api public */ Mongoose.prototype.model = function(name, schema, collection, skipInit) { - if ('string' == typeof schema) { + var model; + if (typeof name === 'function') { + model = name; + name = model.name; + if (!(model.prototype instanceof Model)) { + throw new mongoose.Error('The provided class ' + name + ' must extend Model'); + } + } + + if (typeof schema === 'string') { collection = schema; schema = false; } - if (utils.isObject(schema) && !(schema instanceof Schema)) { + if (utils.isObject(schema) && !(schema.instanceOfSchema)) { schema = new Schema(schema); } + if (schema && !schema.instanceOfSchema) { + throw new Error('The 2nd parameter to `mongoose.model()` should be a ' + + 'schema or a POJO'); + } - if ('boolean' === typeof collection) { + if (typeof collection === 'boolean') { skipInit = collection; collection = null; } @@ -328,19 +387,21 @@ Mongoose.prototype.model = function(name, schema, collection, skipInit) { if (schema) { // cache it so we only apply plugins once this.modelSchemas[name] = schema; - this._applyPlugins(schema); } else { throw new mongoose.Error.MissingSchemaError(name); } } - var model; + if (schema) { + this._applyPlugins(schema); + } + var sub; // connection.model() may be passing a different schema for // an existing model name. in this case don't read from cache. - if (this.models[name] && false !== options.cache) { - if (schema instanceof Schema && schema != this.models[name].schema) { + if (this.models[name] && options.cache !== false) { + if (schema && schema.instanceOfSchema && schema !== this.models[name].schema) { throw new mongoose.Error.OverwriteModelError(name); } @@ -373,18 +434,20 @@ Mongoose.prototype.model = function(name, schema, collection, skipInit) { } var connection = options.connection || this.connection; - model = Model.compile(name, schema, collection, connection, this); + model = this.Model.compile(model || name, schema, collection, connection, this); if (!skipInit) { model.init(); } - if (false === options.cache) { + if (options.cache === false) { return model; } - return this.models[name] = model; + this.models[name] = model; + return this.models[name]; }; +Mongoose.prototype.model.$hasSideEffects = true; /** * Returns an array of model names created on this instance of Mongoose. @@ -401,6 +464,7 @@ Mongoose.prototype.modelNames = function() { var names = Object.keys(this.models); return names; }; +Mongoose.prototype.modelNames.$hasSideEffects = true; /** * Applies global plugins to `schema`. @@ -410,10 +474,20 @@ Mongoose.prototype.modelNames = function() { */ Mongoose.prototype._applyPlugins = function(schema) { - for (var i = 0, l = this.plugins.length; i < l; i++) { + if (schema.$globalPluginsApplied) { + return; + } + var i; + var len; + for (i = 0, len = this.plugins.length; i < len; ++i) { schema.plugin(this.plugins[i][0], this.plugins[i][1]); } + schema.$globalPluginsApplied = true; + for (i = 0, len = schema.childSchemas.length; i < len; ++i) { + this._applyPlugins(schema.childSchemas[i].schema); + } }; +Mongoose.prototype._applyPlugins.$hasSideEffects = true; /** * Declares a global plugin executed on all Schemas. @@ -431,6 +505,7 @@ Mongoose.prototype.plugin = function(fn, opts) { this.plugins.push([fn, opts]); return this; }; +Mongoose.prototype.plugin.$hasSideEffects = true; /** * The default connection of the mongoose module. @@ -452,6 +527,13 @@ Mongoose.prototype.__defineGetter__('connection', function() { return this.connections[0]; }); +Mongoose.prototype.__defineSetter__('connection', function(v) { + if (v instanceof Connection) { + this.connections[0] = v; + this.models = v.models; + } +}); + /*! * Driver depentend APIs */ @@ -621,6 +703,19 @@ Object.defineProperty(Mongoose.prototype, 'Promise', { } }); +/** + * Returns the current ES6-style promise constructor. In Mongoose 4.x, + * equivalent to `mongoose.Promise.ES6`, but will change once we get rid + * of the `.ES6` bit. + * + * @method Promise + * @api public + */ + +Mongoose.prototype.getPromiseConstructor = function() { + return PromiseProvider.get().ES6; +}; + /** * Storage layer for mongoose promises * @@ -666,6 +761,19 @@ Mongoose.prototype.DocumentProvider = require('./document_provider'); Mongoose.prototype.Error = require('./error'); +/** + * The Mongoose CastError constructor + * + * @method CastError + * @param {String} type The name of the type + * @param {Any} value The value that failed to cast + * @param {String} path The path `a.b.c` in the doc where this cast error occurred + * @param {Error} [reason] The original error that was thrown + * @api public + */ + +Mongoose.prototype.CastError = require('./error/cast'); + /** * The [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) driver Mongoose uses. * @@ -684,6 +792,68 @@ Mongoose.prototype.mongo = require('mongodb'); Mongoose.prototype.mquery = require('mquery'); +/** + * Wraps the given Mongoose instance into a thenable (pseudo-promise). This + * is so `connect()` and `disconnect()` can return a thenable while maintaining + * backwards compatibility. + * + * @api private + */ + +function MongooseThenable(mongoose, promise) { + var _this = this; + for (var key in mongoose) { + if (typeof mongoose[key] === 'function' && mongoose[key].$hasSideEffects) { + (function(key) { + _this[key] = function() { + return mongoose[key].apply(mongoose, arguments); + }; + })(key); + } else if (['connection', 'connections'].indexOf(key) !== -1) { + _this[key] = mongoose[key]; + } + } + this.$opPromise = promise; +} + +MongooseThenable.prototype = new Mongoose; + +/** + * Ability to use mongoose object as a pseudo-promise so `.connect().then()` + * and `.disconnect().then()` are viable. + * + * @param {Function} onFulfilled + * @param {Function} onRejected + * @return {Promise} + * @api private + */ + +MongooseThenable.prototype.then = function(onFulfilled, onRejected) { + var Promise = PromiseProvider.get(); + if (!this.$opPromise) { + return new Promise.ES6(function(resolve, reject) { + reject(new Error('Can only call `.then()` if connect() or disconnect() ' + + 'has been called')); + }).then(onFulfilled, onRejected); + } + this.$opPromise.$hasHandler = true; + return this.$opPromise.then(onFulfilled, onRejected); +}; + +/** + * Ability to use mongoose object as a pseudo-promise so `.connect().then()` + * and `.disconnect().then()` are viable. + * + * @param {Function} onFulfilled + * @param {Function} onRejected + * @return {Promise} + * @api private + */ + +MongooseThenable.prototype.catch = function(onRejected) { + return this.then(null, onRejected); +}; + /*! * The exports object is an instance of Mongoose. * diff --git a/lib/internal.js b/lib/internal.js index edf3338a268..df7d5aa8557 100644 --- a/lib/internal.js +++ b/lib/internal.js @@ -24,6 +24,7 @@ function InternalCache() { this.wasPopulated = false; // if this doc was the result of a population this.scope = undefined; this.activePaths = new ActiveRoster; + this.pathsToScopes = {}; // embedded docs this.ownerDocument = undefined; diff --git a/lib/model.js b/lib/model.js index 7bb047fa989..e5fc3a3d2dc 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1,27 +1,33 @@ -/* eslint no-unused-vars: 1 */ - /*! * Module dependencies. */ +var Aggregate = require('./aggregate'); var Document = require('./document'); -var MongooseError = require('./error'); -var VersionError = MongooseError.VersionError; -var DivergentArrayError = MongooseError.DivergentArrayError; +var DocumentNotFoundError = require('./error').DocumentNotFoundError; +var DivergentArrayError = require('./error').DivergentArrayError; +var Error = require('./error'); +var EventEmitter = require('events').EventEmitter; +var OverwriteModelError = require('./error').OverwriteModelError; +var PromiseProvider = require('./promise_provider'); var Query = require('./query'); -var Aggregate = require('./aggregate'); var Schema = require('./schema'); -var Types = require('./schema/index'); -var utils = require('./utils'); -var hasOwnProperty = utils.object.hasOwnProperty; -var isMongooseObject = utils.isMongooseObject; -var EventEmitter = require('events').EventEmitter; -var Promise = require('./promise'); +var VersionError = require('./error').VersionError; +var applyHooks = require('./services/model/applyHooks'); +var applyMethods = require('./services/model/applyMethods'); +var applyStatics = require('./services/model/applyStatics'); +var cast = require('./cast'); +var castUpdate = require('./services/query/castUpdate'); +var discriminator = require('./services/model/discriminator'); +var isPathSelectedInclusive = require('./services/projection/isPathSelectedInclusive'); +var get = require('lodash.get'); +var getSchemaTypes = require('./services/populate/getSchemaTypes'); +var mpath = require('mpath'); +var parallel = require('async/parallel'); +var parallelLimit = require('async/parallelLimit'); +var setDefaultsOnInsert = require('./services/setDefaultsOnInsert'); var util = require('util'); -var tick = utils.tick; - -var async = require('async'); -var PromiseProvider = require('./promise_provider'); +var utils = require('./utils'); var VERSION_WHERE = 1, VERSION_INC = 2, @@ -33,8 +39,8 @@ var VERSION_WHERE = 1, * Provides the interface to MongoDB collections as well as creates document instances. * * @param {Object} doc values with which to create the document - * @inherits Document - * @event `error`: If listening to this event, it is emitted when a document was saved without passing a callback and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model. + * @inherits Document http://mongoosejs.com/docs/api.html#document-js + * @event `error`: If listening to this event, 'error' is emitted when a document was saved without passing a callback and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model. * @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event. * @event `index-single-start`: Emitted when an individual index starts within `Model#ensureIndexes`. The fields and options being used to build the index are also passed with the event. * @event `index-single-done`: Emitted when an individual index finishes within `Model#ensureIndexes`. If an error occurred it is passed with the event. The fields, options, and index name are also passed. @@ -42,7 +48,12 @@ var VERSION_WHERE = 1, */ function Model(doc, fields, skipId) { - Document.call(this, doc, fields, skipId); + if (fields instanceof Schema) { + throw new TypeError('2nd argument to `Model` must be a POJO or string, ' + + '**not** a schema. Make sure you\'re calling `mongoose.model()`, not ' + + '`mongoose.Model()`.'); + } + Document.call(this, doc, fields, skipId, true); } /*! @@ -53,6 +64,7 @@ function Model(doc, fields, skipId) { */ Model.prototype.__proto__ = Document.prototype; +Model.prototype.$isMongooseModelPrototype = true; /** * Connection the model uses. @@ -81,6 +93,16 @@ Model.prototype.collection; Model.prototype.modelName; +/** + * Additional properties to attach to the query when calling `save()` and + * `isNew` is false. + * + * @api public + * @property $where + */ + +Model.prototype.$where; + /** * If this is a discriminator model, `baseModelName` is the name of * the base model. @@ -93,28 +115,30 @@ Model.prototype.baseModelName; Model.prototype.$__handleSave = function(options, callback) { var _this = this; + var i; + var keys; + var len; if (!options.safe && this.schema.options.safe) { options.safe = this.schema.options.safe; } if (typeof options.safe === 'boolean') { options.safe = null; } + var safe = options.safe ? utils.clone(options.safe, { retainKeyOrder: true }) : options.safe; if (this.isNew) { // send entire doc var toObjectOptions = {}; - if (this.schema.options.toObject && - this.schema.options.toObject.retainKeyOrder) { - toObjectOptions.retainKeyOrder = true; - } + toObjectOptions.retainKeyOrder = this.schema.options.retainKeyOrder; toObjectOptions.depopulate = 1; toObjectOptions._skipDepopulateTopLevel = true; toObjectOptions.transform = false; + toObjectOptions.flattenDecimals = false; var obj = this.toObject(toObjectOptions); - if (!utils.object.hasOwnProperty(obj || {}, '_id')) { + if ((obj || {})._id === void 0) { // documents must have an _id else mongoose won't know // what to update later if more changes are made. the user // wouldn't know what _id was generated by mongodb either @@ -127,10 +151,11 @@ Model.prototype.$__handleSave = function(options, callback) { } this.$__version(true, obj); - this.collection.insert(obj, options.safe, function(err, ret) { + this.collection.insert(obj, safe, function(err, ret) { if (err) { _this.isNew = true; _this.emit('isNew', true); + _this.constructor.emit('isNew', true); callback(err); return; @@ -141,6 +166,7 @@ Model.prototype.$__handleSave = function(options, callback) { this.$__reset(); this.isNew = false; this.emit('isNew', false); + this.constructor.emit('isNew', false); // Make it possible to retry the insert this.$__.inserting = true; } else { @@ -155,18 +181,27 @@ Model.prototype.$__handleSave = function(options, callback) { callback(delta); return; } - var where = this.$__where(delta[0]); + var where = this.$__where(delta[0]); if (where instanceof Error) { callback(where); return; } - this.collection.update(where, delta[1], options.safe, function(err, ret) { + if (this.$where) { + keys = Object.keys(this.$where); + len = keys.length; + for (i = 0; i < len; ++i) { + where[keys[i]] = this.$where[keys[i]]; + } + } + + this.collection.update(where, delta[1], safe, function(err, ret) { if (err) { callback(err); return; } + ret.$where = where; callback(null, ret); }); } else { @@ -176,6 +211,7 @@ Model.prototype.$__handleSave = function(options, callback) { } this.emit('isNew', false); + this.constructor.emit('isNew', false); } }; @@ -188,11 +224,15 @@ Model.prototype.$__save = function(options, callback) { _this.$__handleSave(options, function(error, result) { if (error) { - return callback(error); + return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + callback(error); + }); } + // store the modified paths before the document is reset + var modifiedPaths = _this.modifiedPaths(); + _this.$__reset(); - _this.$__storeShard(); var numAffected = 0; if (result) { @@ -207,26 +247,37 @@ Model.prototype.$__save = function(options, callback) { } } + if (_this.schema.options && + _this.schema.options.saveErrorIfNotFound && + numAffected <= 0) { + error = new DocumentNotFoundError(result.$where); + return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + callback(error); + }); + } + // was this an update that required a version bump? if (_this.$__.version && !_this.$__.inserting) { var doIncrement = VERSION_INC === (VERSION_INC & _this.$__.version); _this.$__.version = undefined; + var key = _this.schema.options.versionKey; + var version = _this.getValue(key) || 0; + if (numAffected <= 0) { // the update failed. pass an error back - var err = new VersionError(); + var err = new VersionError(_this, version, modifiedPaths); return callback(err); } // increment version if was successful if (doIncrement) { - var key = _this.schema.options.versionKey; - var version = _this.getValue(key) | 0; _this.setValue(key, version + 1); } } _this.emit('save', _this, numAffected); + _this.constructor.emit('save', _this, numAffected); callback(null, _this, numAffected); }); }; @@ -269,7 +320,7 @@ Model.prototype.$__save = function(options, callback) { */ Model.prototype.save = function(options, fn) { - if ('function' == typeof options) { + if (typeof options === 'function') { fn = options; options = undefined; } @@ -278,27 +329,14 @@ Model.prototype.save = function(options, fn) { options = {}; } - if (options.__noPromise) { - return this.$__save(options, fn); + if (fn) { + fn = this.constructor.$wrapCallback(fn); } - var _this = this; - - return new Promise.ES6(function(resolve, reject) { - _this.$__save(options, function(error, doc, numAffected) { - if (error) { - fn && fn(error); - reject(error); - return; - } - - fn && fn(null, doc, numAffected); - resolve(doc, numAffected); - }); - }); + return this.$__save(options, fn); }; -/** +/*! * Determines whether versioning should be skipped for the given path * * @param {Document} self @@ -334,7 +372,7 @@ function operand(self, where, delta, data, val, op) { delta[op][data.path] = val; // disabled versioning? - if (false === self.schema.options.versionKey) return; + if (self.schema.options.versionKey === false) return; // path excluded from versioning? if (shouldSkipVersioning(self, data.path)) return; @@ -362,19 +400,16 @@ function operand(self, where, delta, data, val, op) { // only increment the version if an array position changes. // modifying elements of an array is ok if position does not change. - if ('$push' == op || '$pushAll' == op || '$addToSet' == op) { + if (op === '$push' || op === '$pushAll' || op === '$addToSet') { self.$__.version = VERSION_INC; - } - else if (/^\$p/.test(op)) { + } else if (/^\$p/.test(op)) { // potentially changing array positions self.increment(); - } - else if (Array.isArray(val)) { + } else if (Array.isArray(val)) { // $set an array self.increment(); - } - // now handling $set, $unset - else if (/\.\d+\.|\.\d+$/.test(data.path)) { + } else if (/\.\d+\.|\.\d+$/.test(data.path)) { + // now handling $set, $unset // subpath of array self.$__.version = VERSION_WHERE; } @@ -396,10 +431,21 @@ function handleAtomics(self, where, delta, data, value) { return; } - if ('function' == typeof value.$__getAtomics) { + if (typeof value.$__getAtomics === 'function') { value.$__getAtomics().forEach(function(atomic) { var op = atomic[0]; var val = atomic[1]; + var usePushEach = false; + if ('usePushEach' in get(self, 'constructor.base.options', {})) { + usePushEach = self.constructor.base.get('usePushEach'); + } + if ('usePushEach' in self.schema.options) { + usePushEach = self.schema.options.usePushEach; + } + if (usePushEach && op === '$pushAll') { + op = '$push'; + val = { $each: val }; + } operand(self, where, delta, data, val, op); }); return; @@ -413,11 +459,11 @@ function handleAtomics(self, where, delta, data, value) { val, op; - if (0 === i) { + if (i === 0) { // $set - if (isMongooseObject(value)) { - value = value.toObject({ depopulate: 1 }); + if (utils.isMongooseObject(value)) { + value = value.toObject({depopulate: 1, _isNested: true}); } else if (value.valueOf) { value = value.valueOf(); } @@ -425,24 +471,27 @@ function handleAtomics(self, where, delta, data, value) { return operand(self, where, delta, data, value); } + function iter(mem) { + return utils.isMongooseObject(mem) + ? mem.toObject({depopulate: 1, _isNested: true}) + : mem; + } + while (i--) { op = ops[i]; val = atomics[op]; - if (isMongooseObject(val)) { - val = val.toObject({ depopulate: 1 }); + if (utils.isMongooseObject(val)) { + val = val.toObject({depopulate: true, transform: false, _isNested: true}); } else if (Array.isArray(val)) { - val = val.map(function(mem) { - return isMongooseObject(mem) - ? mem.toObject({ depopulate: 1 }) - : mem; - }); + val = val.map(iter); } else if (val.valueOf) { val = val.valueOf(); } - if ('$addToSet' === op) - val = { $each: val }; + if (op === '$addToSet') { + val = {$each: val}; + } operand(self, where, delta, data, val, op); } @@ -458,7 +507,7 @@ function handleAtomics(self, where, delta, data, value) { Model.prototype.$__delta = function() { var dirty = this.$__dirty(); - if (!dirty.length && VERSION_ALL != this.$__.version) return; + if (!dirty.length && VERSION_ALL !== this.$__.version) return; var where = {}, delta = {}, @@ -467,6 +516,9 @@ Model.prototype.$__delta = function() { d = 0; where._id = this._doc._id; + if (where._id.toObject) { + where._id = where._id.toObject({ transform: false, depopulate: true }); + } for (; d < len; ++d) { var data = dirty[d]; @@ -478,25 +530,48 @@ Model.prototype.$__delta = function() { continue; } + var pop = this.populated(data.path, true); + if (!pop && this.$__.selected) { + // If any array was selected using an $elemMatch projection, we alter the path and where clause + // NOTE: MongoDB only supports projected $elemMatch on top level array. + var pathSplit = data.path.split('.'); + var top = pathSplit[0]; + if (this.$__.selected[top] && this.$__.selected[top].$elemMatch) { + // If the selected array entry was modified + if (pathSplit.length > 1 && pathSplit[1] == 0 && typeof where[top] === 'undefined') { + where[top] = this.$__.selected[top]; + pathSplit[1] = '$'; + data.path = pathSplit.join('.'); + } + // if the selected array was modified in any other way throw an error + else { + divergent.push(data.path); + continue; + } + } + } + if (divergent.length) continue; if (undefined === value) { operand(this, where, delta, data, 1, '$unset'); - - } else if (null === value) { + } else if (value === null) { operand(this, where, delta, data, null); - } else if (value._path && value._atomics) { // arrays and other custom types (support plugins etc) handleAtomics(this, where, delta, data, value); - } else if (value._path && Buffer.isBuffer(value)) { // MongooseBuffer value = value.toObject(); operand(this, where, delta, data, value); - } else { - value = utils.clone(value, { depopulate: 1 }); + value = utils.clone(value, { + depopulate: true, + transform: false, + virtuals: false, + retainKeyOrder: true, + _isNested: true + }); operand(this, where, delta, data, value); } } @@ -530,8 +605,7 @@ function checkDivergentArray(doc, path, array) { // If any array was selected using an $elemMatch projection, we deny the update. // NOTE: MongoDB only supports projected $elemMatch on top level array. var top = path.split('.')[0]; - if ((doc.$__.selected[top] && doc.$__.selected[top].$elemMatch) || - doc.$__.selected[top + '.$']) { + if (doc.$__.selected[top + '.$']) { return top; } } @@ -548,15 +622,15 @@ function checkDivergentArray(doc, path, array) { // would be similarily destructive as we never received all // elements of the array and potentially would overwrite data. var check = pop.options.match || - pop.options.options && hasOwnProperty(pop.options.options, 'limit') || // 0 is not permitted - pop.options.options && pop.options.options.skip || // 0 is permitted - pop.options.select && // deselected _id? - (0 === pop.options.select._id || - /\s?-_id\s?/.test(pop.options.select)); + pop.options.options && utils.object.hasOwnProperty(pop.options.options, 'limit') || // 0 is not permitted + pop.options.options && pop.options.options.skip || // 0 is permitted + pop.options.select && // deselected _id? + (pop.options.select._id === 0 || + /\s?-_id\s?/.test(pop.options.select)); if (check) { var atomics = array._atomics; - if (0 === Object.keys(atomics).length || atomics.$set || atomics.$pop) { + if (Object.keys(atomics).length === 0 || atomics.$set || atomics.$pop) { return path; } } @@ -573,7 +647,7 @@ function checkDivergentArray(doc, path, array) { Model.prototype.$__version = function(where, delta) { var key = this.schema.options.versionKey; - if (true === where) { + if (where === true) { // this is an insert if (key) this.setValue(key, delta[key] = 0); return; @@ -591,12 +665,18 @@ Model.prototype.$__version = function(where, delta) { // $push $addToSet don't need the where clause set if (VERSION_WHERE === (VERSION_WHERE & this.$__.version)) { - where[key] = this.getValue(key); + var value = this.getValue(key); + if (value != null) where[key] = value; } if (VERSION_INC === (VERSION_INC & this.$__.version)) { - if (!delta.$set || typeof delta.$set[key] === 'undefined') { - delta.$inc || (delta.$inc = {}); + if (get(delta.$set, key, null) != null) { + // Version key is getting set, means we'll increment the doc's version + // after a successful save, so we should set the incremented version so + // future saves don't fail (gh-5779) + ++delta.$set[key]; + } else { + delta.$inc = delta.$inc || {}; delta.$inc[key] = 1; } } @@ -622,7 +702,7 @@ Model.prototype.increment = function increment() { }; /** - * Returns a query object which applies shardkeys if they exist. + * Returns a query object * * @api private * @method $__where @@ -632,23 +712,11 @@ Model.prototype.increment = function increment() { Model.prototype.$__where = function _where(where) { where || (where = {}); - var paths, - len; - if (!where._id) { where._id = this._doc._id; } - if (this.$__.shardval) { - paths = Object.keys(this.$__.shardval); - len = paths.length; - - for (var i = 0; i < len; ++i) { - where[paths[i]] = this.$__.shardval[paths[i]]; - } - } - - if (!this._doc._id) { + if (this._doc._id == null) { return new Error('No _id found on document!'); } @@ -672,7 +740,7 @@ Model.prototype.$__where = function _where(where) { * ####Example: * product.remove().then(function (product) { * ... - * }).onRejected(function (err) { + * }).catch(function (err) { * assert.ok(err) * }) * @@ -682,13 +750,13 @@ Model.prototype.$__where = function _where(where) { */ Model.prototype.remove = function remove(options, fn) { - var Promise = PromiseProvider.get(); - - if ('function' == typeof options) { + if (typeof options === 'function') { fn = options; options = undefined; } + var _this = this; + if (!options) { options = {}; } @@ -696,14 +764,25 @@ Model.prototype.remove = function remove(options, fn) { if (this.$__.removing) { if (fn) { this.$__.removing.then( - function(res) { fn(null, res); }, - function(err) { fn(err); }); + function(res) { fn(null, res); }, + function(err) { fn(err); }); } return this; } + if (this.$__.isDeleted) { + setImmediate(function() { + fn(null, _this); + }); + return this; + } - var _this = this; - var promise = this.$__.removing = new Promise.ES6(function(resolve, reject) { + var Promise = PromiseProvider.get(); + + if (fn) { + fn = this.constructor.$wrapCallback(fn); + } + + this.$__.removing = new Promise.ES6(function(resolve, reject) { var where = _this.$__where(); if (where instanceof Error) { reject(where); @@ -717,19 +796,19 @@ Model.prototype.remove = function remove(options, fn) { _this.collection.remove(where, options, function(err) { if (!err) { + _this.$__.isDeleted = true; _this.emit('remove', _this); + _this.constructor.emit('remove', _this); resolve(_this); fn && fn(null, _this); return; } - + _this.$__.isDeleted = false; reject(err); fn && fn(err); - return; }); }); - - return promise; + return this.$__.removing; }; /** @@ -774,86 +853,38 @@ Model.prototype.model = function model(name) { * @api public */ -Model.discriminator = function discriminator(name, schema) { - if (!(schema instanceof Schema)) { - throw new Error("You must pass a valid discriminator Schema"); - } - - if (this.schema.discriminatorMapping && !this.schema.discriminatorMapping.isRoot) { - throw new Error("Discriminator \"" + name + - "\" can only be a discriminator of the root model"); - } - - var key = this.schema.options.discriminatorKey; - if (schema.path(key)) { - throw new Error("Discriminator \"" + name + - "\" cannot have field with name \"" + key + "\""); - } - - // merges base schema into new discriminator schema and sets new type field. - (function(schema, baseSchema) { - utils.merge(schema, baseSchema); - - var obj = {}; - obj[key] = { type: String, default: name }; - schema.add(obj); - schema.discriminatorMapping = { key: key, value: name, isRoot: false }; - - if (baseSchema.options.collection) { - schema.options.collection = baseSchema.options.collection; +Model.discriminator = function(name, schema) { + var model; + if (typeof name === 'function') { + model = name; + name = utils.getFunctionName(model); + if (!(model.prototype instanceof Model)) { + throw new Error('The provided class ' + name + ' must extend Model'); } - - // throws error if options are invalid - (function(a, b) { - a = utils.clone(a); - b = utils.clone(b); - delete a.toJSON; - delete a.toObject; - delete b.toJSON; - delete b.toObject; - - if (!utils.deepEqual(a, b)) { - throw new Error("Discriminator options are not customizable " + - "(except toJSON & toObject)"); - } - })(schema.options, baseSchema.options); - - var toJSON = schema.options.toJSON, - toObject = schema.options.toObject; - - schema.options = utils.clone(baseSchema.options); - if (toJSON) schema.options.toJSON = toJSON; - if (toObject) schema.options.toObject = toObject; - - schema.callQueue = baseSchema.callQueue.concat(schema.callQueue.slice(schema._defaultMiddleware.length)); - schema._requiredpaths = undefined; // reset just in case Schema#requiredPaths() was called on either schema - })(schema, this.schema); - - if (!this.discriminators) { - this.discriminators = {}; } - if (!this.schema.discriminatorMapping) { - this.schema.discriminatorMapping = { key: key, value: null, isRoot: true }; + schema = discriminator(this, name, schema); + if (this.db.models[name]) { + throw new OverwriteModelError(name); } - if (this.discriminators[name]) { - throw new Error("Discriminator with name \"" + name + "\" already exists"); - } + schema.$isRootDiscriminator = true; - this.discriminators[name] = this.db.model(name, schema, this.collection.name); - this.discriminators[name].prototype.__proto__ = this.prototype; - Object.defineProperty(this.discriminators[name], 'baseModelName', { + model = this.db.model(model || name, schema, this.collection.name); + this.discriminators[name] = model; + var d = this.discriminators[name]; + d.prototype.__proto__ = this.prototype; + Object.defineProperty(d, 'baseModelName', { value: this.modelName, configurable: true, writable: false }); // apply methods and statics - applyMethods(this.discriminators[name], schema); - applyStatics(this.discriminators[name], schema); + applyMethods(d, schema); + applyStatics(d, schema); - return this.discriminators[name]; + return d; }; // Model (class) features @@ -862,26 +893,69 @@ Model.discriminator = function discriminator(name, schema) { * Give the constructor the ability to emit events. */ -for (var i in EventEmitter.prototype) +for (var i in EventEmitter.prototype) { Model[i] = EventEmitter.prototype[i]; +} /** - * Called when the model compiles. + * Performs any async initialization of this model against MongoDB. Currently, + * this function is only responsible for building [indexes](https://docs.mongodb.com/manual/indexes/), + * unless [`autoIndex`](http://mongoosejs.com/docs/guide.html#autoIndex) is turned off. * - * @api private + * This function is called automatically, so you don't need to call it. + * This function is also idempotent, so you may call it to get back a promise + * that will resolve when your indexes are finished building as an alternative + * to `MyModel.on('index')` + * + * ####Example: + * + * var eventSchema = new Schema({ thing: { type: 'string', unique: true }}) + * // This calls `Event.init()` implicitly, so you don't need to call + * // `Event.init()` on your own. + * var Event = mongoose.model('Event', eventSchema); + * + * Event.init().then(function(Event) { + * // You can also use `Event.on('index')` if you prefer event emitters + * // over promises. + * console.log('Indexes are done building!'); + * }); + * + * @api public + * @param {Function} [callback] + * @returns {Promise} */ -Model.init = function init() { - if ((this.schema.options.autoIndex) || - (this.schema.options.autoIndex === null && this.db.config.autoIndex)) { - this.ensureIndexes(); +Model.init = function init(callback) { + this.schema.emit('init', this); + + if (this.$init) { + return this.$init; } - this.schema.emit('init', this); + var _this = this; + var Promise = PromiseProvider.get(); + this.$init = new Promise.ES6(function(resolve, reject) { + if ((_this.schema.options.autoIndex) || + (_this.schema.options.autoIndex == null && _this.db.config.autoIndex)) { + _this.ensureIndexes({ _automatic: true, __noPromise: true }, function(error) { + if (error) { + callback && callback(error); + return reject(error); + } + callback && callback(null, _this); + resolve(_this); + }); + } else { + resolve(_this); + } + }); + + return this.$init; }; /** - * Sends `ensureIndex` commands to mongo for each index declared in the schema. + * Sends `createIndex` commands to mongo for each index declared in the schema. + * The `createIndex` commands are sent in series. * * ####Example: * @@ -902,65 +976,126 @@ Model.init = function init() { * * _NOTE: It is not recommended that you run this in production. Index creation may impact database performance depending on your load. Use with caution._ * - * The `ensureIndex` commands are not sent in parallel. This is to avoid the `MongoError: cannot add index with a background operation in progress` error. See [this ticket](https://github.com/Automattic/mongoose/issues/1365) for more information. - * + * @param {Object} [options] internal options * @param {Function} [cb] optional callback * @return {Promise} * @api public */ -Model.ensureIndexes = function ensureIndexes(cb) { - var promise = new Promise(cb); +Model.ensureIndexes = function ensureIndexes(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (options && options.__noPromise) { + _ensureIndexes(this, options, callback); + return; + } - var indexes = this.schema.indexes(); - if (!indexes.length) { - process.nextTick(promise.fulfill.bind(promise)); - return promise; + if (callback) { + callback = this.$wrapCallback(callback); } - // Indexes are created one-by-one to support how MongoDB < 2.4 deals - // with background indexes. + var _this = this; + var Promise = PromiseProvider.get(); + return new Promise.ES6(function(resolve, reject) { + _ensureIndexes(_this, options || {}, function(error) { + if (error) { + callback && callback(error); + reject(error); + } + callback && callback(); + resolve(); + }); + }); +}; - var self = this; +/** + * Similar to `ensureIndexes()`, except for it uses the [`createIndex`](http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#createIndex) + * function. The `ensureIndex()` function checks to see if an index with that + * name already exists, and, if not, does not attempt to create the index. + * `createIndex()` bypasses this check. + * + * @param {Object} [options] internal options + * @param {Function} [cb] optional callback + * @return {Promise} + * @api public + */ + +Model.createIndexes = function createIndexes(options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + options = options || {}; + options.createIndex = true; + return this.ensureIndexes(options, callback); +}; + +function _ensureIndexes(model, options, callback) { + var indexes = model.schema.indexes(); + options = options || {}; var done = function(err) { - if (err && self.schema.options.emitIndexErrors) { - self.emit('error', err); + if (err && model.schema.options.emitIndexErrors) { + model.emit('error', err); } - self.emit('index', err); - promise.resolve(err); + model.emit('index', err); + callback && callback(err); }; + if (!indexes.length) { + setImmediate(function() { + done(); + }); + return; + } + // Indexes are created one-by-one to support how MongoDB < 2.4 deals + // with background indexes. + var indexSingleDone = function(err, fields, options, name) { - self.emit('index-single-done', err, fields, options, name); + model.emit('index-single-done', err, fields, options, name); }; var indexSingleStart = function(fields, options) { - self.emit('index-single-start', fields, options); + model.emit('index-single-start', fields, options); }; var create = function() { + if (options._automatic) { + if (model.schema.options.autoIndex === false || + (model.schema.options.autoIndex == null && model.db.config.autoIndex === false)) { + return done(); + } + } + var index = indexes.shift(); if (!index) return done(); var indexFields = index[0]; - var options = index[1]; + var indexOptions = index[1]; _handleSafe(options); indexSingleStart(indexFields, options); - - self.collection.ensureIndex(indexFields, options, tick(function(err,name) { - indexSingleDone(err,indexFields, options, name); + var methodName = options.createIndex ? 'createIndex' : 'ensureIndex'; + model.collection[methodName](indexFields, indexOptions, utils.tick(function(err, name) { + indexSingleDone(err, indexFields, indexOptions, name); if (err) { return done(err); - } else { - create(); } + create(); })); }; - create(); - return promise; -}; + setImmediate(function() { + // If buffering is off, do this manually. + if (options._automatic && !model.collection.collection) { + model.collection.addQueue(create, []); + } else { + create(); + } + }); +} function _handleSafe(options) { if (options.safe) { @@ -1028,41 +1163,146 @@ Model.base; Model.discriminators; /** - * Removes documents from the collection. + * Translate any aliases fields/conditions so the final query or document object is pure * * ####Example: * - * Comment.remove({ title: 'baby born from alien father' }, function (err) { - * - * }); + * Character + * .find(Character.translateAliases({ + * '名': 'Eddard Stark' // Alias for 'name' + * }) + * .exec(function(err, characters) {}) * * ####Note: + * Only translate arguments of object type anything else is returned raw + * + * @param {Object} raw fields/conditions that may contain aliased keys + * @return {Object} the translated 'pure' fields/conditions + */ +Model.translateAliases = function translateAliases(fields) { + var aliases = this.schema.aliases; + + if (typeof fields === 'object') { + // Fields is an object (query conditions or document fields) + for (var key in fields) { + if (aliases[key]) { + fields[aliases[key]] = fields[key]; + delete fields[key]; + } + } + + return fields; + } else { + // Don't know typeof fields + return fields; + } +}; + +/** + * Removes all documents that match `conditions` from the collection. + * To remove just the first document that matches `conditions`, set the `single` + * option to true. * - * To remove documents without waiting for a response from MongoDB, do not pass a `callback`, then call `exec` on the returned [Query](#query-js): + * ####Example: * - * var query = Comment.remove({ _id: id }); - * query.exec(); + * Character.remove({ name: 'Eddard Stark' }, function (err) {}); * * ####Note: * - * This method sends a remove command directly to MongoDB, no Mongoose documents are involved. Because no Mongoose documents are involved, _no middleware (hooks) are executed_. + * This method sends a remove command directly to MongoDB, no Mongoose documents + * are involved. Because no Mongoose documents are involved, _no middleware + * (hooks) are executed_. * * @param {Object} conditions * @param {Function} [callback] - * @return {Promise} Promise + * @return {Query} * @api public */ Model.remove = function remove(conditions, callback) { - if ('function' === typeof conditions) { + if (typeof conditions === 'function') { + callback = conditions; + conditions = {}; + } + + // get the mongodb collection object + var mq = new this.Query({}, {}, this, this.collection); + + if (callback) { + callback = this.$wrapCallback(callback); + } + + return mq.remove(conditions, callback); +}; + +/** + * Deletes the first document that matches `conditions` from the collection. + * Behaves like `remove()`, but deletes at most one document regardless of the + * `single` option. + * + * ####Example: + * + * Character.deleteOne({ name: 'Eddard Stark' }, function (err) {}); + * + * ####Note: + * + * Like `Model.remove()`, this function does **not** trigger `pre('remove')` or `post('remove')` hooks. + * + * @param {Object} conditions + * @param {Function} [callback] + * @return {Query} + * @api public + */ + +Model.deleteOne = function deleteOne(conditions, callback) { + if (typeof conditions === 'function') { + callback = conditions; + conditions = {}; + } + + // get the mongodb collection object + var mq = new this.Query(conditions, {}, this, this.collection); + + if (callback) { + callback = this.$wrapCallback(callback); + } + + return mq.deleteOne(callback); +}; + +/** + * Deletes all of the documents that match `conditions` from the collection. + * Behaves like `remove()`, but deletes all documents that match `conditions` + * regardless of the `single` option. + * + * ####Example: + * + * Character.deleteMany({ name: /Stark/, age: { $gte: 18 } }, function (err) {}); + * + * ####Note: + * + * Like `Model.remove()`, this function does **not** trigger `pre('remove')` or `post('remove')` hooks. + * + * @param {Object} conditions + * @param {Function} [callback] + * @return {Query} + * @api public + */ + +Model.deleteMany = function deleteMany(conditions, callback) { + if (typeof conditions === 'function') { callback = conditions; conditions = {}; } // get the mongodb collection object - var mq = new Query(conditions, {}, this, this.collection); + var mq = new this.Query(conditions, {}, this, this.collection); + + if (callback) { + callback = this.$wrapCallback(callback); + } - return mq.remove(callback); + return mq.deleteMany(callback); }; /** @@ -1098,7 +1338,7 @@ Model.remove = function remove(conditions, callback) { * * @param {Object} conditions * @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo) - * @param {Object} [options] optional + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] * @return {Query} * @see field selection #query_Query-select @@ -1107,41 +1347,51 @@ Model.remove = function remove(conditions, callback) { */ Model.find = function find(conditions, projection, options, callback) { - if ('function' == typeof conditions) { + if (typeof conditions === 'function') { callback = conditions; conditions = {}; projection = null; options = null; - } else if ('function' == typeof projection) { + } else if (typeof projection === 'function') { callback = projection; projection = null; options = null; - } else if ('function' == typeof options) { + } else if (typeof options === 'function') { callback = options; options = null; } - // get the raw mongodb collection object - var mq = new Query({}, options, this, this.collection); + var mq = new this.Query({}, {}, this, this.collection); mq.select(projection); - if (this.schema.discriminatorMapping && mq.selectedInclusively()) { + mq.setOptions(options); + if (this.schema.discriminatorMapping && + this.schema.discriminatorMapping.isRoot && + mq.selectedInclusively()) { + // Need to select discriminator key because original schema doesn't have it mq.select(this.schema.options.discriminatorKey); } + if (callback) { + callback = this.$wrapCallback(callback); + } + return mq.find(conditions, callback); }; /** * Finds a single document by its _id field. `findById(id)` is almost* - * equivalent to `findOne({ _id: id })`. + * equivalent to `findOne({ _id: id })`. If you want to query by a document's + * `_id`, use `findById()` instead of `findOne()`. * * The `id` is cast based on the Schema before sending the command. * - * Note: `findById()` triggers `findOne` hooks. + * This function triggers the following middleware: + * - `findOne()` * - * * Except for how it treats `undefined`. Because the MongoDB driver - * deletes keys that have value `undefined`, `findById(undefined)` gets - * translated to `findById({ _id: null })`. + * \* Except for how it treats `undefined`. If you use `findOne()`, you'll see + * that `findOne(undefined)` and `findOne({ _id: undefined })` are equivalent + * to `findOne({})` and return arbitrary documents. However, mongoose + * translates `findById(undefined)` into `findOne({ _id: null })`. * * ####Example: * @@ -1168,7 +1418,7 @@ Model.find = function find(conditions, projection, options, callback) { * * @param {Object|String|Number} id value of `_id` to query by * @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo) - * @param {Object} [options] optional + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] * @return {Query} * @see field selection #query_Query-select @@ -1180,7 +1430,12 @@ Model.findById = function findById(id, projection, options, callback) { if (typeof id === 'undefined') { id = null; } - return this.findOne({ _id: id }, projection, options, callback); + + if (callback) { + callback = this.$wrapCallback(callback); + } + + return this.findOne({_id: id}, projection, options, callback); }; /** @@ -1188,6 +1443,10 @@ Model.findById = function findById(id, projection, options, callback) { * * The `conditions` are cast to their respective SchemaTypes before the command is sent. * + * *Note:* `conditions` is optional, and if `conditions` is null or undefined, + * mongoose will send an empty `findOne` command to MongoDB, which will return + * an arbitrary document. If you're querying by `_id`, use `findById()` instead. + * * ####Example: * * // find one iphone adventures - iphone adventures?? @@ -1213,7 +1472,7 @@ Model.findById = function findById(id, projection, options, callback) { * * @param {Object} [conditions] * @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo) - * @param {Object} [options] optional + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] * @return {Query} * @see field selection #query_Query-select @@ -1222,14 +1481,14 @@ Model.findById = function findById(id, projection, options, callback) { */ Model.findOne = function findOne(conditions, projection, options, callback) { - if ('function' == typeof options) { + if (typeof options === 'function') { callback = options; options = null; - } else if ('function' == typeof projection) { + } else if (typeof projection === 'function') { callback = projection; projection = null; options = null; - } else if ('function' == typeof conditions) { + } else if (typeof conditions === 'function') { callback = conditions; conditions = {}; projection = null; @@ -1237,12 +1496,19 @@ Model.findOne = function findOne(conditions, projection, options, callback) { } // get the mongodb collection object - var mq = new Query({}, options, this, this.collection); + var mq = new this.Query({}, {}, this, this.collection); mq.select(projection); - if (this.schema.discriminatorMapping && mq.selectedInclusively()) { + mq.setOptions(options); + if (this.schema.discriminatorMapping && + this.schema.discriminatorMapping.isRoot && + mq.selectedInclusively()) { mq.select(this.schema.options.discriminatorKey); } + if (callback) { + callback = this.$wrapCallback(callback); + } + return mq.findOne(conditions, callback); }; @@ -1263,11 +1529,17 @@ Model.findOne = function findOne(conditions, projection, options, callback) { */ Model.count = function count(conditions, callback) { - if ('function' === typeof conditions) - callback = conditions, conditions = {}; + if (typeof conditions === 'function') { + callback = conditions; + conditions = {}; + } // get the mongodb collection object - var mq = new Query({}, {}, this, this.collection); + var mq = new this.Query({}, {}, this, this.collection); + + if (callback) { + callback = this.$wrapCallback(callback); + } return mq.count(conditions, callback); }; @@ -1298,12 +1570,15 @@ Model.count = function count(conditions, callback) { Model.distinct = function distinct(field, conditions, callback) { // get the mongodb collection object - var mq = new Query({}, {}, this, this.collection); + var mq = new this.Query({}, {}, this, this.collection); - if ('function' == typeof conditions) { + if (typeof conditions === 'function') { callback = conditions; conditions = {}; } + if (callback) { + callback = this.$wrapCallback(callback); + } return mq.distinct(field, conditions, callback); }; @@ -1333,8 +1608,9 @@ Model.distinct = function distinct(field, conditions, callback) { */ Model.where = function where(path, val) { + void val; // eslint // get the mongodb collection object - var mq = new Query({}, {}, this, this.collection).find({}); + var mq = new this.Query({}, {}, this, this.collection).find({}); return mq.where.apply(mq, arguments); }; @@ -1343,7 +1619,7 @@ Model.where = function where(path, val) { * * Sometimes you need to query for things in mongodb using a JavaScript expression. You can do so via `find({ $where: javascript })`, or you can use the mongoose shortcut method $where via a Query chain or from your mongoose Model. * - * Blog.$where('this.comments.length > 5').exec(function (err, docs) {}); + * Blog.$where('this.username.indexOf("val") !== -1').exec(function (err, docs) {}); * * @param {String|Function} argument is a javascript string or anonymous function * @method $where @@ -1354,7 +1630,7 @@ Model.where = function where(path, val) { */ Model.$where = function $where() { - var mq = new Query({}, {}, this, this.collection).find({}); + var mq = new this.Query({}, {}, this, this.collection).find({}); return mq.$where.apply(mq, arguments); }; @@ -1367,8 +1643,14 @@ Model.$where = function $where() { * * - `new`: bool - if true, return the modified document rather than the original. defaults to false (changed in 4.0) * - `upsert`: bool - creates the object if it doesn't exist. defaults to false. + * - `fields`: {Object|String} - Field selection. Equivalent to `.select(fields).findOneAndUpdate()` + * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update - * - `select`: sets the document fields to return + * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema. + * - `setDefaultsOnInsert`: if this and `upsert` are true, mongoose will apply the [defaults](http://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://docs.mongodb.org/v2.4/reference/operator/update/setOnInsert/). + * - `passRawResult`: if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) + * - `strict`: overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) for this update + * - `runSettersOnQuery`: bool - if true, run all setters defined on the associated model's schema for all fields defined in the query and the update. * * ####Examples: * @@ -1385,23 +1667,23 @@ Model.$where = function $where() { * ####Example: * * var query = { name: 'borne' }; - * Model.findOneAndUpdate(query, { name: 'jason borne' }, options, callback) + * Model.findOneAndUpdate(query, { name: 'jason bourne' }, options, callback) * * // is sent as - * Model.findOneAndUpdate(query, { $set: { name: 'jason borne' }}, options, callback) + * Model.findOneAndUpdate(query, { $set: { name: 'jason bourne' }}, options, callback) * - * This helps prevent accidentally overwriting your document with `{ name: 'jason borne' }`. + * This helps prevent accidentally overwriting your document with `{ name: 'jason bourne' }`. * * ####Note: * * Values are cast to their appropriate types when using the findAndModify helpers. - * However, the below are never executed. + * However, the below are not executed by default. * - * - defaults - * - setters + * - defaults. Use the `setDefaultsOnInsert` option to override. + * - setters. Use the `runSettersOnQuery` option to override. * - * `findAndModify` helpers support limited defaults and validation. You can - * enable these by setting the `setDefaultsOnInsert` and `runValidators` options, + * `findAndModify` helpers support limited validation. You can + * enable these by setting the `runValidators` options, * respectively. * * If you need full-fledged validation, use the traditional approach of first @@ -1409,13 +1691,14 @@ Model.$where = function $where() { * * Model.findById(id, function (err, doc) { * if (err) .. - * doc.name = 'jason borne'; + * doc.name = 'jason bourne'; * doc.save(callback); * }); * * @param {Object} [conditions] * @param {Object} [update] - * @param {Object} [options] + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) + * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](http://mongoosejs.com/docs/api.html#query_Query-lean). * @param {Function} [callback] * @return {Query} * @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command @@ -1423,39 +1706,51 @@ Model.$where = function $where() { */ Model.findOneAndUpdate = function(conditions, update, options, callback) { - if ('function' == typeof options) { + if (typeof options === 'function') { callback = options; options = null; - } - else if (1 === arguments.length) { - if ('function' == typeof conditions) { + } else if (arguments.length === 1) { + if (typeof conditions === 'function') { var msg = 'Model.findOneAndUpdate(): First argument must not be a function.\n\n' - + ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options, callback)\n' - + ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options)\n' - + ' ' + this.modelName + '.findOneAndUpdate(conditions, update)\n' - + ' ' + this.modelName + '.findOneAndUpdate(update)\n' - + ' ' + this.modelName + '.findOneAndUpdate()\n'; + + ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options, callback)\n' + + ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options)\n' + + ' ' + this.modelName + '.findOneAndUpdate(conditions, update)\n' + + ' ' + this.modelName + '.findOneAndUpdate(update)\n' + + ' ' + this.modelName + '.findOneAndUpdate()\n'; throw new TypeError(msg); } update = conditions; conditions = undefined; } + if (callback) { + callback = this.$wrapCallback(callback); + } var fields; if (options && options.fields) { fields = options.fields; - options.fields = undefined; } - update = utils.clone(update, { depopulate: 1 }); + var retainKeyOrder = get(options, 'retainKeyOrder') || + get(this, 'schema.options.retainKeyOrder') || + false; + update = utils.clone(update, { + depopulate: true, + _isNested: true, + retainKeyOrder: retainKeyOrder + }); if (this.schema.options.versionKey && options && options.upsert) { - if (!update.$setOnInsert) { - update.$setOnInsert = {}; + if (options.overwrite) { + update[this.schema.options.versionKey] = 0; + } else { + if (!update.$setOnInsert) { + update.$setOnInsert = {}; + } + update.$setOnInsert[this.schema.options.versionKey] = 0; } - update.$setOnInsert[this.schema.options.versionKey] = 0; } - var mq = new Query({}, {}, this, this.collection); + var mq = new this.Query({}, {}, this, this.collection); mq.select(fields); return mq.findOneAndUpdate(conditions, update, options, callback); @@ -1470,14 +1765,20 @@ Model.findOneAndUpdate = function(conditions, update, options, callback) { * callback. The query executes immediately if `callback` is passed else a * Query object is returned. * - * This function triggers `findOneAndUpdate` middleware. + * This function triggers the following middleware: + * - `findOneAndUpdate()` * * ####Options: * * - `new`: bool - true to return the modified document rather than the original. defaults to false * - `upsert`: bool - creates the object if it doesn't exist. defaults to false. + * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema. + * - `setDefaultsOnInsert`: if this and `upsert` are true, mongoose will apply the [defaults](http://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://docs.mongodb.org/v2.4/reference/operator/update/setOnInsert/). * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update * - `select`: sets the document fields to return + * - `passRawResult`: if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) + * - `strict`: overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) for this update + * - `runSettersOnQuery`: bool - if true, run all setters defined on the associated model's schema for all fields defined in the query and the update. * * ####Examples: * @@ -1493,23 +1794,23 @@ Model.findOneAndUpdate = function(conditions, update, options, callback) { * * ####Example: * - * Model.findByIdAndUpdate(id, { name: 'jason borne' }, options, callback) + * Model.findByIdAndUpdate(id, { name: 'jason bourne' }, options, callback) * * // is sent as - * Model.findByIdAndUpdate(id, { $set: { name: 'jason borne' }}, options, callback) + * Model.findByIdAndUpdate(id, { $set: { name: 'jason bourne' }}, options, callback) * - * This helps prevent accidentally overwriting your document with `{ name: 'jason borne' }`. + * This helps prevent accidentally overwriting your document with `{ name: 'jason bourne' }`. * * ####Note: * * Values are cast to their appropriate types when using the findAndModify helpers. - * However, the below are never executed. + * However, the below are not executed by default. * - * - defaults - * - setters + * - defaults. Use the `setDefaultsOnInsert` option to override. + * - setters. Use the `runSettersOnQuery` option to override. * - * `findAndModify` helpers support limited defaults and validation. You can - * enable these by setting the `setDefaultsOnInsert` and `runValidators` options, + * `findAndModify` helpers support limited validation. You can + * enable these by setting the `runValidators` options, * respectively. * * If you need full-fledged validation, use the traditional approach of first @@ -1517,13 +1818,14 @@ Model.findOneAndUpdate = function(conditions, update, options, callback) { * * Model.findById(id, function (err, doc) { * if (err) .. - * doc.name = 'jason borne'; + * doc.name = 'jason bourne'; * doc.save(callback); * }); * * @param {Object|Number|String} id value of `_id` to query by * @param {Object} [update] - * @param {Object} [options] + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) + * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](http://mongoosejs.com/docs/api.html#query_Query-lean). * @param {Function} [callback] * @return {Query} * @see Model.findOneAndUpdate #model_Model.findOneAndUpdate @@ -1532,28 +1834,26 @@ Model.findOneAndUpdate = function(conditions, update, options, callback) { */ Model.findByIdAndUpdate = function(id, update, options, callback) { - var args; - if (1 === arguments.length) { - if ('function' == typeof id) { + if (callback) { + callback = this.$wrapCallback(callback); + } + if (arguments.length === 1) { + if (typeof id === 'function') { var msg = 'Model.findByIdAndUpdate(): First argument must not be a function.\n\n' - + ' ' + this.modelName + '.findByIdAndUpdate(id, callback)\n' - + ' ' + this.modelName + '.findByIdAndUpdate(id)\n' - + ' ' + this.modelName + '.findByIdAndUpdate()\n'; + + ' ' + this.modelName + '.findByIdAndUpdate(id, callback)\n' + + ' ' + this.modelName + '.findByIdAndUpdate(id)\n' + + ' ' + this.modelName + '.findByIdAndUpdate()\n'; throw new TypeError(msg); } - return this.findOneAndUpdate({_id: id }, undefined); + return this.findOneAndUpdate({_id: id}, undefined); } - args = utils.args(arguments, 1); - // if a model is passed in instead of an id if (id instanceof Document) { id = id._id; } - if (id) { - args.unshift({ _id: id }); - } - return this.findOneAndUpdate.apply(this, args); + + return this.findOneAndUpdate.call(this, {_id: id}, update, options, callback); }; /** @@ -1563,10 +1863,16 @@ Model.findByIdAndUpdate = function(id, update, options, callback) { * * Executes immediately if `callback` is passed else a Query object is returned. * + * This function triggers the following middleware: + * - `findOneAndRemove()` + * * ####Options: * * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update + * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 * - `select`: sets the document fields to return + * - `passRawResult`: if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) + * - `strict`: overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) for this update * * ####Examples: * @@ -1577,13 +1883,13 @@ Model.findByIdAndUpdate = function(id, update, options, callback) { * A.findOneAndRemove() // returns Query * * Values are cast to their appropriate types when using the findAndModify helpers. - * However, the below are never executed. + * However, the below are not executed by default. * - * - defaults - * - setters + * - defaults. Use the `setDefaultsOnInsert` option to override. + * - setters. Use the `runSettersOnQuery` option to override. * - * `findAndModify` helpers support limited defaults and validation. You can - * enable these by setting the `setDefaultsOnInsert` and `runValidators` options, + * `findAndModify` helpers support limited validation. You can + * enable these by setting the `runValidators` options, * respectively. * * If you need full-fledged validation, use the traditional approach of first @@ -1591,12 +1897,12 @@ Model.findByIdAndUpdate = function(id, update, options, callback) { * * Model.findById(id, function (err, doc) { * if (err) .. - * doc.name = 'jason borne'; + * doc.name = 'jason bourne'; * doc.save(callback); * }); * * @param {Object} conditions - * @param {Object} [options] + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] * @return {Query} * @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command @@ -1604,18 +1910,21 @@ Model.findByIdAndUpdate = function(id, update, options, callback) { */ Model.findOneAndRemove = function(conditions, options, callback) { - if (1 === arguments.length && 'function' == typeof conditions) { + if (arguments.length === 1 && typeof conditions === 'function') { var msg = 'Model.findOneAndRemove(): First argument must not be a function.\n\n' - + ' ' + this.modelName + '.findOneAndRemove(conditions, callback)\n' - + ' ' + this.modelName + '.findOneAndRemove(conditions)\n' - + ' ' + this.modelName + '.findOneAndRemove()\n'; + + ' ' + this.modelName + '.findOneAndRemove(conditions, callback)\n' + + ' ' + this.modelName + '.findOneAndRemove(conditions)\n' + + ' ' + this.modelName + '.findOneAndRemove()\n'; throw new TypeError(msg); } - if ('function' == typeof options) { + if (typeof options === 'function') { callback = options; options = undefined; } + if (callback) { + callback = this.$wrapCallback(callback); + } var fields; if (options) { @@ -1623,7 +1932,7 @@ Model.findOneAndRemove = function(conditions, options, callback) { options.select = undefined; } - var mq = new Query({}, {}, this, this.collection); + var mq = new this.Query({}, {}, this, this.collection); mq.select(fields); return mq.findOneAndRemove(conditions, options, callback); @@ -1636,10 +1945,15 @@ Model.findOneAndRemove = function(conditions, options, callback) { * * Executes immediately if `callback` is passed, else a `Query` object is returned. * + * This function triggers the following middleware: + * - `findOneAndRemove()` + * * ####Options: * * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update * - `select`: sets the document fields to return + * - `passRawResult`: if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) + * - `strict`: overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) for this update * * ####Examples: * @@ -1650,7 +1964,7 @@ Model.findOneAndRemove = function(conditions, options, callback) { * A.findByIdAndRemove() // returns Query * * @param {Object|Number|String} id value of `_id` to query by - * @param {Object} [options] + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] * @return {Query} * @see Model.findOneAndRemove #model_Model.findOneAndRemove @@ -1658,19 +1972,27 @@ Model.findOneAndRemove = function(conditions, options, callback) { */ Model.findByIdAndRemove = function(id, options, callback) { - if (1 === arguments.length && 'function' == typeof id) { + if (arguments.length === 1 && typeof id === 'function') { var msg = 'Model.findByIdAndRemove(): First argument must not be a function.\n\n' - + ' ' + this.modelName + '.findByIdAndRemove(id, callback)\n' - + ' ' + this.modelName + '.findByIdAndRemove(id)\n' - + ' ' + this.modelName + '.findByIdAndRemove()\n'; + + ' ' + this.modelName + '.findByIdAndRemove(id, callback)\n' + + ' ' + this.modelName + '.findByIdAndRemove(id)\n' + + ' ' + this.modelName + '.findByIdAndRemove()\n'; throw new TypeError(msg); } + if (callback) { + callback = this.$wrapCallback(callback); + } - return this.findOneAndRemove({ _id: id }, options, callback); + return this.findOneAndRemove({_id: id}, options, callback); }; /** - * Shortcut for creating a new Document that is automatically saved to the db if valid. + * Shortcut for saving one or more documents to the database. + * `MyModel.create(docs)` does `new MyModel(doc).save()` for every doc in + * docs. + * + * This function triggers the following middleware: + * - `save()` * * ####Example: * @@ -1695,22 +2017,24 @@ Model.findByIdAndRemove = function(id, options, callback) { * // ... * }) * - * @param {Array|Object...} doc(s) + * @param {Array|Object|*} doc(s) * @param {Function} [callback] callback * @return {Promise} * @api public */ Model.create = function create(doc, callback) { - var args, - cb; + var args; + var cb; + var discriminatorKey = this.schema.options.discriminatorKey; if (Array.isArray(doc)) { args = doc; cb = callback; } else { var last = arguments[arguments.length - 1]; - if ('function' == typeof last) { + // Handle falsy callbacks re: #5061 + if (typeof last === 'function' || !last) { cb = last; args = utils.args(arguments, 0, arguments.length - 1); } else { @@ -1719,11 +2043,14 @@ Model.create = function create(doc, callback) { } var Promise = PromiseProvider.get(); - var ModelConstructor = this; + var _this = this; + if (cb) { + cb = this.$wrapCallback(cb); + } var promise = new Promise.ES6(function(resolve, reject) { if (args.length === 0) { - process.nextTick(function() { + setImmediate(function() { cb && cb(null); resolve(null); }); @@ -1731,16 +2058,31 @@ Model.create = function create(doc, callback) { } var toExecute = []; + var firstError; args.forEach(function(doc) { toExecute.push(function(callback) { - var toSave = new ModelConstructor(doc); + var Model = _this.discriminators && doc[discriminatorKey] ? + _this.discriminators[doc[discriminatorKey]] : + _this; + var toSave = doc; var callbackWrapper = function(error, doc) { if (error) { - return callback(error); + if (!firstError) { + firstError = error; + } + return callback(null, { error: error }); } - callback(null, doc); + callback(null, { doc: doc }); }; + if (!(toSave instanceof Model)) { + try { + toSave = new Model(toSave); + } catch (error) { + return callbackWrapper(error); + } + } + // Hack to avoid getting a promise because of // $__registerHooksFromSchema if (toSave.$__original_save) { @@ -1751,23 +2093,346 @@ Model.create = function create(doc, callback) { }); }); - async.parallel(toExecute, function(error, savedDocs) { - if (error) { - cb && cb(error); - reject(error); + parallel(toExecute, function(error, res) { + var savedDocs = []; + var len = res.length; + for (var i = 0; i < len; ++i) { + if (res[i].doc) { + savedDocs.push(res[i].doc); + } + } + + if (firstError) { + if (cb) { + cb(firstError, savedDocs); + } else { + reject(firstError); + } return; } if (doc instanceof Array) { resolve(savedDocs); - cb && cb.call(ModelConstructor, null, savedDocs); + cb && cb.call(_this, null, savedDocs); } else { resolve.apply(promise, savedDocs); if (cb) { - savedDocs.unshift(null); - cb.apply(ModelConstructor, savedDocs); + cb.apply(_this, [null].concat(savedDocs)); + } + } + }); + }); + + return promise; +}; + +/*! + * ignore + */ + +var INSERT_MANY_CONVERT_OPTIONS = { + depopulate: true, + transform: false, + _skipDepopulateTopLevel: true, + flattenDecimals: false +}; + +/** + * Shortcut for validating an array of documents and inserting them into + * MongoDB if they're all valid. This function is faster than `.create()` + * because it only sends one operation to the server, rather than one for each + * document. + * + * Mongoose always validates each document **before** sending `insertMany` + * to MongoDB. So if one document has a validation error, no documents will + * be saved, unless you set + * [the `ordered` option to false](https://docs.mongodb.com/manual/reference/method/db.collection.insertMany/#error-handling). + * + * This function does **not** trigger save middleware. + * + * This function triggers the following middleware: + * - `insertMany()` + * + * ####Example: + * + * var arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]; + * Movies.insertMany(arr, function(error, docs) {}); + * + * @param {Array|Object|*} doc(s) + * @param {Object} [options] see the [mongodb driver options](http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#insertMany) + * @param {Boolean} [options.ordered = true] if true, will fail fast on the first error encountered. If false, will insert all the documents it can and report errors later. An `insertMany()` with `ordered = false` is called an "unordered" `insertMany()`. + * @param {Boolean} [options.rawResult = false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `false`, will return the [raw result from the MongoDB driver](http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~insertWriteOpCallback) with a `mongoose` property that contains `validationErrors` if this is an unordered `insertMany`. + * @param {Function} [callback] callback + * @return {Promise} + * @api public + */ + +Model.insertMany = function(arr, options, callback) { + var _this = this; + if (typeof options === 'function') { + callback = options; + options = null; + } + if (callback) { + callback = this.$wrapCallback(callback); + } + callback = callback || utils.noop; + options = options || {}; + var limit = get(options, 'limit', 1000); + var rawResult = get(options, 'rawResult', false); + var ordered = get(options, 'ordered', true); + + if (!Array.isArray(arr)) { + arr = [arr]; + } + + var toExecute = []; + var validationErrors = []; + arr.forEach(function(doc) { + toExecute.push(function(callback) { + doc = new _this(doc); + doc.validate({ __noPromise: true }, function(error) { + if (error) { + // Option `ordered` signals that insert should be continued after reaching + // a failing insert. Therefore we delegate "null", meaning the validation + // failed. It's up to the next function to filter out all failed models + if (ordered === false) { + validationErrors.push(error); + return callback(null, null); + } + return callback(error); + } + callback(null, doc); + }); + }); + }); + + parallelLimit(toExecute, limit, function(error, docs) { + if (error) { + callback && callback(error); + return; + } + // We filter all failed pre-validations by removing nulls + var docAttributes = docs.filter(function(doc) { + return doc != null; + }); + // Quickly escape while there aren't any valid docAttributes + if (docAttributes.length < 1) { + callback(null, []); + return; + } + var docObjects = docAttributes.map(function(doc) { + if (doc.schema.options.versionKey) { + doc[doc.schema.options.versionKey] = 0; + } + if (doc.initializeTimestamps) { + return doc.initializeTimestamps().toObject(INSERT_MANY_CONVERT_OPTIONS); + } + return doc.toObject(INSERT_MANY_CONVERT_OPTIONS); + }); + + _this.collection.insertMany(docObjects, options, function(error, res) { + if (error) { + callback && callback(error); + return; + } + for (var i = 0; i < docAttributes.length; ++i) { + docAttributes[i].isNew = false; + docAttributes[i].emit('isNew', false); + docAttributes[i].constructor.emit('isNew', false); + } + if (rawResult) { + if (ordered === false) { + // Decorate with mongoose validation errors in case of unordered, + // because then still do `insertMany()` + res.mongoose = { + validationErrors: validationErrors + }; + } + return callback(null, res); + } + callback(null, docAttributes); + }); + }); +}; + +/** + * Sends multiple `insertOne`, `updateOne`, `updateMany`, `replaceOne`, + * `deleteOne`, and/or `deleteMany` operations to the MongoDB server in one + * command. This is faster than sending multiple independent operations (like) + * if you use `create()`) because with `bulkWrite()` there is only one round + * trip to MongoDB. + * + * Mongoose will perform casting on all operations you provide. + * + * This function does **not** trigger any middleware, not `save()` nor `update()`. + * If you need to trigger + * `save()` middleware for every document use [`create()`](http://mongoosejs.com/docs/api.html#model_Model.create) instead. + * + * ####Example: + * + * Character.bulkWrite([ + * { + * insertOne: { + * document: { + * name: 'Eddard Stark', + * title: 'Warden of the North' + * } + * } + * }, + * { + * updateOne: { + * filter: { name: 'Eddard Stark' }, + * // If you were using the MongoDB driver directly, you'd need to do + * // `update: { $set: { title: ... } }` but mongoose adds $set for + * // you. + * update: { title: 'Hand of the King' } + * } + * }, + * { + * deleteOne: { + * { + * filter: { name: 'Eddard Stark' } + * } + * } + * } + * ]).then(handleResult); + * + * @param {Array} ops + * @param {Object} [options] + * @param {Function} [callback] callback `function(error, bulkWriteOpResult) {}` + * @return {Promise} resolves to a `BulkWriteOpResult` if the operation succeeds + * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~BulkWriteOpResult + * @api public + */ + +Model.bulkWrite = function(ops, options, callback) { + var Promise = PromiseProvider.get(); + var _this = this; + if (typeof options === 'function') { + callback = options; + options = null; + } + if (callback) { + callback = this.$wrapCallback(callback); + } + options = options || {}; + + var validations = ops.map(function(op) { + if (op['insertOne']) { + return function(callback) { + op['insertOne']['document'] = new _this(op['insertOne']['document']); + op['insertOne']['document'].validate({ __noPromise: true }, function(error) { + if (error) { + return callback(error); + } + callback(null); + }); + }; + } else if (op['updateOne']) { + op = op['updateOne']; + return function(callback) { + try { + op['filter'] = cast(_this.schema, op['filter']); + op['update'] = castUpdate(_this.schema, op['update'], + _this.schema.options.strict); + if (op.setDefaultsOnInsert) { + setDefaultsOnInsert(op['filter'], _this.schema, op['update'], { + setDefaultsOnInsert: true, + upsert: op.upsert + }); + } + } catch (error) { + return callback(error); + } + + callback(null); + }; + } else if (op['updateMany']) { + op = op['updateMany']; + return function(callback) { + try { + op['filter'] = cast(_this.schema, op['filter']); + op['update'] = castUpdate(_this.schema, op['update'], { + strict: _this.schema.options.strict, + overwrite: false + }); + if (op.setDefaultsOnInsert) { + setDefaultsOnInsert(op['filter'], _this.schema, op['update'], { + setDefaultsOnInsert: true, + upsert: op.upsert + }); + } + } catch (error) { + return callback(error); + } + + callback(null); + }; + } else if (op['replaceOne']) { + return function(callback) { + try { + op['replaceOne']['filter'] = cast(_this.schema, + op['replaceOne']['filter']); + } catch (error) { + return callback(error); + } + + // set `skipId`, otherwise we get "_id field cannot be changed" + op['replaceOne']['replacement'] = + new _this(op['replaceOne']['replacement'], null, true); + op['replaceOne']['replacement'].validate({ __noPromise: true }, function(error) { + if (error) { + return callback(error); + } + callback(null); + }); + }; + } else if (op['deleteOne']) { + return function(callback) { + try { + op['deleteOne']['filter'] = cast(_this.schema, + op['deleteOne']['filter']); + } catch (error) { + return callback(error); + } + + callback(null); + }; + } else if (op['deleteMany']) { + return function(callback) { + try { + op['deleteMany']['filter'] = cast(_this.schema, + op['deleteMany']['filter']); + } catch (error) { + return callback(error); } + + callback(null); + }; + } else { + return function(callback) { + callback(new Error('Invalid op passed to `bulkWrite()`')); + }; + } + }); + + var promise = new Promise.ES6(function(resolve, reject) { + parallel(validations, function(error) { + if (error) { + callback && callback(error); + return reject(error); } + + _this.collection.bulkWrite(ops, options, function(error, res) { + if (error) { + callback && callback(error); + return reject(error); + } + + callback && callback(null, res); + resolve(res); + }); }); }); @@ -1795,7 +2460,10 @@ Model.hydrate = function(obj) { }; /** - * Updates documents in the database without returning them. + * Updates one document in the database without returning it. + * + * This function triggers the following middleware: + * - `update()` * * ####Examples: * @@ -1810,6 +2478,8 @@ Model.hydrate = function(obj) { * - `safe` (boolean) safe mode (defaults to value set in schema (true)) * - `upsert` (boolean) whether to create the doc if it doesn't match (false) * - `multi` (boolean) whether multiple documents should be updated (false) + * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema. + * - `setDefaultsOnInsert`: if this and `upsert` are true, mongoose will apply the [defaults](http://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://docs.mongodb.org/v2.4/reference/operator/update/setOnInsert/). * - `strict` (boolean) overrides the `strict` option for this update * - `overwrite` (boolean) disables update-only mode, allowing you to overwrite the doc (false) * @@ -1827,13 +2497,13 @@ Model.hydrate = function(obj) { * ####Example: * * var query = { name: 'borne' }; - * Model.update(query, { name: 'jason borne' }, options, callback) + * Model.update(query, { name: 'jason bourne' }, options, callback) * * // is sent as - * Model.update(query, { $set: { name: 'jason borne' }}, options, callback) + * Model.update(query, { $set: { name: 'jason bourne' }}, options, callback) * // if overwrite option is false. If overwrite is true, sent without the $set wrapper. * - * This helps prevent accidentally overwriting all documents in your collection with `{ name: 'jason borne' }`. + * This helps prevent accidentally overwriting all documents in your collection with `{ name: 'jason bourne' }`. * * ####Note: * @@ -1858,7 +2528,7 @@ Model.hydrate = function(obj) { * * Model.findOne({ name: 'borne' }, function (err, doc) { * if (err) .. - * doc.name = 'jason borne'; + * doc.name = 'jason bourne'; * doc.save(callback); * }) * @@ -1866,31 +2536,116 @@ Model.hydrate = function(obj) { * @see response http://docs.mongodb.org/v2.6/reference/command/update/#output * @param {Object} conditions * @param {Object} doc - * @param {Object} [options] + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Function} [callback] * @return {Query} * @api public */ Model.update = function update(conditions, doc, options, callback) { - var mq = new Query({}, {}, this, this.collection); + return _update(this, 'update', conditions, doc, options, callback); +}; + +/** + * Same as `update()`, except MongoDB will update _all_ documents that match + * `criteria` (as opposed to just the first one) regardless of the value of + * the `multi` option. + * + * **Note** updateMany will _not_ fire update middleware. Use `pre('updateMany')` + * and `post('updateMany')` instead. + * + * This function triggers the following middleware: + * - `updateMany()` + * + * @param {Object} conditions + * @param {Object} doc + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) + * @param {Function} [callback] + * @return {Query} + * @api public + */ + +Model.updateMany = function updateMany(conditions, doc, options, callback) { + return _update(this, 'updateMany', conditions, doc, options, callback); +}; + +/** + * Same as `update()`, except MongoDB will update _only_ the first document that + * matches `criteria` regardless of the value of the `multi` option. + * + * This function triggers the following middleware: + * - `updateOne()` + * + * @param {Object} conditions + * @param {Object} doc + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) + * @param {Function} [callback] + * @return {Query} + * @api public + */ + +Model.updateOne = function updateOne(conditions, doc, options, callback) { + return _update(this, 'updateOne', conditions, doc, options, callback); +}; + +/** + * Same as `update()`, except MongoDB replace the existing document with the + * given document (no atomic operators like `$set`). + * + * This function triggers the following middleware: + * - `replaceOne()` + * + * @param {Object} conditions + * @param {Object} doc + * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) + * @param {Function} [callback] + * @return {Query} + * @api public + */ + +Model.replaceOne = function replaceOne(conditions, doc, options, callback) { + return _update(this, 'replaceOne', conditions, doc, options, callback); +}; + +/*! + * ignore + */ + +function _update(model, op, conditions, doc, options, callback) { + var mq = new model.Query({}, {}, model, model.collection); + if (callback) { + callback = model.$wrapCallback(callback); + } // gh-2406 // make local deep copy of conditions if (conditions instanceof Document) { conditions = conditions.toObject(); } else { - conditions = utils.clone(conditions, { retainKeyOrder: true }); + conditions = utils.clone(conditions, {retainKeyOrder: true}); } options = typeof options === 'function' ? options : utils.clone(options); - return mq.update(conditions, doc, options, callback); -}; + if (model.schema.options.versionKey && options && options.upsert) { + if (options.overwrite) { + doc[model.schema.options.versionKey] = 0; + } else { + if (!doc.$setOnInsert) { + doc.$setOnInsert = {}; + } + doc.$setOnInsert[model.schema.options.versionKey] = 0; + } + } + + return mq[op](conditions, doc, options, callback); +} /** * Executes a mapReduce command. * * `o` is an object specifying all mapReduce options as well as the map and reduce functions. All options are delegated to the driver implementation. See [node-mongodb-native mapReduce() documentation](http://mongodb.github.io/node-mongodb-native/api-generated/collection.html#mapreduce) for more detail about options. * + * This function does not trigger any middleware. + * * ####Example: * * var o = {}; @@ -1937,9 +2692,13 @@ Model.update = function update(conditions, doc, options, callback) { * }); * }) * - * // a promise is returned so you may instead write + * // `mapReduce()` returns a promise. However, ES6 promises can only + * // resolve to exactly one value, + * o.resolveToObject = true; * var promise = User.mapReduce(o); - * promise.then(function (model, stats) { + * promise.then(function (res) { + * var model = res.model; + * var stats = res.stats; * console.log('map reduce took %d ms', stats.processtime) * return model.find().where('value').gt(10).exec(); * }).then(function (docs) { @@ -1954,53 +2713,71 @@ Model.update = function update(conditions, doc, options, callback) { */ Model.mapReduce = function mapReduce(o, callback) { - var promise = new Promise(callback); - var self = this; - - if (!Model.mapReduce.schema) { - var opts = { noId: true, noVirtualId: true, strict: false }; - Model.mapReduce.schema = new Schema({}, opts); + var _this = this; + if (callback) { + callback = this.$wrapCallback(callback); } + var resolveToObject = o.resolveToObject; + var Promise = PromiseProvider.get(); + return new Promise.ES6(function(resolve, reject) { + if (!Model.mapReduce.schema) { + var opts = {noId: true, noVirtualId: true, strict: false}; + Model.mapReduce.schema = new Schema({}, opts); + } - if (!o.out) o.out = { inline: 1 }; - if (false !== o.verbose) o.verbose = true; - - o.map = String(o.map); - o.reduce = String(o.reduce); - - if (o.query) { - var q = new Query(o.query); - q.cast(this); - o.query = q._conditions; - q = undefined; - } + if (!o.out) o.out = {inline: 1}; + if (o.verbose !== false) o.verbose = true; - this.collection.mapReduce(null, null, o, function(err, ret, stats) { - if (err) return promise.error(err); + o.map = String(o.map); + o.reduce = String(o.reduce); - if (ret.findOne && ret.mapReduce) { - // returned a collection, convert to Model - var model = Model.compile( - '_mapreduce_' + ret.collectionName - , Model.mapReduce.schema - , ret.collectionName - , self.db - , self.base); + if (o.query) { + var q = new _this.Query(o.query); + q.cast(_this); + o.query = q._conditions; + q = undefined; + } - model._mapreduce = true; + _this.collection.mapReduce(null, null, o, function(err, ret, stats) { + if (err) { + callback && callback(err); + reject(err); + return; + } - return promise.fulfill(model, stats); - } + if (ret.findOne && ret.mapReduce) { + // returned a collection, convert to Model + var model = Model.compile( + '_mapreduce_' + ret.collectionName + , Model.mapReduce.schema + , ret.collectionName + , _this.db + , _this.base); + + model._mapreduce = true; + + callback && callback(null, model, stats); + return resolveToObject ? resolve({ + model: model, + stats: stats + }) : resolve(model, stats); + } - promise.fulfill(ret, stats); + callback && callback(null, ret, stats); + if (resolveToObject) { + return resolve({ model: ret, stats: stats }); + } + resolve(ret, stats); + }); }); - - return promise; }; /** * geoNear support for Mongoose * + * This function does not trigger any middleware. In particular, this + * bypasses `find()` middleware. + * * ####Options: * - `lean` {Boolean} return the raw object * - All options supported by the driver are also supported @@ -2018,8 +2795,8 @@ Model.mapReduce = function mapReduce(o, callback) { * console.log(results); * }); * - * @param {Object/Array} GeoJSON point or legacy coordinate pair [x,y] to search near - * @param {Object} options for the qurery + * @param {Object|Array} GeoJSON point or legacy coordinate pair [x,y] to search near + * @param {Object} options for the query * @param {Function} [callback] optional callback for the query * @return {Promise} * @see http://docs.mongodb.org/manual/core/2dsphere/ @@ -2028,24 +2805,28 @@ Model.mapReduce = function mapReduce(o, callback) { */ Model.geoNear = function(near, options, callback) { - if ('function' == typeof options) { + if (typeof options === 'function') { callback = options; options = {}; } - var self = this; + if (callback) { + callback = this.$wrapCallback(callback); + } + + var _this = this; var Promise = PromiseProvider.get(); if (!near) { return new Promise.ES6(function(resolve, reject) { var error = new Error('Must pass a near option to geoNear'); reject(error); callback && callback(error); - return; }); } - var x,y; - var _this = this; + var x; + var y; + var schema = this.schema; return new Promise.ES6(function(resolve, reject) { var handler = function(err, res) { @@ -2062,35 +2843,43 @@ Model.geoNear = function(near, options, callback) { var count = res.results.length; // if there are no results, fulfill the promise now - if (count == 0) { + if (count === 0) { resolve(res.results, res.stats); callback && callback(null, res.results, res.stats); return; } var errSeen = false; + + function init(err) { + if (err && !errSeen) { + errSeen = true; + reject(err); + callback && callback(err); + return; + } + if (--count <= 0) { + resolve(res.results, res.stats); + callback && callback(null, res.results, res.stats); + } + } + for (var i = 0; i < res.results.length; i++) { var temp = res.results[i].obj; - res.results[i].obj = new self(); - res.results[i].obj.init(temp, function(err) { - if (err && !errSeen) { - errSeen = true; - reject(err); - callback && callback(err); - return; - } - if (--count <= 0) { - resolve(res.results, res.stats); - callback && callback(null, res.results, res.stats); - } - }); + res.results[i].obj = new _this(); + res.results[i].obj.init(temp, init); } }; + if (options.query != null) { + options.query = utils.clone(options.query, { retainKeyOrder: 1 }); + cast(schema, options.query); + } + if (Array.isArray(near)) { if (near.length !== 2) { var error = new Error('If using legacy coordinates, must be an array ' + - 'of size 2 for geoNear'); + 'of size 2 for geoNear'); reject(error); callback && callback(error); return; @@ -2099,9 +2888,9 @@ Model.geoNear = function(near, options, callback) { y = near[1]; _this.collection.geoNear(x, y, options, handler); } else { - if (near.type != 'Point' || !Array.isArray(near.coordinates)) { + if (near.type !== 'Point' || !Array.isArray(near.coordinates)) { error = new Error('Must pass either a legacy coordinate array or ' + - 'GeoJSON Point to geoNear'); + 'GeoJSON Point to geoNear'); reject(error); callback && callback(error); return; @@ -2117,16 +2906,18 @@ Model.geoNear = function(near, options, callback) { * * If a `callback` is passed, the `aggregate` is executed and a `Promise` is returned. If a callback is not passed, the `aggregate` itself is returned. * + * This function does not trigger any middleware. + * * ####Example: * * // Find the max balance of all accounts * Users.aggregate( - * { $group: { _id: null, maxBalance: { $max: '$balance' }}} - * , { $project: { _id: 0, maxBalance: 1 }} - * , function (err, res) { - * if (err) return handleError(err); - * console.log(res); // [ { maxBalance: 98000 } ] - * }); + * { $group: { _id: null, maxBalance: { $max: '$balance' }}}, + * { $project: { _id: 0, maxBalance: 1 }}, + * function (err, res) { + * if (err) return handleError(err); + * console.log(res); // [ { maxBalance: 98000 } ] + * }); * * // Or use the aggregation pipeline builder. * Users.aggregate() @@ -2156,11 +2947,11 @@ Model.aggregate = function aggregate() { aggregate, callback; - if ('function' === typeof args[args.length - 1]) { + if (typeof args[args.length - 1] === 'function') { callback = args.pop(); } - if (1 === args.length && util.isArray(args[0])) { + if (args.length === 1 && util.isArray(args[0])) { aggregate = new Aggregate(args[0]); } else { aggregate = new Aggregate(args); @@ -2168,16 +2959,22 @@ Model.aggregate = function aggregate() { aggregate.model(this); - if ('undefined' === typeof callback) { + if (typeof callback === 'undefined') { return aggregate; } - return aggregate.exec(callback); + if (callback) { + callback = this.$wrapCallback(callback); + } + + aggregate.exec(callback); }; /** * Implements `$geoSearch` functionality for Mongoose * + * This function does not trigger any middleware + * * ####Example: * * var options = { near: [10, 10], maxDistance: 5 }; @@ -2191,8 +2988,9 @@ Model.aggregate = function aggregate() { * - `limit` {Number} The maximum number of results to return * - `lean` {Boolean} return the raw object instead of the Mongoose Model * - * @param {Object} condition an object that specifies the match condition (required) - * @param {Object} options for the geoSearch, some (near, maxDistance) are required + * @param {Object} conditions an object that specifies the match condition (required) + * @param {Object} [options] for the geoSearch, some (near, maxDistance) are required + * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](http://mongoosejs.com/docs/api.html#query_Query-lean). * @param {Function} [callback] optional callback * @return {Promise} * @see http://docs.mongodb.org/manual/reference/command/geoSearch/ @@ -2201,58 +2999,73 @@ Model.aggregate = function aggregate() { */ Model.geoSearch = function(conditions, options, callback) { - if ('function' == typeof options) { + if (typeof options === 'function') { callback = options; options = {}; } - - var promise = new Promise(callback); - - if (conditions == undefined || !utils.isObject(conditions)) { - return promise.error(new Error("Must pass conditions to geoSearch")); - } - - if (!options.near) { - return promise.error(new Error("Must specify the near option in geoSearch")); - } - - if (!Array.isArray(options.near)) { - return promise.error(new Error("near option must be an array [x, y]")); + if (callback) { + callback = this.$wrapCallback(callback); } + var _this = this; + var Promise = PromiseProvider.get(); + return new Promise.ES6(function(resolve, reject) { + var error; + if (conditions === undefined || !utils.isObject(conditions)) { + error = new Error('Must pass conditions to geoSearch'); + } else if (!options.near) { + error = new Error('Must specify the near option in geoSearch'); + } else if (!Array.isArray(options.near)) { + error = new Error('near option must be an array [x, y]'); + } - // send the conditions in the options object - options.search = conditions; - var self = this; - - this.collection.geoHaystackSearch(options.near[0], options.near[1], options, function(err, res) { - // have to deal with driver problem. Should be fixed in a soon-ish release - // (7/8/2013) - if (err || res.errmsg) { - if (!err) err = new Error(res.errmsg); - if (res && res.code !== undefined) err.code = res.code; - return promise.error(err); + if (error) { + callback && callback(error); + reject(error); + return; } - var count = res.results.length; - if (options.lean || (count == 0)) return promise.fulfill(res.results, res.stats); + // send the conditions in the options object + options.search = conditions; + + _this.collection.geoHaystackSearch(options.near[0], options.near[1], options, function(err, res) { + // have to deal with driver problem. Should be fixed in a soon-ish release + // (7/8/2013) + if (err) { + callback && callback(err); + reject(err); + return; + } + + var count = res.results.length; + if (options.lean || count === 0) { + callback && callback(null, res.results, res.stats); + resolve(res.results, res.stats); + return; + } + + var errSeen = false; - var errSeen = false; - for (var i = 0; i < res.results.length; i++) { - var temp = res.results[i]; - res.results[i] = new self(); - res.results[i].init(temp, {}, function(err) { + function init(err) { if (err && !errSeen) { - errSeen = true; - return promise.error(err); + callback && callback(err); + reject(err); + return; } - --count || (!errSeen && promise.fulfill(res.results, res.stats)); - }); - } - }); + if (!--count && !errSeen) { + callback && callback(null, res.results, res.stats); + resolve(res.results, res.stats); + } + } - return promise; + for (var i = 0; i < res.results.length; i++) { + var temp = res.results[i]; + res.results[i] = new _this(); + res.results[i].init(temp, {}, init); + } + }); + }); }; /** @@ -2277,8 +3090,8 @@ Model.geoSearch = function(conditions, options, callback) { * * User.populate(user, opts, function (err, user) { * console.log(user); - * }) - * }) + * }); + * }); * * // populates an array of objects * User.find(match, function (err, users) { @@ -2291,6 +3104,11 @@ Model.geoSearch = function(conditions, options, callback) { * // imagine a Weapon model exists with two saved documents: * // { _id: 389, name: 'whip' } * // { _id: 8921, name: 'boomerang' } + * // and this schema: + * // new Schema({ + * // name: String, + * // weapon: { type: ObjectId, ref: 'Weapon' } + * // }); * * var user = { name: 'Indiana Jones', weapon: 389 } * Weapon.populate(user, { path: 'weapon', model: 'Weapon' }, function (err, user) { @@ -2305,104 +3123,84 @@ Model.geoSearch = function(conditions, options, callback) { * console.log('%s uses a %s', users.name, user.weapon.name) * // Indiana Jones uses a whip * // Batman uses a boomerang - * }) - * }) + * }); + * }); * // Note that we didn't need to specify the Weapon model because - * // we were already using it's populate() method. + * // it is in the schema's ref * * @param {Document|Array} docs Either a single document or array of documents to populate. * @param {Object} options A hash of key/val (path, options) used for population. - * @param {Function} [cb(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`. + * @param {Function} [callback(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`. * @return {Promise} * @api public */ -Model.populate = function(docs, paths, cb) { - var promise = new Promise(cb); - - // always resolve on nextTick for consistent async behavior - function resolve() { - var args = utils.args(arguments); - - process.nextTick(function() { - promise.resolve.apply(promise, args); - }); +Model.populate = function(docs, paths, callback) { + var _this = this; + if (callback) { + callback = this.$wrapCallback(callback); } // normalized paths + var noPromise = paths && !!paths.__noPromise; paths = utils.populate(paths); - var pending = paths.length; - if (0 === pending) { - resolve(null, docs); - return promise; - } + // data that should persist across subPopulate calls + var cache = {}; - // each path has its own query options and must be executed separately - var i = pending; - var path; - var model = this; - while (i--) { - path = paths[i]; - if ('function' === typeof path.model) model = path.model; - populate(model, docs, path, subPopulate.call(model, docs, path, next)); - } - - return promise; - - function next(err) { - if (err) return resolve(err); - if (--pending) return; - resolve(null, docs); + if (noPromise) { + _populate(this, docs, paths, cache, callback); + } else { + var Promise = PromiseProvider.get(); + return new Promise.ES6(function(resolve, reject) { + _populate(_this, docs, paths, cache, function(error, docs) { + if (error) { + callback && callback(error); + reject(error); + } else { + callback && callback(null, docs); + resolve(docs); + } + }); + }); } }; /*! - * Populates deeply if `populate` option is present. + * Populate helper * - * @param {Document|Array} docs - * @param {Object} options - * @param {Function} cb + * @param {Model} model the model to use + * @param {Document|Array} docs Either a single document or array of documents to populate. + * @param {Object} paths + * @param {Function} [cb(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`. * @return {Function} * @api private */ -function subPopulate(docs, options, cb) { - var model = this; - var prefix = options.path + '.'; - var pop = options.populate; - if (!pop) { - return cb; - } +function _populate(model, docs, paths, cache, callback) { + var pending = paths.length; - // normalize as array - if (!Array.isArray(pop)) { - pop = [pop]; + if (pending === 0) { + return callback(null, docs); } - return function(err) { - var pending = pop.length; + // each path has its own query options and must be executed separately + var i = pending; + var path; + while (i--) { + path = paths[i]; + populate(model, docs, path, next); + } - function next(err) { - if (err) return cb(err); - if (--pending) return; - cb(); + function next(err) { + if (err) { + return callback(err); } - - if (err || !pending) return cb(err); - - pop.forEach(function(subOptions) { - // path needs parent's path prefixed to it - if (!subOptions._originalPath) { - subOptions._originalPath = subOptions.path; - subOptions.path = prefix + subOptions.path; - } - if (typeof subOptions.model === 'string') { - subOptions.model = model.model(subOptions.model); - } - Model.populate.call(subOptions.model || model, docs, subOptions, next); - }); - }; + if (--pending) { + return; + } + callback(null, docs); + } } /*! @@ -2411,56 +3209,35 @@ function subPopulate(docs, options, cb) { var excludeIdReg = /\s?-_id\s?/, excludeIdRegGlobal = /\s?-_id\s?/g; -function populate(model, docs, options, cb) { - var modelsMap, rawIds; +function populate(model, docs, options, callback) { + var modelsMap; // normalize single / multiple docs passed if (!Array.isArray(docs)) { docs = [docs]; } - if (0 === docs.length || docs.every(utils.isNullOrUndefined)) { - return cb(); + if (docs.length === 0 || docs.every(utils.isNullOrUndefined)) { + return callback(); } modelsMap = getModelsMapForPopulate(model, docs, options); - rawIds = getIdsForAndAddIdsInMapPopulate(modelsMap); + if (modelsMap instanceof Error) { + return setImmediate(function() { + callback(modelsMap); + }); + } var i, len = modelsMap.length, - mod, match, select, promise, vals = []; - - promise = new Promise(function(err, vals, options, assignmentOpts) { - if (err) return cb(err); - - var lean = options.options && options.options.lean, - len = vals.length, - rawOrder = {}, rawDocs = {}, key, val; - - // optimization: - // record the document positions as returned by - // the query result. - for (var i = 0; i < len; i++) { - val = vals[i]; - key = String(utils.getValue('_id', val)); - rawDocs[key] = val; - rawOrder[key] = i; - - // flag each as result of population - if (!lean) val.$__.wasPopulated = true; - } + mod, match, select, vals = []; - assignVals({ - rawIds: rawIds, - rawDocs: rawDocs, - rawOrder: rawOrder, - docs: docs, - path: options.path, - options: assignmentOpts - }); - cb(); - }); + function flatten(item) { + // no need to include undefined values in our query + return undefined !== item; + } var _remaining = len; + var hasOne = false; for (i = 0; i < len; i++) { mod = modelsMap[i]; select = mod.options.select; @@ -2471,30 +3248,28 @@ function populate(model, docs, options, cb) { match = {}; } - var ids = utils.array.flatten(mod.ids, function(item) { - // no need to include undefined values in our query - return undefined !== item; - }); - + var ids = utils.array.flatten(mod.ids, flatten); ids = utils.array.unique(ids); - if (0 === ids.length || ids.every(utils.isNullOrUndefined)) { - return cb(); + if (ids.length === 0 || ids.every(utils.isNullOrUndefined)) { + --_remaining; + continue; } - match._id || (match._id = { - $in: ids - }); + hasOne = true; + if (mod.foreignField !== '_id' || !match['_id']) { + match[mod.foreignField] = { $in: ids }; + } var assignmentOpts = {}; assignmentOpts.sort = mod.options.options && mod.options.options.sort || undefined; - assignmentOpts.excludeId = excludeIdReg.test(select) || (select && 0 === select._id); + assignmentOpts.excludeId = excludeIdReg.test(select) || (select && select._id === 0); if (assignmentOpts.excludeId) { // override the exclusion from the query so we can use the _id // for document matching during assignment. we'll delete the // _id back off before returning the result. - if ('string' == typeof select) { + if (typeof select === 'string') { select = select.replace(excludeIdRegGlobal, ' '); } else { // preserve original select conditions by copying @@ -2508,146 +3283,501 @@ function populate(model, docs, options, cb) { mod.options.options.limit = mod.options.options.limit * ids.length; } - mod.Model.find(match, select, mod.options.options, next.bind(this, mod.options, assignmentOpts)); + var subPopulate = utils.clone(mod.options.populate, { + retainKeyOrder: true + }); + var query = mod.Model.find(match, select, mod.options.options); + + // If we're doing virtual populate and projection is inclusive and foreign + // field is not selected, automatically select it because mongoose needs it. + // If projection is exclusive and client explicitly unselected the foreign + // field, that's the client's fault. + if (mod.foreignField !== '_id' && query.selectedInclusively() && + !isPathSelectedInclusive(query._fields, mod.foreignField)) { + query.select(mod.foreignField); + } + + // If we need to sub-populate, call populate recursively + if (subPopulate) { + query.populate(subPopulate); + } + + query.exec(next.bind(this, mod, assignmentOpts)); + } + + if (!hasOne) { + return callback(); } function next(options, assignmentOpts, err, valsFromDb) { - if (err) return promise.resolve(err); + if (mod.options.options && mod.options.options.limit) { + mod.options.options.limit = assignmentOpts.originalLimit; + } + + if (err) return callback(err); vals = vals.concat(valsFromDb); + _assign(null, vals, options, assignmentOpts); if (--_remaining === 0) { - promise.resolve(err, vals, options, assignmentOpts); + callback(); } } + + function _assign(err, vals, mod, assignmentOpts) { + if (err) return callback(err); + + var options = mod.options; + var isVirtual = mod.isVirtual; + var justOne = mod.justOne; + var _val; + var lean = options.options && options.options.lean; + var len = vals.length; + var rawOrder = {}; + var rawDocs = {}; + var key; + var val; + + // Clone because `assignRawDocsToIdStructure` will mutate the array + var allIds = [].concat(mod.allIds.map(function(v) { + if (Array.isArray(v)) { + return [].concat(v); + } + return v; + })); + + // optimization: + // record the document positions as returned by + // the query result. + for (var i = 0; i < len; i++) { + val = vals[i]; + if (val) { + _val = utils.getValue(mod.foreignField, val); + if (Array.isArray(_val)) { + var _valLength = _val.length; + for (var j = 0; j < _valLength; ++j) { + var __val = _val[j]; + if (__val instanceof Document) { + __val = __val._id; + } + key = String(__val); + if (rawDocs[key]) { + if (Array.isArray(rawDocs[key])) { + rawDocs[key].push(val); + rawOrder[key].push(i); + } else { + rawDocs[key] = [rawDocs[key], val]; + rawOrder[key] = [rawOrder[key], i]; + } + } else { + if (isVirtual && !justOne) { + rawDocs[key] = [val]; + rawOrder[key] = [i]; + } else { + rawDocs[key] = val; + rawOrder[key] = i; + } + } + } + } else { + if (_val instanceof Document) { + _val = _val._id; + } + key = String(_val); + if (rawDocs[key]) { + if (Array.isArray(rawDocs[key])) { + rawDocs[key].push(val); + rawOrder[key].push(i); + } else { + rawDocs[key] = [rawDocs[key], val]; + rawOrder[key] = [rawOrder[key], i]; + } + } else { + rawDocs[key] = val; + rawOrder[key] = i; + } + } + // flag each as result of population + if (!lean) { + val.$__.wasPopulated = true; + } + } + } + + assignVals({ + originalModel: model, + rawIds: mod.allIds, + allIds: allIds, + localField: mod.localField, + foreignField: mod.foreignField, + rawDocs: rawDocs, + rawOrder: rawOrder, + docs: mod.docs, + path: options.path, + options: assignmentOpts, + justOne: mod.justOne, + isVirtual: mod.isVirtual, + allOptions: mod + }); + } } -function getModelsMapForPopulate(model, docs, options) { - var i, doc, len = docs.length, - available = {}, - map = [], - modelNameFromQuery = options.model && options.model.modelName || options.model, - schema, refPath, Model, currentOptions, modelNames, modelName, discriminatorKey, modelForFindSchema; +/*! + * Assigns documents returned from a population query back + * to the original document path. + */ + +function assignVals(o) { + // replace the original ids in our intermediate _ids structure + // with the documents found by query + assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, o.options, + o.localField, o.foreignField); + + // now update the original documents being populated using the + // result structure that contains real documents. - schema = model._getSchema(options.path); + var docs = o.docs; + var rawIds = o.rawIds; + var options = o.options; - if (schema && schema.caster) { - schema = schema.caster; + function setValue(val) { + return valueFilter(val, options); } - if (!schema && model.discriminators) { - discriminatorKey = model.schema.discriminatorMapping.key; + for (var i = 0; i < docs.length; ++i) { + if (utils.getValue(o.path, docs[i]) == null && + !o.originalModel.schema._getVirtual(o.path)) { + continue; + } + + if (o.isVirtual && !o.justOne && !Array.isArray(rawIds[i])) { + if (rawIds[i] == null) { + rawIds[i] = []; + } else { + rawIds[i] = [rawIds[i]]; + } + } + + if (o.isVirtual && docs[i].constructor.name === 'model') { + // If virtual populate and doc is already init-ed, need to walk through + // the actual doc to set rather than setting `_doc` directly + mpath.set(o.path, rawIds[i], docs[i]); + } else { + var parts = o.path.split('.'); + var cur = docs[i]; + for (var j = 0; j < parts.length - 1; ++j) { + if (cur[parts[j]] == null) { + cur[parts[j]] = {}; + } + cur = cur[parts[j]]; + } + if (docs[i].$__) { + o.allOptions.model = o.allOptions.Model; + docs[i].populated(o.path, o.allIds[i], o.allOptions); + } + utils.setValue(o.path, rawIds[i], docs[i], setValue); + } } +} + +/*! + * Assign `vals` returned by mongo query to the `rawIds` + * structure returned from utils.getVals() honoring + * query sort order if specified by user. + * + * This can be optimized. + * + * Rules: + * + * if the value of the path is not an array, use findOne rules, else find. + * for findOne the results are assigned directly to doc path (including null results). + * for find, if user specified sort order, results are assigned directly + * else documents are put back in original order of array if found in results + * + * @param {Array} rawIds + * @param {Array} vals + * @param {Boolean} sort + * @api private + */ - refPath = schema && schema.options && schema.options.refPath; +function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, localFields, foreignFields, recursed) { + // honor user specified sort order + var newOrder = []; + var sorting = options.sort && rawIds.length > 1; + var doc; + var sid; + var id; - for (i = 0; i < len; i++) { - doc = docs[i]; + for (var i = 0; i < rawIds.length; ++i) { + id = rawIds[i]; - if (refPath) { - modelNames = utils.getValue(refPath, doc); + if (Array.isArray(id)) { + // handle [ [id0, id2], [id3] ] + assignRawDocsToIdStructure(id, resultDocs, resultOrder, options, localFields, foreignFields, true); + newOrder.push(id); + continue; + } + + if (id === null && !sorting) { + // keep nulls for findOne unless sorting, which always + // removes them (backward compat) + newOrder.push(id); + continue; + } + + sid = String(id); + + if (recursed) { + // apply find behavior + + // assign matching documents in original order unless sorting + doc = resultDocs[sid]; + if (doc) { + if (sorting) { + newOrder[resultOrder[sid]] = doc; + } else { + newOrder.push(doc); + } + } else { + newOrder.push(id); + } } else { - if (!modelNameFromQuery) { - var schemaForCurrentDoc; + // apply findOne behavior - if document in results, assign, else assign null + newOrder[i] = doc = resultDocs[sid] || null; + } + } - if (!schema && discriminatorKey) { - modelForFindSchema = utils.getValue(discriminatorKey, doc); + rawIds.length = 0; + if (newOrder.length) { + // reassign the documents based on corrected order - if (modelForFindSchema) { - schemaForCurrentDoc = model.db.model(modelForFindSchema)._getSchema(options.path); + // forEach skips over sparse entries in arrays so we + // can safely use this to our advantage dealing with sorted + // result sets too. + newOrder.forEach(function(doc, i) { + if (!doc) { + return; + } + rawIds[i] = doc; + }); + } +} - if (schemaForCurrentDoc && schemaForCurrentDoc.caster) { - schemaForCurrentDoc = schemaForCurrentDoc.caster; - } +function getModelsMapForPopulate(model, docs, options) { + var i, doc, len = docs.length, + available = {}, + map = [], + modelNameFromQuery = options.model && options.model.modelName || options.model, + schema, refPath, Model, currentOptions, modelNames, modelName, discriminatorKey, modelForFindSchema; + + var originalModel = options.model; + var isVirtual = false; + var isRefPathArray = false; + var modelSchema = model.schema; + + for (i = 0; i < len; i++) { + doc = docs[i]; + + schema = getSchemaTypes(modelSchema, doc, options.path); + var isUnderneathDocArray = schema && schema.$isUnderneathDocArray; + if (isUnderneathDocArray && + options && + options.options && + options.options.sort) { + return new Error('Cannot populate with `sort` on path ' + options.path + + ' because it is a subproperty of a document array'); + } + + if (Array.isArray(schema)) { + for (var j = 0; j < schema.length; ++j) { + var _modelNames = _getModelNames(doc, schema[j]); + if (!_modelNames) { + continue; + } + modelNames = (modelNames || []); + for (var x = 0; x < _modelNames.length; ++x) { + if (modelNames.indexOf(_modelNames[x]) === -1) { + modelNames.push(_modelNames[x]); } - } else { - schemaForCurrentDoc = schema; } + } + } else { + modelNames = _getModelNames(doc, schema); + if (!modelNames) { + continue; + } + } - modelNames = [ - schemaForCurrentDoc && schemaForCurrentDoc.options && schemaForCurrentDoc.options.ref // declared in schema - || model.modelName // an ad-hoc structure - ]; + var virtual = model.schema._getVirtual(options.path); + var localField; + if (virtual && virtual.options) { + var virtualPrefix = virtual.$nestedSchemaPath ? + virtual.$nestedSchemaPath + '.' : ''; + if (typeof virtual.options.localField === 'function') { + localField = virtualPrefix + virtual.options.localField.call(doc, doc); } else { - modelNames = [modelNameFromQuery]; // query options + localField = virtualPrefix + virtual.options.localField; } + } else { + localField = options.path; + } + var foreignField = virtual && virtual.options ? + virtual.options.foreignField : + '_id'; + var justOne = virtual && virtual.options && virtual.options.justOne; + if (virtual && virtual.options && virtual.options.ref) { + isVirtual = true; } - if (!modelNames) - continue; + if (virtual && (!localField || !foreignField)) { + throw new Error('If you are populating a virtual, you must set the ' + + 'localField and foreignField options'); + } - if (!Array.isArray(modelNames)) { - modelNames = [modelNames]; + options.isVirtual = isVirtual; + if (typeof localField === 'function') { + localField = localField.call(doc, doc); } + if (typeof foreignField === 'function') { + foreignField = foreignField.call(doc); + } + var ret = convertTo_id(utils.getValue(localField, doc)); + var id = String(utils.getValue(foreignField, doc)); + options._docs[id] = Array.isArray(ret) ? ret.slice() : ret; var k = modelNames.length; while (k--) { modelName = modelNames[k]; + if (modelName == null) { + continue; + } + var _doc = Array.isArray(doc) && isRefPathArray ? doc[k] : doc; + var _ret = Array.isArray(ret) && isRefPathArray ? ret[k] : ret; + try { + Model = originalModel && originalModel.modelName ? + originalModel : + model.db.model(modelName); + } catch (error) { + return error; + } + if (!available[modelName]) { - Model = model.db.model(modelName); currentOptions = { model: Model }; - if (schema && !discriminatorKey) { - options.model = Model; + if (isVirtual && virtual.options && virtual.options.options) { + currentOptions.options = utils.clone(virtual.options.options, { + retainKeyOrder: true + }); } - utils.merge(currentOptions, options); + if (schema && !discriminatorKey) { + currentOptions.model = Model; + } + options.model = Model; available[modelName] = { Model: Model, options: currentOptions, - docs: [doc], - ids: [] + docs: [_doc], + ids: [_ret], + allIds: [ret], + // Assume only 1 localField + foreignField + localField: localField, + foreignField: foreignField, + justOne: justOne, + isVirtual: isVirtual }; map.push(available[modelName]); } else { - available[modelName].docs.push(doc); + available[modelName].docs.push(_doc); + available[modelName].ids.push(_ret); + available[modelName].allIds.push(ret); } - } } - return map; -} + function _getModelNames(doc, schema) { + var modelNames; + var discriminatorKey; -function getIdsForAndAddIdsInMapPopulate(modelsMap) { - var rawIds = [], // for the correct position - i, j, doc, docs, id, len, len2, ret, isDocument, options, path; - - len2 = modelsMap.length; - for (j = 0; j < len2; j++) { - docs = modelsMap[j].docs; - len = docs.length; - options = modelsMap[j].options; - path = options.path; - - for (i = 0; i < len; i++) { - ret = undefined; - doc = docs[i]; - id = String(utils.getValue("_id", doc)); - isDocument = !!doc.$__; - - if (!ret || Array.isArray(ret) && 0 === ret.length) { - ret = utils.getValue(path, doc); - } + if (schema && schema.caster) { + schema = schema.caster; + } - if (ret) { - ret = convertTo_id(ret); + if (!schema && model.discriminators) { + discriminatorKey = model.schema.discriminatorMapping.key; + } - options._docs[id] = Array.isArray(ret) ? ret.slice() : ret; - } + refPath = schema && schema.options && schema.options.refPath; - rawIds.push(ret); - modelsMap[j].ids.push(ret); + if (refPath) { + modelNames = utils.getValue(refPath, doc); + isRefPathArray = Array.isArray(modelNames); + } else { + if (!modelNameFromQuery) { + var modelForCurrentDoc = model; + var schemaForCurrentDoc; + + if (!schema && discriminatorKey) { + modelForFindSchema = utils.getValue(discriminatorKey, doc); + + if (modelForFindSchema) { + try { + modelForCurrentDoc = model.db.model(modelForFindSchema); + } catch (error) { + return error; + } + + schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path); - if (isDocument) { - // cache original populated _ids and model used - doc.populated(path, options._docs[id], options); + if (schemaForCurrentDoc && schemaForCurrentDoc.caster) { + schemaForCurrentDoc = schemaForCurrentDoc.caster; + } + } + } else { + schemaForCurrentDoc = schema; + } + var virtual = modelForCurrentDoc.schema._getVirtual(options.path); + + var ref; + if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) { + modelNames = [ref]; + } else if ((ref = get(virtual, 'options.ref')) != null) { + if (typeof ref === 'function') { + ref = ref.call(doc, doc); + } + + // When referencing nested arrays, the ref should be an Array + // of modelNames. + if (Array.isArray(ref)) { + modelNames = ref; + } else { + modelNames = [ref]; + } + + isVirtual = true; + } else { + // We may have a discriminator, in which case we don't want to + // populate using the base model by default + modelNames = discriminatorKey ? null : [model.modelName]; + } + } else { + modelNames = [modelNameFromQuery]; // query options } } + + if (!modelNames) { + return; + } + + if (!Array.isArray(modelNames)) { + modelNames = [modelNames]; + } + + return modelNames; } - return rawIds; + return map; } /*! @@ -2666,40 +3796,16 @@ function convertTo_id(val) { val[i] = val[i]._id; } } - return val; + if (val.isMongooseArray) { + return val._schema.cast(val, val._parent); + } + + return [].concat(val); } return val; } -/*! - * Assigns documents returned from a population query back - * to the original document path. - */ - -function assignVals(o) { - // replace the original ids in our intermediate _ids structure - // with the documents found by query - - assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, o.options); - - // now update the original documents being populated using the - // result structure that contains real documents. - - var docs = o.docs; - var path = o.path; - var rawIds = o.rawIds; - var options = o.options; - - for (var i = 0; i < docs.length; ++i) { - if (utils.getValue(path, docs[i]) == null) - continue; - utils.setValue(path, rawIds[i], docs[i], function(val) { - return valueFilter(val, options); - }); - } -} - /*! * 1) Apply backwards compatible find/findOne behavior to sub documents * @@ -2760,7 +3866,7 @@ function valueFilter(val, assignmentOpts) { function maybeRemoveId(subdoc, assignmentOpts) { if (assignmentOpts.excludeId) { - if ('function' == typeof subdoc.setValue) { + if (typeof subdoc.setValue === 'function') { delete subdoc._doc._id; } else { delete subdoc._id; @@ -2774,124 +3880,35 @@ function maybeRemoveId(subdoc, assignmentOpts) { */ function isDoc(doc) { - if (null == doc) + if (doc == null) { return false; + } var type = typeof doc; - if ('string' == type) + if (type === 'string') { return false; + } - if ('number' == type) + if (type === 'number') { return false; + } - if (Buffer.isBuffer(doc)) + if (Buffer.isBuffer(doc)) { return false; + } - if ('ObjectID' == doc.constructor.name) + if (doc.constructor.name === 'ObjectID') { return false; + } // only docs return true; } -/*! - * Assign `vals` returned by mongo query to the `rawIds` - * structure returned from utils.getVals() honoring - * query sort order if specified by user. - * - * This can be optimized. - * - * Rules: - * - * if the value of the path is not an array, use findOne rules, else find. - * for findOne the results are assigned directly to doc path (including null results). - * for find, if user specified sort order, results are assigned directly - * else documents are put back in original order of array if found in results - * - * @param {Array} rawIds - * @param {Array} vals - * @param {Boolean} sort - * @api private - */ - -function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, recursed) { - // honor user specified sort order - var newOrder = []; - var sorting = options.sort && rawIds.length > 1; - var doc; - var sid; - var id; - - for (var i = 0; i < rawIds.length; ++i) { - id = rawIds[i]; - - if (Array.isArray(id)) { - // handle [ [id0, id2], [id3] ] - assignRawDocsToIdStructure(id, resultDocs, resultOrder, options, true); - newOrder.push(id); - continue; - } - - if (null === id && !sorting) { - // keep nulls for findOne unless sorting, which always - // removes them (backward compat) - newOrder.push(id); - continue; - } - - sid = String(id); - - if (recursed) { - // apply find behavior - - // assign matching documents in original order unless sorting - doc = resultDocs[sid]; - if (doc) { - if (sorting) { - newOrder[resultOrder[sid]] = doc; - } else { - newOrder.push(doc); - } - } else { - newOrder.push(id); - } - } else { - // apply findOne behavior - if document in results, assign, else assign null - newOrder[i] = doc = resultDocs[sid] || null; - } - } - - rawIds.length = 0; - if (newOrder.length) { - // reassign the documents based on corrected order - - // forEach skips over sparse entries in arrays so we - // can safely use this to our advantage dealing with sorted - // result sets too. - newOrder.forEach(function(doc, i) { - rawIds[i] = doc; - }); - } -} - -/** - * Finds the schema for `path`. This is different than - * calling `schema.path` as it also resolves paths with - * positional selectors (something.$.another.$.path). - * - * @param {String} path - * @return {Schema} - * @api private - */ - -Model._getSchema = function _getSchema(path) { - return this.schema._getSchema(path); -}; - /*! * Compiler utility. * - * @param {String} name model name + * @param {String|Function} name model name or class extending Model * @param {Schema} schema * @param {String} collectionName * @param {Connection} connection @@ -2899,7 +3916,7 @@ Model._getSchema = function _getSchema(path) { */ Model.compile = function compile(name, schema, collectionName, connection, base) { - var versioningEnabled = false !== schema.options.versionKey; + var versioningEnabled = schema.options.versionKey !== false; if (versioningEnabled && !schema.paths[schema.options.versionKey]) { // add versioning to top level documents only @@ -2908,81 +3925,110 @@ Model.compile = function compile(name, schema, collectionName, connection, base) schema.add(o); } - // generate new class - function model(doc, fields, skipId) { - if (!(this instanceof model)) - return new model(doc, fields, skipId); - Model.call(this, doc, fields, skipId); + var model; + if (typeof name === 'function' && name.prototype instanceof Model) { + model = name; + name = model.name; + schema.loadClass(model, false); + model.prototype.$isMongooseModelPrototype = true; + } else { + // generate new class + model = function model(doc, fields, skipId) { + if (!(this instanceof model)) { + return new model(doc, fields, skipId); + } + Model.call(this, doc, fields, skipId); + }; } model.hooks = schema.s.hooks.clone(); model.base = base; model.modelName = name; - model.__proto__ = Model; - model.prototype.__proto__ = Model.prototype; + if (!(model.prototype instanceof Model)) { + model.__proto__ = Model; + model.prototype.__proto__ = Model.prototype; + } model.model = Model.prototype.model; model.db = model.prototype.db = connection; model.discriminators = model.prototype.discriminators = undefined; model.prototype.$__setSchema(schema); + var _userProvidedOptions = schema._userProvidedOptions || {}; + var bufferCommands = true; + if (connection.config.bufferCommands != null) { + bufferCommands = connection.config.bufferCommands; + } + if (_userProvidedOptions.bufferCommands != null) { + bufferCommands = _userProvidedOptions.bufferCommands; + } + var collectionOptions = { - bufferCommands: schema.options.bufferCommands, + bufferCommands: bufferCommands, capped: schema.options.capped }; model.prototype.collection = connection.collection( collectionName - , collectionOptions + , collectionOptions ); // apply methods and statics applyMethods(model, schema); applyStatics(model, schema); + applyHooks(model, schema); model.schema = model.prototype.schema; model.collection = model.prototype.collection; + // Create custom query constructor + model.Query = function() { + Query.apply(this, arguments); + this.options.retainKeyOrder = model.schema.options.retainKeyOrder; + }; + model.Query.prototype = Object.create(Query.prototype); + model.Query.base = Query.base; + applyQueryMethods(model, schema.query); + + var kareemOptions = { + useErrorHandlers: true, + numCallbackParams: 1 + }; + model.$__insertMany = model.hooks.createWrapper('insertMany', + model.insertMany, model, kareemOptions); + model.insertMany = function(arr, options, callback) { + var Promise = PromiseProvider.get(); + if (typeof options === 'function') { + callback = options; + options = null; + } + return new Promise.ES6(function(resolve, reject) { + model.$__insertMany(arr, options, function(error, result) { + if (error) { + callback && callback(error); + return reject(error); + } + callback && callback(null, result); + resolve(result); + }); + }); + }; + return model; }; /*! - * Register methods for this model + * Register custom query methods for this model * * @param {Model} model * @param {Schema} schema */ -var applyMethods = function(model, schema) { - for (var i in schema.methods) { - if (typeof schema.methods[i] === 'function') { - model.prototype[i] = schema.methods[i]; - } else { - (function(_i) { - Object.defineProperty(model.prototype, _i, { - get: function() { - var h = {}; - for (var k in schema.methods[_i]) { - h[k] = schema.methods[_i][k].bind(this); - } - return h; - }, - configurable: true - }); - })(i); - } - } -}; -/*! - * Register statics for this model - * @param {Model} model - * @param {Schema} schema - */ -var applyStatics = function(model, schema) { - for (var i in schema.statics) { - model[i] = schema.statics[i]; +function applyQueryMethods(model, methods) { + for (var i in methods) { + model.Query.prototype[i] = methods[i]; } -}; +} /*! * Subclass this model with `conn`, `schema`, and `collection` settings. @@ -2995,32 +4041,42 @@ var applyStatics = function(model, schema) { Model.__subclass = function subclass(conn, schema, collection) { // subclass model using this connection and collection name - var model = this; + var _this = this; var Model = function Model(doc, fields, skipId) { if (!(this instanceof Model)) { return new Model(doc, fields, skipId); } - model.call(this, doc, fields, skipId); + _this.call(this, doc, fields, skipId); }; - Model.__proto__ = model; - Model.prototype.__proto__ = model.prototype; + Model.__proto__ = _this; + Model.prototype.__proto__ = _this.prototype; Model.db = Model.prototype.db = conn; - var s = schema && 'string' != typeof schema - ? schema - : model.prototype.schema; + var s = schema && typeof schema !== 'string' + ? schema + : _this.prototype.schema; var options = s.options || {}; + var _userProvidedOptions = s._userProvidedOptions || {}; if (!collection) { - collection = model.prototype.schema.get('collection') - || utils.toCollectionName(model.modelName, options); + collection = _this.prototype.schema.get('collection') + || utils.toCollectionName(_this.modelName, options); } + var bufferCommands = true; + if (s) { + if (conn.config.bufferCommands != null) { + bufferCommands = conn.config.bufferCommands; + } + if (_userProvidedOptions.bufferCommands != null) { + bufferCommands = _userProvidedOptions.bufferCommands; + } + } var collectionOptions = { - bufferCommands: s ? options.bufferCommands : true, + bufferCommands: bufferCommands, capped: s && options.capped }; @@ -3030,6 +4086,17 @@ Model.__subclass = function subclass(conn, schema, collection) { return Model; }; +Model.$wrapCallback = function(callback) { + var _this = this; + return function() { + try { + callback.apply(null, arguments); + } catch (error) { + _this.emit('error', error); + } + }; +}; + /*! * Module exports. */ diff --git a/lib/plugins/idGetter.js b/lib/plugins/idGetter.js new file mode 100644 index 00000000000..186aceb55c3 --- /dev/null +++ b/lib/plugins/idGetter.js @@ -0,0 +1,26 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function(schema) { + // ensure the documents receive an id getter unless disabled + var autoIdGetter = !schema.paths['id'] && + (!schema.options.noVirtualId && schema.options.id); + if (autoIdGetter) { + schema.virtual('id').get(idGetter); + } +}; + +/*! + * Returns this documents _id cast to a string. + */ + +function idGetter() { + if (this._id != null) { + return String(this._id); + } + + return null; +} diff --git a/lib/plugins/saveSubdocs.js b/lib/plugins/saveSubdocs.js new file mode 100644 index 00000000000..ffb90b4d4a1 --- /dev/null +++ b/lib/plugins/saveSubdocs.js @@ -0,0 +1,37 @@ +'use strict'; + +var each = require('async/each'); + +/*! + * ignore + */ + +module.exports = function(schema) { + schema.callQueue.unshift(['pre', ['save', function(next) { + if (this.ownerDocument) { + next(); + return; + } + + var _this = this; + var subdocs = this.$__getAllSubdocs(); + + if (!subdocs.length) { + next(); + return; + } + + each(subdocs, function(subdoc, cb) { + subdoc.save(function(err) { + cb(err); + }); + }, function(error) { + if (error) { + return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + next(error); + }); + } + next(); + }); + }]]); +}; diff --git a/lib/plugins/sharding.js b/lib/plugins/sharding.js new file mode 100644 index 00000000000..1d5cf5944ec --- /dev/null +++ b/lib/plugins/sharding.js @@ -0,0 +1,76 @@ +'use strict'; + +var utils = require('../utils'); + +/*! + * ignore + */ + +module.exports = function shardingPlugin(schema) { + schema.post('init', function() { + storeShard.call(this); + return this; + }); + schema.pre('save', function(next) { + applyWhere.call(this); + next(); + }); + schema.post('save', function() { + storeShard.call(this); + }); +}; + +/*! + * ignore + */ + +function applyWhere() { + var paths; + var len; + + if (this.$__.shardval) { + paths = Object.keys(this.$__.shardval); + len = paths.length; + + this.$where = this.$where || {}; + for (var i = 0; i < len; ++i) { + this.$where[paths[i]] = this.$__.shardval[paths[i]]; + } + } +} + +/*! + * ignore + */ + +module.exports.storeShard = storeShard; + +/*! + * ignore + */ + +function storeShard() { + // backwards compat + var key = this.schema.options.shardKey || this.schema.options.shardkey; + if (!(key && utils.getFunctionName(key.constructor) === 'Object')) { + return; + } + + var orig = this.$__.shardval = {}, + paths = Object.keys(key), + len = paths.length, + val; + + for (var i = 0; i < len; ++i) { + val = this.getValue(paths[i]); + if (utils.isMongooseObject(val)) { + orig[paths[i]] = val.toObject({depopulate: true, _isNested: true}); + } else if (val !== null && val !== undefined && val.valueOf && + // Explicitly don't take value of dates + (!val.constructor || utils.getFunctionName(val.constructor) !== 'Date')) { + orig[paths[i]] = val.valueOf(); + } else { + orig[paths[i]] = val; + } + } +} diff --git a/lib/plugins/validateBeforeSave.js b/lib/plugins/validateBeforeSave.js new file mode 100644 index 00000000000..706cc0692b3 --- /dev/null +++ b/lib/plugins/validateBeforeSave.js @@ -0,0 +1,47 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function(schema) { + schema.callQueue.unshift(['pre', ['save', function(next, options) { + var _this = this; + // Nested docs have their own presave + if (this.ownerDocument) { + return next(); + } + + var hasValidateBeforeSaveOption = options && + (typeof options === 'object') && + ('validateBeforeSave' in options); + + var shouldValidate; + if (hasValidateBeforeSaveOption) { + shouldValidate = !!options.validateBeforeSave; + } else { + shouldValidate = this.schema.options.validateBeforeSave; + } + + // Validate + if (shouldValidate) { + // HACK: use $__original_validate to avoid promises so bluebird doesn't + // complain + if (this.$__original_validate) { + this.$__original_validate({__noPromise: true}, function(error) { + return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + next(error); + }); + }); + } else { + this.validate({__noPromise: true}, function(error) { + return _this.schema.s.hooks.execPost('save:error', _this, [ _this], { error: error }, function(error) { + next(error); + }); + }); + } + } else { + next(); + } + }]]); +}; diff --git a/lib/promise.js b/lib/promise.js index 403eaa37b3d..1e1ba6955e1 100644 --- a/lib/promise.js +++ b/lib/promise.js @@ -66,6 +66,29 @@ Promise.prototype = Object.create(MPromise.prototype, { } }); +/*! + * ignore + */ + +Promise.prototype.then = util.deprecate(Promise.prototype.then, + 'Mongoose: mpromise (mongoose\'s default promise library) is deprecated, ' + + 'plug in your own promise library instead: ' + + 'http://mongoosejs.com/docs/promises.html'); + +/** + * ES6-style `.catch()` shorthand + * + * @method catch + * @memberOf Promise + * @param {Function} onReject + * @return {Promise} + * @api public + */ + +Promise.prototype.catch = function(onReject) { + return this.then(null, onReject); +}; + /*! * Override event names for backward compatibility. */ @@ -235,7 +258,7 @@ Promise.prototype.addErrback = Promise.prototype.onReject; * var ids = meetups.map(function (m) { * return m._id; * }); - * return People.find({ meetups: { $in: ids }).exec(); + * return People.find({ meetups: { $in: ids } }).exec(); * }).then(function (people) { * if (people.length < 10000) { * throw new Error('Too few people!!!'); diff --git a/lib/query.js b/lib/query.js index 591f75bfe0f..2d0f28011ea 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1,19 +1,25 @@ -/* eslint no-unused-vars: 1 */ - /*! * Module dependencies. */ +var CastError = require('./error/cast'); +var ObjectParameterError = require('./error/objectParameter'); +var PromiseProvider = require('./promise_provider'); +var QueryCursor = require('./cursor/QueryCursor'); +var QueryStream = require('./querystream'); +var cast = require('./cast'); +var castUpdate = require('./services/query/castUpdate'); +var hasDollarKeys = require('./services/query/hasDollarKeys'); +var helpers = require('./queryhelpers'); +var isInclusive = require('./services/projection/isInclusive'); var mquery = require('mquery'); -var util = require('util'); var readPref = require('./drivers').ReadPreference; -var PromiseProvider = require('./promise_provider'); +var selectPopulatedFields = require('./services/query/selectPopulatedFields'); +var setDefaultsOnInsert = require('./services/setDefaultsOnInsert'); +var slice = require('sliced'); var updateValidators = require('./services/updateValidators'); +var util = require('util'); var utils = require('./utils'); -var helpers = require('./queryhelpers'); -var Document = require('./document'); -var QueryStream = require('./querystream'); -var cast = require('./cast'); /** * Query constructor used for building queries. @@ -70,19 +76,35 @@ function Query(conditions, options, model, collection) { this.find(conditions); } + this.options = this.options || {}; + if (this.schema != null && this.schema.options.collation != null) { + this.options.collation = this.schema.options.collation; + } + if (this.schema) { + var kareemOptions = { + useErrorHandlers: true, + numCallbackParams: 1, + nullResultByDefault: true + }; this._count = this.model.hooks.createWrapper('count', - Query.prototype._count, this); + Query.prototype._count, this, kareemOptions); this._execUpdate = this.model.hooks.createWrapper('update', - Query.prototype._execUpdate, this); + Query.prototype._execUpdate, this, kareemOptions); this._find = this.model.hooks.createWrapper('find', - Query.prototype._find, this); + Query.prototype._find, this, kareemOptions); this._findOne = this.model.hooks.createWrapper('findOne', - Query.prototype._findOne, this); + Query.prototype._findOne, this, kareemOptions); this._findOneAndRemove = this.model.hooks.createWrapper('findOneAndRemove', - Query.prototype._findOneAndRemove, this); + Query.prototype._findOneAndRemove, this, kareemOptions); this._findOneAndUpdate = this.model.hooks.createWrapper('findOneAndUpdate', - Query.prototype._findOneAndUpdate, this); + Query.prototype._findOneAndUpdate, this, kareemOptions); + this._replaceOne = this.model.hooks.createWrapper('replaceOne', + Query.prototype._replaceOne, this, kareemOptions); + this._updateMany = this.model.hooks.createWrapper('updateMany', + Query.prototype._updateMany, this, kareemOptions); + this._updateOne = this.model.hooks.createWrapper('updateOne', + Query.prototype._updateOne, this, kareemOptions); } } @@ -152,12 +174,15 @@ Query.use$geoWithin = mquery.use$geoWithin; */ Query.prototype.toConstructor = function toConstructor() { + var model = this.model; + var coll = this.mongooseCollection; + var CustomQuery = function(criteria, options) { if (!(this instanceof CustomQuery)) { return new CustomQuery(criteria, options); } this._mongooseOptions = utils.clone(p._mongooseOptions); - Query.call(this, criteria, options || null); + Query.call(this, criteria, options || null, model, coll); }; util.inherits(CustomQuery, Query); @@ -170,14 +195,15 @@ Query.prototype.toConstructor = function toConstructor() { p.setOptions(this.options); p.op = this.op; - p._conditions = utils.clone(this._conditions); + p._conditions = utils.clone(this._conditions, { retainKeyOrder: true }); p._fields = utils.clone(this._fields); - p._update = utils.clone(this._update); + p._update = utils.clone(this._update, { + flattenDecimals: false, + retainKeyOrder: true + }); p._path = this._path; p._distinct = this._distinct; p._collection = this._collection; - p.model = this.model; - p.mongooseCollection = this.mongooseCollection; p._mongooseOptions = this._mongooseOptions; return CustomQuery; @@ -239,6 +265,49 @@ Query.prototype.toConstructor = function toConstructor() { * @api public */ +Query.prototype.slice = function() { + if (arguments.length === 0) { + return this; + } + + this._validate('slice'); + + var path; + var val; + + if (arguments.length === 1) { + var arg = arguments[0]; + if (typeof arg === 'object' && !Array.isArray(arg)) { + var keys = Object.keys(arg); + var numKeys = keys.length; + for (var i = 0; i < numKeys; ++i) { + this.slice(keys[i], arg[keys[i]]); + } + return this; + } + this._ensurePath('slice'); + path = this._path; + val = arguments[0]; + } else if (arguments.length === 2) { + if ('number' === typeof arguments[0]) { + this._ensurePath('slice'); + path = this._path; + val = slice(arguments); + } else { + path = arguments[0]; + val = arguments[1]; + } + } else if (arguments.length === 3) { + path = arguments[0]; + val = slice(arguments, 1); + } + + var p = {}; + p[path] = { $slice: val }; + return this.select(p); +}; + + /** * Specifies the complementary comparison value for paths specified with `where()` * @@ -444,7 +513,7 @@ Query.prototype.toConstructor = function toConstructor() { * @method regex * @memberOf Query * @param {String} [path] - * @param {Number} val + * @param {String|RegExp} val * @api public */ @@ -462,17 +531,51 @@ Query.prototype.toConstructor = function toConstructor() { */ /** - * Specifies a `$mod` condition + * Specifies a `$mod` condition, filters documents for documents whose + * `path` property is a number that is equal to `remainder` modulo `divisor`. + * + * ####Example + * + * // All find products whose inventory is odd + * Product.find().mod('inventory', [2, 1]); + * Product.find().where('inventory').mod([2, 1]); + * // This syntax is a little strange, but supported. + * Product.find().where('inventory').mod(2, 1); * * @method mod * @memberOf Query * @param {String} [path] - * @param {Number} val + * @param {Array} val must be of length 2, first element is `divisor`, 2nd element is `remainder`. * @return {Query} this * @see $mod http://docs.mongodb.org/manual/reference/operator/mod/ * @api public */ +Query.prototype.mod = function() { + var val; + var path; + + if (arguments.length === 1) { + this._ensurePath('mod'); + val = arguments[0]; + path = this._path; + } else if (arguments.length === 2 && !Array.isArray(arguments[1])) { + this._ensurePath('mod'); + val = slice(arguments); + path = this._path; + } else if (arguments.length === 3) { + val = slice(arguments, 1); + path = arguments[0]; + } else { + val = arguments[1]; + path = arguments[0]; + } + + var conds = this._conditions[path] || (this._conditions[path] = {}); + conds.$mod = val; + return this; +}; + /** * Specifies an `$exists` condition * @@ -716,6 +819,10 @@ Query.prototype.toConstructor = function toConstructor() { * * When using string syntax, prefixing a path with `-` will flag that path as excluded. When a path does not have the `-` prefix, it is included. Lastly, if a path is prefixed with `+`, it forces inclusion of the path, which is useful for paths excluded at the [schema level](/docs/api.html#schematype_SchemaType-select). * + * A projection _must_ be either inclusive or exclusive. In other words, you must + * either list the fields to include (which excludes all others), or list the fields + * to exclude (which implies all other fields are included). The [`_id` field is the only exception because MongoDB includes it by default](https://docs.mongodb.com/manual/tutorial/project-fields-from-query-results/#suppress-id-field). + * * ####Example * * // include a and b, exclude other fields @@ -732,12 +839,6 @@ Query.prototype.toConstructor = function toConstructor() { * // force inclusion of field excluded at schema level * query.select('+path') * - * ####NOTE: - * - * Cannot be used with `distinct()`. - * - * _v2 had slightly different syntax such as allowing arrays of field names. This support was removed in v3._ - * * @method select * @memberOf Query * @param {Object|String} arg @@ -746,6 +847,51 @@ Query.prototype.toConstructor = function toConstructor() { * @api public */ +Query.prototype.select = function select() { + var arg = arguments[0]; + if (!arg) return this; + var i; + var len; + + if (arguments.length !== 1) { + throw new Error('Invalid select: select only takes 1 argument'); + } + + this._validate('select'); + + var fields = this._fields || (this._fields = {}); + var userProvidedFields = this._userProvidedFields || (this._userProvidedFields = {}); + var type = typeof arg; + + if (('string' == type || Object.prototype.toString.call(arg) === '[object Arguments]') && + 'number' == typeof arg.length || Array.isArray(arg)) { + if ('string' == type) + arg = arg.split(/\s+/); + + for (i = 0, len = arg.length; i < len; ++i) { + var field = arg[i]; + if (!field) continue; + var include = '-' == field[0] ? 0 : 1; + if (include === 0) field = field.substring(1); + fields[field] = include; + userProvidedFields[field] = include; + } + + return this; + } + + if (utils.isObject(arg)) { + var keys = Object.keys(arg); + for (i = 0; i < keys.length; ++i) { + fields[keys[i]] = arg[keys[i]]; + userProvidedFields[keys[i]] = arg[keys[i]]; + } + return this; + } + + throw new TypeError('Invalid select() argument. Must be string or object.'); +}; + /** * _DEPRECATED_ Sets the slaveOk option. * @@ -821,8 +967,9 @@ Query.prototype.toConstructor = function toConstructor() { Query.prototype.read = function read(pref, tags) { // first cast into a ReadPreference object to support tags - var read = readPref.apply(readPref, arguments); - return Query.base.read.call(this, read); + var read = readPref.call(readPref, pref, tags); + this.options.readPreference = read; + return this; }; /** @@ -839,25 +986,34 @@ Query.prototype.read = function read(pref, tags) { */ /** - * Sets query options. + * Sets query options. Some options only make sense for certain operations. * * ####Options: * - * - [tailable](http://www.mongodb.org/display/DOCS/Tailable+Cursors) * - * - [sort](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Bsort(\)%7D%7D) * - * - [limit](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Blimit%28%29%7D%7D) * - * - [skip](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Bskip%28%29%7D%7D) * - * - [maxscan](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24maxScan) * - * - [batchSize](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7BbatchSize%28%29%7D%7D) * - * - [comment](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24comment) * - * - [snapshot](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Bsnapshot%28%29%7D%7D) * - * - [hint](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24hint) * - * - [readPreference](http://docs.mongodb.org/manual/applications/replication/#read-preference) ** - * - [lean](./api.html#query_Query-lean) * - * - [safe](http://www.mongodb.org/display/DOCS/getLastError+Command) - * - * _* denotes a query helper method is also available_ - * _** query helper method to set `readPreference` is `read()`_ + * The following options are only for `find()`: + * - [tailable](http://www.mongodb.org/display/DOCS/Tailable+Cursors) + * - [sort](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Bsort(\)%7D%7D) + * - [limit](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Blimit%28%29%7D%7D) + * - [skip](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Bskip%28%29%7D%7D) + * - [maxscan](https://docs.mongodb.org/v3.2/reference/operator/meta/maxScan/#metaOp._S_maxScan) + * - [batchSize](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7BbatchSize%28%29%7D%7D) + * - [comment](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24comment) + * - [snapshot](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%7B%7Bsnapshot%28%29%7D%7D) + * - [readPreference](http://docs.mongodb.org/manual/applications/replication/#read-preference) + * - [hint](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24hint) + * + * The following options are only for `update()`, `updateOne()`, `updateMany()`, `replaceOne()`, `findOneAndUpdate()`, and `findByIdAndUpdate()`: + * - [upsert](https://docs.mongodb.com/manual/reference/method/db.collection.update/) + * - [writeConcern](https://docs.mongodb.com/manual/reference/method/db.collection.update/) + * + * The following options are only for `find()`, `findOne()`, `findById()`, `findOneAndUpdate()`, and `findByIdAndUpdate()`: + * - [lean](./api.html#query_Query-lean) + * + * The following options are only for all operations **except** `update()`, `updateOne()`, `updateMany()`, `remove()`, `deleteOne()`, and `deleteMany()`: + * - [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) + * + * The following options are for all operations: + * - [collation](https://docs.mongodb.com/manual/reference/collation/) * * @param {Object} options * @api public @@ -876,10 +1032,19 @@ Query.prototype.setOptions = function(options, overwrite) { return this; } - if (!(options && 'Object' == options.constructor.name)) { + if (options == null) { return this; } + if (Array.isArray(options.populate)) { + var populate = options.populate; + delete options.populate; + var _numPopulate = populate.length; + for (var i = 0; i < _numPopulate; ++i) { + this.populate(populate[i]); + } + } + return Query.base.setOptions.call(this, options); }; @@ -934,6 +1099,48 @@ Query.prototype.getUpdate = function() { * @receiver Query */ +Query.prototype._updateForExec = function() { + var update = utils.clone(this._update, { + retainKeyOrder: true, + transform: false, + depopulate: true + }); + var ops = Object.keys(update); + var i = ops.length; + var ret = {}; + + while (i--) { + var op = ops[i]; + + if (this.options.overwrite) { + ret[op] = update[op]; + continue; + } + + if ('$' !== op[0]) { + // fix up $set sugar + if (!ret.$set) { + if (update.$set) { + ret.$set = update.$set; + } else { + ret.$set = {}; + } + } + ret.$set[op] = update[op]; + ops.splice(i, 1); + if (!~ops.indexOf('$set')) ops.push('$set'); + } else if ('$set' === op) { + if (!ret.$set) { + ret[op] = update[op]; + } + } else { + ret[op] = update[op]; + } + } + + return ret; +}; + /** * Makes sure _path is set. * @@ -964,21 +1171,26 @@ Query.prototype._optionsForExec = function(model) { var options = Query.base._optionsForExec.call(this); delete options.populate; + delete options.retainKeyOrder; model = model || this.model; if (!model) { return options; - } else { - if (!('safe' in options) && model.schema.options.safe) { - options.safe = model.schema.options.safe; - } + } - if (!('readPreference' in options) && model.schema.options.read) { - options.readPreference = model.schema.options.read; - } + if (!('safe' in options) && model.schema.options.safe) { + options.safe = model.schema.options.safe; + } - return options; + if (!('readPreference' in options) && model.schema.options.read) { + options.readPreference = model.schema.options.read; + } + + if (options.upsert !== void 0) { + options.upsert = !!options.upsert; } + + return options; }; /** @@ -998,16 +1210,95 @@ Query.prototype._optionsForExec = function(model) { * * This is a [great](https://groups.google.com/forum/#!topic/mongoose-orm/u2_DzDydcnA/discussion) option in high-performance read-only scenarios, especially when combined with [stream](#query_Query-stream). * - * @param {Boolean} bool defaults to true + * @param {Boolean|Object} bool defaults to true * @return {Query} this * @api public */ Query.prototype.lean = function(v) { - this._mongooseOptions.lean = arguments.length ? !!v : true; + this._mongooseOptions.lean = arguments.length ? v : true; + return this; +}; + +/** + * Gets/sets the error flag on this query. If this flag is not null or + * undefined, the `exec()` promise will reject without executing. + * + * ####Example: + * + * Query().error(); // Get current error value + * Query().error(null); // Unset the current error + * Query().error(new Error('test')); // `exec()` will resolve with test + * Schema.pre('find', function() { + * if (!this.getQuery().userId) { + * this.error(new Error('Not allowed to query without setting userId')); + * } + * }); + * + * Note that query casting runs **after** hooks, so cast errors will override + * custom errors. + * + * ####Example: + * var TestSchema = new Schema({ num: Number }); + * var TestModel = db.model('Test', TestSchema); + * TestModel.find({ num: 'not a number' }).error(new Error('woops')).exec(function(error) { + * // `error` will be a cast error because `num` failed to cast + * }); + * + * @param {Error|null} err if set, `exec()` will fail fast before sending the query to MongoDB + * @returns {Query} this + * @api public + */ + +Query.prototype.error = function error(err) { + if (arguments.length === 0) { + return this._error; + } + + this._error = err; return this; }; +/*! + * ignore + */ + +Query.prototype._unsetCastError = function _unsetCastError() { + if (this._error != null && !(this._error instanceof CastError)) { + return; + } + return this.error(null); +}; + +/** + * Getter/setter around the current mongoose-specific options for this query + * (populate, lean, etc.) + * + * @param {Object} options if specified, overwrites the current options + * @returns {Object} the options + * @api public + */ + +Query.prototype.mongooseOptions = function(v) { + if (arguments.length > 0) { + this._mongooseOptions = v; + } + return this._mongooseOptions; +}; + +/*! + * ignore + */ + +Query.prototype._castConditions = function() { + try { + this.cast(this.model); + this._unsetCastError(); + } catch (err) { + this.error(err); + } +}; + /** * Thunk around find() * @@ -1016,8 +1307,10 @@ Query.prototype.lean = function(v) { * @api private */ Query.prototype._find = function(callback) { - if (this._castError) { - callback(this._castError); + this._castConditions(); + + if (this.error() != null) { + callback(this.error()); return this; } @@ -1025,8 +1318,9 @@ Query.prototype._find = function(callback) { this._fields = this._castFields(this._fields); var fields = this._fieldsForExec(); - var options = this._mongooseOptions; - var self = this; + var mongooseOptions = this._mongooseOptions; + var _this = this; + var userProvidedFields = _this._userProvidedFields || {}; var cb = function(err, docs) { if (err) { @@ -1037,22 +1331,26 @@ Query.prototype._find = function(callback) { return callback(null, docs); } - if (!options.populate) { - return true === options.lean - ? callback(null, docs) - : completeMany(self.model, docs, fields, self, null, callback); + if (!mongooseOptions.populate) { + return !!mongooseOptions.lean === true + ? callback(null, docs) + : completeMany(_this.model, docs, fields, userProvidedFields, null, callback); } - var pop = helpers.preparePopulationOptionsMQ(self, options); - self.model.populate(docs, pop, function(err, docs) { + var pop = helpers.preparePopulationOptionsMQ(_this, mongooseOptions); + pop.__noPromise = true; + _this.model.populate(docs, pop, function(err, docs) { if (err) return callback(err); - return true === options.lean - ? callback(null, docs) - : completeMany(self.model, docs, fields, self, pop, callback); + return !!mongooseOptions.lean === true + ? callback(null, docs) + : completeMany(_this.model, docs, fields, userProvidedFields, pop, callback); }); }; - return Query.base.find.call(this, {}, cb); + var options = this._optionsForExec(); + options.fields = this._fieldsForExec(); + var filter = this._conditions; + return this._collection.find(filter, options, cb); }; /** @@ -1064,14 +1362,14 @@ Query.prototype._find = function(callback) { * * query.find({ name: 'Los Pollos Hermanos' }).find(callback) * - * @param {Object} [criteria] mongodb selector + * @param {Object} [filter] mongodb selector * @param {Function} [callback] * @return {Query} this * @api public */ Query.prototype.find = function(conditions, callback) { - if ('function' == typeof conditions) { + if (typeof conditions === 'function') { callback = conditions; conditions = {}; } @@ -1080,15 +1378,10 @@ Query.prototype.find = function(conditions, callback) { if (mquery.canMerge(conditions)) { this.merge(conditions); - } - - prepareDiscriminatorCriteria(this); - try { - this.cast(this.model); - this._castError = null; - } catch (err) { - this._castError = err; + prepareDiscriminatorCriteria(this); + } else if (conditions != null) { + this.error(new ObjectParameterError(conditions, 'filter', 'find')); } // if we don't have a callback, then just return the query object @@ -1101,6 +1394,57 @@ Query.prototype.find = function(conditions, callback) { return this; }; +/** + * Merges another Query or conditions object into this one. + * + * When a Query is passed, conditions, field selection and options are merged. + * + * @param {Query|Object} source + * @return {Query} this + */ + +Query.prototype.merge = function(source) { + if (!source) { + return this; + } + + var opts = { retainKeyOrder: this.options.retainKeyOrder, overwrite: true }; + + if (source instanceof Query) { + // if source has a feature, apply it to ourselves + + if (source._conditions) { + utils.merge(this._conditions, source._conditions, opts); + } + + if (source._fields) { + this._fields || (this._fields = {}); + utils.merge(this._fields, source._fields, opts); + } + + if (source.options) { + this.options || (this.options = {}); + utils.merge(this.options, source.options, opts); + } + + if (source._update) { + this._update || (this._update = {}); + utils.mergeClone(this._update, source._update); + } + + if (source._distinct) { + this._distinct = source._distinct; + } + + return this; + } + + // plain object + utils.merge(this._conditions, source, opts); + + return this; +}; + /*! * hydrates many documents * @@ -1112,22 +1456,45 @@ Query.prototype.find = function(conditions, callback) { * @param {Function} callback */ -function completeMany(model, docs, fields, self, pop, callback) { +function completeMany(model, docs, fields, userProvidedFields, pop, callback) { var arr = []; var count = docs.length; var len = count; - var opts = pop ? - { populated: pop } - : undefined; + var opts = pop ? { populated: pop } : undefined; + var error = null; + function init(_error) { + if (error != null) { + return; + } + if (_error != null) { + error = _error; + return callback(error); + } + --count || callback(null, arr); + } for (var i = 0; i < len; ++i) { - arr[i] = helpers.createModel(model, docs[i], fields); - arr[i].init(docs[i], opts, function(err) { - if (err) return callback(err); - --count || callback(null, arr); - }); + arr[i] = helpers.createModel(model, docs[i], fields, userProvidedFields); + arr[i].init(docs[i], opts, init); } } +/** + * Adds a collation to this op (MongoDB 3.4 and up) + * + * @param {Object} value + * @return {Query} this + * @see MongoDB docs https://docs.mongodb.com/manual/reference/method/cursor.collation/#cursor.collation + * @api public + */ + +Query.prototype.collation = function(value) { + if (this.options == null) { + this.options = {}; + } + this.options.collation = value; + return this; +}; + /** * Thunk around findOne() * @@ -1137,8 +1504,10 @@ function completeMany(model, docs, fields, self, pop, callback) { */ Query.prototype._findOne = function(callback) { - if (this._castError) { - return callback(this._castError); + this._castConditions(); + + if (this.error()) { + return callback(this.error()); } this._applyPaths(); @@ -1146,10 +1515,11 @@ Query.prototype._findOne = function(callback) { var options = this._mongooseOptions; var projection = this._fieldsForExec(); - var self = this; + var userProvidedFields = this._userProvidedFields || {}; + var _this = this; // don't pass in the conditions because we already merged them in - Query.base.findOne.call(self, {}, function(err, doc) { + Query.base.findOne.call(_this, {}, function(err, doc) { if (err) { return callback(err); } @@ -1158,20 +1528,20 @@ Query.prototype._findOne = function(callback) { } if (!options.populate) { - return true === options.lean - ? callback(null, doc) - : completeOne(self.model, doc, null, projection, self, null, callback); + return !!options.lean === true + ? callback(null, doc) + : completeOne(_this.model, doc, null, {}, projection, userProvidedFields, null, callback); } - var pop = helpers.preparePopulationOptionsMQ(self, options); - self.model.populate(doc, pop, function(err, doc) { + var pop = helpers.preparePopulationOptionsMQ(_this, options); + pop.__noPromise = true; + _this.model.populate(doc, pop, function(err, doc) { if (err) { return callback(err); } - - return true === options.lean - ? callback(null, doc) - : completeOne(self.model, doc, null, projection, self, pop, callback); + return !!options.lean === true + ? callback(null, doc) + : completeOne(_this.model, doc, null, {}, projection, userProvidedFields, pop, callback); }); }); }; @@ -1181,6 +1551,14 @@ Query.prototype._findOne = function(callback) { * * Passing a `callback` executes the query. The result of the query is a single document. * + * * *Note:* `conditions` is optional, and if `conditions` is null or undefined, + * mongoose will send an empty `findOne` command to MongoDB, which will return + * an arbitrary document. If you're querying by `_id`, use `Model.findById()` + * instead. + * + * This function triggers the following middleware: + * - `findOne()` + * * ####Example * * var query = Kitten.where({ color: 'white' }); @@ -1191,25 +1569,27 @@ Query.prototype._findOne = function(callback) { * } * }); * - * @param {Object|Query} [criteria] mongodb selector - * @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo) - * @param {Function} [callback] + * @param {Object} [filter] mongodb selector + * @param {Object} [projection] optional fields to return + * @param {Object} [options] see [`setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions) + * @param {Function} [callback] optional params are (error, document) * @return {Query} this * @see findOne http://docs.mongodb.org/manual/reference/method/db.collection.findOne/ + * @see Query.select #query_Query-select * @api public */ Query.prototype.findOne = function(conditions, projection, options, callback) { - if ('function' == typeof conditions) { + if (typeof conditions === 'function') { callback = conditions; conditions = null; projection = null; options = null; - } else if ('function' == typeof projection) { + } else if (typeof projection === 'function') { callback = projection; options = null; projection = null; - } else if ('function' == typeof options) { + } else if (typeof options === 'function') { callback = options; options = null; } @@ -1229,15 +1609,17 @@ Query.prototype.findOne = function(conditions, projection, options, callback) { if (mquery.canMerge(conditions)) { this.merge(conditions); - } - prepareDiscriminatorCriteria(this); + prepareDiscriminatorCriteria(this); - try { - this.cast(this.model); - this._castError = null; - } catch (err) { - this._castError = err; + try { + this.cast(this.model); + this.error(null); + } catch (err) { + this.error(err); + } + } else if (conditions != null) { + this.error(new ObjectParameterError(conditions, 'filter', 'findOne')); } if (!callback) { @@ -1262,10 +1644,11 @@ Query.prototype._count = function(callback) { try { this.cast(this.model); } catch (err) { - process.nextTick(function() { - callback(err); - }); - return this; + this.error(err); + } + + if (this.error()) { + return callback(this.error()); } var conds = this._conditions; @@ -1279,6 +1662,9 @@ Query.prototype._count = function(callback) { * * Passing a `callback` executes the query. * + * This function triggers the following middleware: + * - `count()` + * * ####Example: * * var countQuery = model.where({ 'color': 'black' }).count(); @@ -1293,14 +1679,14 @@ Query.prototype._count = function(callback) { * }) * * @param {Object} [criteria] mongodb selector - * @param {Function} [callback] + * @param {Function} [callback] optional params are (error, count) * @return {Query} this * @see count http://docs.mongodb.org/manual/reference/method/db.collection.count/ * @api public */ Query.prototype.count = function(conditions, callback) { - if ('function' == typeof conditions) { + if (typeof conditions === 'function') { callback = conditions; conditions = undefined; } @@ -1324,6 +1710,8 @@ Query.prototype.count = function(conditions, callback) { * * Passing a `callback` executes the query. * + * This function does not trigger any middleware. + * * ####Example * * distinct(field, conditions, callback) @@ -1335,7 +1723,7 @@ Query.prototype.count = function(conditions, callback) { * * @param {String} [field] * @param {Object|Query} [criteria] - * @param {Function} [callback] + * @param {Function} [callback] optional params are (error, arr) * @return {Query} this * @see distinct http://docs.mongodb.org/manual/reference/method/db.collection.distinct/ * @api public @@ -1343,10 +1731,10 @@ Query.prototype.count = function(conditions, callback) { Query.prototype.distinct = function(field, conditions, callback) { if (!callback) { - if ('function' === typeof conditions) { + if (typeof conditions === 'function') { callback = conditions; conditions = undefined; - } else if ('function' === typeof field) { + } else if (typeof field === 'function') { callback = field; field = undefined; conditions = undefined; @@ -1400,34 +1788,18 @@ Query.prototype.distinct = function(field, conditions, callback) { */ Query.prototype.sort = function(arg) { - var nArg = {}; - if (arguments.length > 1) { - throw new Error("sort() only takes 1 Argument"); + throw new Error('sort() only takes 1 Argument'); } - if (Array.isArray(arg)) { - // time to deal with the terrible syntax - for (var i = 0; i < arg.length; i++) { - if (!Array.isArray(arg[i])) throw new Error("Invalid sort() argument."); - nArg[arg[i][0]] = arg[i][1]; - } - - } else { - nArg = arg; - } - - // workaround for gh-2374 when sort is called after count - // if previous operation is count, we ignore - if (this.op == 'count') { - delete this.op; - } - return Query.base.sort.call(this, nArg); + return Query.base.sort.call(this, arg); }; /** * Declare and/or execute this query as a remove() operation. * + * This function does not trigger any middleware + * * ####Example * * Model.remove({ artist: 'Anne Murray' }, callback) @@ -1443,7 +1815,7 @@ Query.prototype.sort = function(arg) { * query.remove({ name: 'Anne Murray' }, callback) * query.remove({ name: 'Anne Murray' }).remove(callback) * - * // executed without a callback (unsafe write) + * // executed without a callback * query.exec() * * // summary @@ -1452,33 +1824,167 @@ Query.prototype.sort = function(arg) { * query.remove(fn) // executes * query.remove() * - * @param {Object|Query} [criteria] mongodb selector - * @param {Function} [callback] + * @param {Object|Query} [filter] mongodb selector + * @param {Function} [callback] optional params are (error, writeOpResult) * @return {Query} this + * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult * @see remove http://docs.mongodb.org/manual/reference/method/db.collection.remove/ * @api public */ -Query.prototype.remove = function(cond, callback) { - if ('function' == typeof cond) { - callback = cond; - cond = null; +Query.prototype.remove = function(filter, callback) { + if (typeof filter === 'function') { + callback = filter; + filter = null; } - var cb = 'function' == typeof callback; + filter = utils.toObject(filter, { retainKeyOrder: true }); try { - this.cast(this.model); + this.cast(this.model, filter); + this.merge(filter); } catch (err) { - if (cb) return process.nextTick(callback.bind(null, err)); - return this; + this.error(err); + } + + prepareDiscriminatorCriteria(this); + + if (!callback) { + return Query.base.remove.call(this); } - return Query.base.remove.call(this, cond, callback); + return this._remove(callback); }; /*! - * hydrates a document + * ignore + */ + +Query.prototype._remove = function(callback) { + if (this.error() != null) { + callback(this.error()); + return this; + } + + return Query.base.remove.call(this, callback); +}; + +/** + * Declare and/or execute this query as a `deleteOne()` operation. Works like + * remove, except it deletes at most one document regardless of the `single` + * option. + * + * This function does not trigger any middleware. + * + * ####Example + * + * Character.deleteOne({ name: 'Eddard Stark' }, callback) + * Character.deleteOne({ name: 'Eddard Stark' }).then(next) + * + * @param {Object|Query} [filter] mongodb selector + * @param {Function} [callback] optional params are (error, writeOpResult) + * @return {Query} this + * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult + * @see remove http://docs.mongodb.org/manual/reference/method/db.collection.remove/ + * @api public + */ + +Query.prototype.deleteOne = function(filter, callback) { + if (typeof filter === 'function') { + callback = filter; + filter = null; + } + + filter = utils.toObject(filter, { retainKeyOrder: true }); + + try { + this.cast(this.model, filter); + this.merge(filter); + } catch (err) { + this.error(err); + } + + prepareDiscriminatorCriteria(this); + + if (!callback) { + return Query.base.deleteOne.call(this); + } + + return this._deleteOne.call(this, callback); +}; + +/*! + * ignore + */ + +Query.prototype._deleteOne = function(callback) { + if (this.error() != null) { + callback(this.error()); + return this; + } + + return Query.base.deleteOne.call(this, callback); +}; + +/** + * Declare and/or execute this query as a `deleteMany()` operation. Works like + * remove, except it deletes _every_ document that matches `criteria` in the + * collection, regardless of the value of `single`. + * + * This function does not trigger any middleware + * + * ####Example + * + * Character.deleteMany({ name: /Stark/, age: { $gte: 18 } }, callback) + * Character.deleteMany({ name: /Stark/, age: { $gte: 18 } }).then(next) + * + * @param {Object|Query} [filter] mongodb selector + * @param {Function} [callback] optional params are (error, writeOpResult) + * @return {Query} this + * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult + * @see remove http://docs.mongodb.org/manual/reference/method/db.collection.remove/ + * @api public + */ + +Query.prototype.deleteMany = function(filter, callback) { + if (typeof filter === 'function') { + callback = filter; + filter = null; + } + + filter = utils.toObject(filter, { retainKeyOrder: true }); + + try { + this.cast(this.model, filter); + this.merge(filter); + } catch (err) { + this.error(err); + } + + prepareDiscriminatorCriteria(this); + + if (!callback) { + return Query.base.deleteMany.call(this); + } + + return this._deleteMany.call(this, callback); +}; + +/*! + * ignore + */ + +Query.prototype._deleteMany = function(callback) { + if (this.error() != null) { + callback(this.error()); + return this; + } + + return Query.base.deleteMany.call(this, callback); +}; + +/*! + * hydrates a document * * @param {Model} model * @param {Document} doc @@ -1489,18 +1995,23 @@ Query.prototype.remove = function(cond, callback) { * @param {Function} callback */ -function completeOne(model, doc, res, fields, self, pop, callback) { +function completeOne(model, doc, res, options, fields, userProvidedFields, pop, callback) { var opts = pop ? - { populated: pop } - : undefined; + {populated: pop} + : undefined; - var casted = helpers.createModel(model, doc, fields); + var casted = helpers.createModel(model, doc, fields, userProvidedFields); casted.init(doc, opts, function(err) { if (err) { return callback(err); } - if (res) { - return callback(null, casted, res); + + if (options.rawResult) { + res.value = casted; + return callback(null, res); + } + if (options.passRawResult) { + return callback(null, casted, decorateResult(res)); } callback(null, casted); }); @@ -1527,12 +2038,21 @@ function prepareDiscriminatorCriteria(query) { * * Finds a matching document, updates it according to the `update` arg, passing any `options`, and returns the found document (if any) to the callback. The query executes immediately if `callback` is passed. * + * This function triggers the following middleware: + * - `findOneAndUpdate()` + * * ####Available options * * - `new`: bool - if true, return the modified document rather than the original. defaults to false (changed in 4.0) * - `upsert`: bool - creates the object if it doesn't exist. defaults to false. + * - `fields`: {Object|String} - Field selection. Equivalent to `.select(fields).findOneAndUpdate()` * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update + * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 + * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema. + * - `setDefaultsOnInsert`: if this and `upsert` are true, mongoose will apply the [defaults](http://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://docs.mongodb.org/v2.4/reference/operator/update/setOnInsert/). * - `passRawResult`: if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) + * - `context` (string) if set to 'query' and `runValidators` is on, `this` will refer to the query in custom validator functions that update validation runs. Does nothing if `runValidators` is false. + * - `runSettersOnQuery`: bool - if true, run all setters defined on the associated model's schema for all fields defined in the query and the update. * * ####Callback Signature * function(error, doc) { @@ -1556,8 +2076,12 @@ function prepareDiscriminatorCriteria(query) { * @param {Object|Query} [query] * @param {Object} [doc] * @param {Object} [options] - * @param {Function} [callback] + * @param {Boolean} [options.passRawResult] if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) + * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) + * @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. + * @param {Function} [callback] optional params are (error, doc), _unless_ `passRawResult` is used, in which case params are (error, doc, writeOpResult) * @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command + * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult * @return {Query} this * @api public */ @@ -1568,13 +2092,13 @@ Query.prototype.findOneAndUpdate = function(criteria, doc, options, callback) { switch (arguments.length) { case 3: - if ('function' == typeof options) { + if (typeof options === 'function') { callback = options; options = {}; } break; case 2: - if ('function' == typeof doc) { + if (typeof doc === 'function') { callback = doc; doc = criteria; criteria = undefined; @@ -1582,7 +2106,7 @@ Query.prototype.findOneAndUpdate = function(criteria, doc, options, callback) { options = undefined; break; case 1: - if ('function' == typeof criteria) { + if (typeof criteria === 'function') { callback = criteria; criteria = options = doc = undefined; } else { @@ -1600,7 +2124,19 @@ Query.prototype.findOneAndUpdate = function(criteria, doc, options, callback) { this._mergeUpdate(doc); } - options && this.setOptions(options); + if (options) { + options = utils.clone(options, { retainKeyOrder: true }); + if (options.projection) { + this.select(options.projection); + delete options.projection; + } + if (options.fields) { + this.select(options.fields); + delete options.fields; + } + + this.setOptions(options); + } if (!callback) { return this; @@ -1609,7 +2145,7 @@ Query.prototype.findOneAndUpdate = function(criteria, doc, options, callback) { return this._findOneAndUpdate(callback); }; -/** +/*! * Thunk around findOneAndUpdate() * * @param {Function} [callback] @@ -1617,6 +2153,12 @@ Query.prototype.findOneAndUpdate = function(criteria, doc, options, callback) { */ Query.prototype._findOneAndUpdate = function(callback) { + this._castConditions(); + + if (this.error() != null) { + return callback(this.error()); + } + this._findAndModify('update', callback); return this; }; @@ -1626,9 +2168,13 @@ Query.prototype._findOneAndUpdate = function(callback) { * * Finds a matching document, removes it, passing the found document (if any) to the callback. Executes immediately if `callback` is passed. * + * This function triggers the following middleware: + * - `findOneAndRemove()` + * * ####Available options * * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update + * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 * - `passRawResult`: if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) * * ####Callback Signature @@ -1651,7 +2197,9 @@ Query.prototype._findOneAndUpdate = function(callback) { * @memberOf Query * @param {Object} [conditions] * @param {Object} [options] - * @param {Function} [callback] + * @param {Boolean} [options.passRawResult] if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) + * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) + * @param {Function} [callback] optional params are (error, document) * @return {Query} this * @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command * @api public @@ -1663,13 +2211,13 @@ Query.prototype.findOneAndRemove = function(conditions, options, callback) { switch (arguments.length) { case 2: - if ('function' == typeof options) { + if (typeof options === 'function') { callback = options; options = {}; } break; case 1: - if ('function' == typeof conditions) { + if (typeof conditions === 'function') { callback = conditions; conditions = undefined; options = undefined; @@ -1692,7 +2240,7 @@ Query.prototype.findOneAndRemove = function(conditions, options, callback) { return this; }; -/** +/*! * Thunk around findOneAndRemove() * * @param {Function} [callback] @@ -1700,10 +2248,27 @@ Query.prototype.findOneAndRemove = function(conditions, options, callback) { * @api private */ Query.prototype._findOneAndRemove = function(callback) { + this._castConditions(); + + if (this.error() != null) { + return callback(this.error()); + } + Query.base.findOneAndRemove.call(this, callback); }; -/** +/*! + * ignore + */ + +function decorateResult(res) { + if (res) { + res._kareemIgnore = true; + } + return res; +} + +/*! * Override mquery.prototype._findAndModify to provide casting etc. * * @param {String} type - either "remove" or "update" @@ -1712,15 +2277,15 @@ Query.prototype._findOneAndRemove = function(callback) { */ Query.prototype._findAndModify = function(type, callback) { - if ('function' != typeof callback) { - throw new Error("Expected callback in _findAndModify"); + if (typeof callback !== 'function') { + throw new Error('Expected callback in _findAndModify'); } var model = this.model; var schema = model.schema; - var self = this; + var _this = this; var castedQuery; - var castedDoc; + var castedDoc = this._update; var fields; var opts; var doValidate; @@ -1736,7 +2301,12 @@ Query.prototype._findAndModify = function(type, callback) { this._mongooseOptions.strict = opts.strict; } - if ('remove' == type) { + var isOverwriting = this.options.overwrite && !hasDollarKeys(castedDoc); + if (isOverwriting) { + castedDoc = new this.model(castedDoc, null, true); + } + + if (type === 'remove') { opts.remove = true; } else { if (!('new' in opts)) { @@ -1749,33 +2319,40 @@ Query.prototype._findAndModify = function(type, callback) { opts.remove = false; } - castedDoc = castDoc(this, opts.overwrite); - if (!castedDoc) { - if (opts.upsert) { - // still need to do the upsert to empty doc - var doc = utils.clone(castedQuery); - delete doc._id; - castedDoc = { $set: doc }; - } else { - return this.findOne(callback); - } - } else if (castedDoc instanceof Error) { - return callback(castedDoc); + if (isOverwriting) { + doValidate = function(callback) { + castedDoc.validate(callback); + }; } else { - // In order to make MongoDB 2.6 happy (see - // https://jira.mongodb.org/browse/SERVER-12266 and related issues) - // if we have an actual update document but $set is empty, junk the $set. - if (castedDoc.$set && Object.keys(castedDoc.$set).length === 0) { - delete castedDoc.$set; + castedDoc = castDoc(this, opts.overwrite); + castedDoc = setDefaultsOnInsert(this._conditions, schema, castedDoc, opts); + if (!castedDoc) { + if (opts.upsert) { + // still need to do the upsert to empty doc + var doc = utils.clone(castedQuery); + delete doc._id; + castedDoc = {$set: doc}; + } else { + return this.findOne(callback); + } + } else if (castedDoc instanceof Error) { + return callback(castedDoc); + } else { + // In order to make MongoDB 2.6 happy (see + // https://jira.mongodb.org/browse/SERVER-12266 and related issues) + // if we have an actual update document but $set is empty, junk the $set. + if (castedDoc.$set && Object.keys(castedDoc.$set).length === 0) { + delete castedDoc.$set; + } } - } - doValidate = updateValidators(this, schema, castedDoc, opts); + doValidate = updateValidators(this, schema, castedDoc, opts); + } } this._applyPaths(); + var userProvidedFields = this._userProvidedFields || {}; - self = this; var options = this._mongooseOptions; if (this._fields) { @@ -1794,41 +2371,59 @@ Query.prototype._findAndModify = function(type, callback) { } if (!doc || (utils.isObject(doc) && Object.keys(doc).length === 0)) { + if (opts.rawResult) { + return callback(null, res); + } + // opts.passRawResult will be deprecated soon + if (opts.passRawResult) { + return callback(null, null, decorateResult(res)); + } return callback(null, null); } - if (!opts.passRawResult) { - res = null; - } - if (!options.populate) { - return true === options.lean - ? callback(null, doc) - : completeOne(self.model, doc, res, fields, self, null, callback); + if (!!options.lean === true) { + return _completeOneLean(doc, res, opts, callback); + } + return completeOne(_this.model, doc, res, opts, fields, userProvidedFields, null, callback); } - var pop = helpers.preparePopulationOptionsMQ(self, options); - self.model.populate(doc, pop, function(err, doc) { + var pop = helpers.preparePopulationOptionsMQ(_this, options); + pop.__noPromise = true; + _this.model.populate(doc, pop, function(err, doc) { if (err) { return callback(err); } - return true === options.lean - ? callback(null, doc) - : completeOne(self.model, doc, res, fields, self, pop, callback); + if (!!options.lean === true) { + return _completeOneLean(doc, res, opts, callback); + } + return completeOne(_this.model, doc, res, opts, fields, userProvidedFields, pop, callback); }); }; - if ((opts.runValidators || opts.setDefaultsOnInsert) && doValidate) { - doValidate(function(error) { + if (opts.runValidators && doValidate) { + var _callback = function(error) { if (error) { return callback(error); } - self._collection.findAndModify(castedQuery, castedDoc, opts, utils.tick(function(error, res) { + if (castedDoc && castedDoc.toBSON) { + castedDoc = castedDoc.toBSON(); + } + _this._collection.findAndModify(castedQuery, castedDoc, opts, utils.tick(function(error, res) { return cb(error, res ? res.value : res, res); })); - }); + }; + + try { + doValidate(_callback); + } catch (error) { + callback(error); + } } else { + if (castedDoc && castedDoc.toBSON) { + castedDoc = castedDoc.toBSON(); + } this._collection.findAndModify(castedQuery, castedDoc, opts, utils.tick(function(error, res) { return cb(error, res ? res.value : res, res); })); @@ -1837,7 +2432,21 @@ Query.prototype._findAndModify = function(type, callback) { return this; }; -/** +/*! + * ignore + */ + +function _completeOneLean(doc, res, opts, callback) { + if (opts.rawResult) { + return callback(null, res); + } + if (opts.passRawResult) { + return callback(null, doc, decorateResult(res)); + } + return callback(null, doc); +} + +/*! * Override mquery.prototype._mergeUpdate to handle mongoose objects in * updates. * @@ -1862,61 +2471,235 @@ Query.prototype._mergeUpdate = function(doc) { */ function convertSortToArray(opts) { - if (Array.isArray(opts.sort)) return; - if (!utils.isObject(opts.sort)) return; + if (Array.isArray(opts.sort)) { + return; + } + if (!utils.isObject(opts.sort)) { + return; + } var sort = []; - for (var key in opts.sort) if (utils.object.hasOwnProperty(opts.sort, key)) { - sort.push([ key, opts.sort[key] ]); + for (var key in opts.sort) { + if (utils.object.hasOwnProperty(opts.sort, key)) { + sort.push([key, opts.sort[key]]); + } } opts.sort = sort; } -/** - * Internal helper for update +/*! + * Internal thunk for .update() * - * @param {Object} castedQuery - * @param {Object} castedDoc the update command - * @param {Object} options * @param {Function} callback - * @return {Query} this * @see Model.update #model_Model.update * @api private */ -Query.prototype._execUpdate = function(castedQuery, castedDoc, options, callback) { +Query.prototype._execUpdate = function(callback) { var schema = this.model.schema; var doValidate; var _this; - if (this._castError) { - callback(this._castError); + this._castConditions(); + + var castedQuery = this._conditions; + var castedDoc = this._update; + var options = this.options; + + if (this.error() != null) { + callback(this.error()); return this; } - if (this.options.runValidators || this.options.setDefaultsOnInsert) { + var isOverwriting = this.options.overwrite && !hasDollarKeys(castedDoc); + if (isOverwriting) { + castedDoc = new this.model(castedDoc, null, true); + } + + if (this.options.runValidators) { _this = this; - doValidate = updateValidators(this, schema, castedDoc, options); - doValidate(function(err) { + if (isOverwriting) { + doValidate = function(callback) { + castedDoc.validate(callback); + }; + } else { + doValidate = updateValidators(this, schema, castedDoc, options); + } + var _callback = function(err) { if (err) { return callback(err); } + if (castedDoc.toBSON) { + castedDoc = castedDoc.toBSON(); + } Query.base.update.call(_this, castedQuery, castedDoc, options, callback); - }); + }; + try { + doValidate(_callback); + } catch (err) { + process.nextTick(function() { + callback(err); + }); + } return this; } + if (castedDoc.toBSON) { + castedDoc = castedDoc.toBSON(); + } Query.base.update.call(this, castedQuery, castedDoc, options, callback); return this; }; +/*! + * Internal thunk for .updateMany() + * + * @param {Function} callback + * @see Model.update #model_Model.update + * @api private + */ +Query.prototype._updateMany = function(callback) { + var schema = this.model.schema; + var doValidate; + var _this; + + this._castConditions(); + + var castedQuery = this._conditions; + var castedDoc = this._update; + var options = this.options; + + if (this.error() != null) { + callback(this.error()); + return this; + } + + if (this.options.runValidators) { + _this = this; + doValidate = updateValidators(this, schema, castedDoc, options); + var _callback = function(err) { + if (err) { + return callback(err); + } + + Query.base.updateMany.call(_this, castedQuery, castedDoc, options, callback); + }; + try { + doValidate(_callback); + } catch (err) { + process.nextTick(function() { + callback(err); + }); + } + return this; + } + + Query.base.updateMany.call(this, castedQuery, castedDoc, options, callback); + return this; +}; + +/*! + * Internal thunk for .updateOne() + * + * @param {Function} callback + * @see Model.update #model_Model.update + * @api private + */ +Query.prototype._updateOne = function(callback) { + var schema = this.model.schema; + var doValidate; + var _this; + + this._castConditions(); + + var castedQuery = this._conditions; + var castedDoc = this._update; + var options = this.options; + + if (this.error() != null) { + callback(this.error()); + return this; + } + + if (this.options.runValidators) { + _this = this; + doValidate = updateValidators(this, schema, castedDoc, options); + var _callback = function(err) { + if (err) { + return callback(err); + } + + Query.base.updateOne.call(_this, castedQuery, castedDoc, options, callback); + }; + try { + doValidate(_callback); + } catch (err) { + process.nextTick(function() { + callback(err); + }); + } + return this; + } + + Query.base.updateOne.call(this, castedQuery, castedDoc, options, callback); + return this; +}; + +/*! + * Internal thunk for .replaceOne() + * + * @param {Function} callback + * @see Model.replaceOne #model_Model.replaceOne + * @api private + */ +Query.prototype._replaceOne = function(callback) { + var schema = this.model.schema; + var doValidate; + var _this; + + var castedQuery = this._conditions; + var castedDoc = this._update; + var options = this.options; + + if (this.error() != null) { + callback(this.error()); + return this; + } + + if (this.options.runValidators) { + _this = this; + doValidate = updateValidators(this, schema, castedDoc, options); + var _callback = function(err) { + if (err) { + return callback(err); + } + + Query.base.replaceOne.call(_this, castedQuery, castedDoc, options, callback); + }; + try { + doValidate(_callback); + } catch (err) { + process.nextTick(function() { + callback(err); + }); + } + return this; + } + + Query.base.replaceOne.call(this, castedQuery, castedDoc, options, callback); + return this; +}; + /** * Declare and/or execute this query as an update() operation. * * _All paths passed that are not $atomic operations will become $set ops._ * + * This function triggers the following middleware: + * - `update()` + * * ####Example * * Model.where({ _id: id }).update({ title: 'words' }) @@ -1925,18 +2708,29 @@ Query.prototype._execUpdate = function(castedQuery, castedDoc, options, callback * * Model.where({ _id: id }).update({ $set: { title: 'words' }}) * + * ####Valid options: + * + * - `safe` (boolean) safe mode (defaults to value set in schema (true)) + * - `upsert` (boolean) whether to create the doc if it doesn't match (false) + * - `multi` (boolean) whether multiple documents should be updated (false) + * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema. + * - `setDefaultsOnInsert`: if this and `upsert` are true, mongoose will apply the [defaults](http://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://docs.mongodb.org/v2.4/reference/operator/update/setOnInsert/). + * - `strict` (boolean) overrides the `strict` option for this update + * - `overwrite` (boolean) disables update-only mode, allowing you to overwrite the doc (false) + * - `context` (string) if set to 'query' and `runValidators` is on, `this` will refer to the query in custom validator functions that update validation runs. Does nothing if `runValidators` is false. + * * ####Note * * Passing an empty object `{}` as the doc will result in a no-op unless the `overwrite` option is passed. Without the `overwrite` option set, the update operation will be ignored and the callback executed without sending the command to MongoDB so as to prevent accidently overwritting documents in the collection. * * ####Note * - * The operation is only executed when a callback is passed. To force execution without a callback (which would be an unsafe write), we must first call update() and then execute it by using the `exec()` method. + * The operation is only executed when a callback is passed. To force execution without a callback, we must first call update() and then execute it by using the `exec()` method. * * var q = Model.where({ _id: id }); * q.update({ $set: { name: 'bob' }}).update(); // not executed * - * q.update({ $set: { name: 'bob' }}).exec(); // executed as unsafe + * q.update({ $set: { name: 'bob' }}).exec(); // executed * * // keys that are not $atomic ops become $set. * // this executes the same command as the previous example. @@ -1956,69 +2750,245 @@ Query.prototype._execUpdate = function(castedQuery, castedDoc, options, callback * Model.where() * .update({ name: /^match/ }, { $set: { arr: [] }}, { multi: true }, callback) * - * // more multi updates - * Model.where() - * .setOptions({ multi: true }) - * .update({ $set: { arr: [] }}, callback) + * // more multi updates + * Model.where() + * .setOptions({ multi: true }) + * .update({ $set: { arr: [] }}, callback) + * + * // single update by default + * Model.where({ email: 'address@example.com' }) + * .update({ $inc: { counter: 1 }}, callback) + * + * API summary + * + * update(criteria, doc, options, cb) // executes + * update(criteria, doc, options) + * update(criteria, doc, cb) // executes + * update(criteria, doc) + * update(doc, cb) // executes + * update(doc) + * update(cb) // executes + * update(true) // executes + * update() + * + * @param {Object} [criteria] + * @param {Object} [doc] the update command + * @param {Object} [options] + * @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. + * @param {Function} [callback] optional, params are (error, writeOpResult) + * @return {Query} this + * @see Model.update #model_Model.update + * @see update http://docs.mongodb.org/manual/reference/method/db.collection.update/ + * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult + * @api public + */ + +Query.prototype.update = function(conditions, doc, options, callback) { + if (typeof options === 'function') { + // .update(conditions, doc, callback) + callback = options; + options = null; + } else if (typeof doc === 'function') { + // .update(doc, callback); + callback = doc; + doc = conditions; + conditions = {}; + options = null; + } else if (typeof conditions === 'function') { + // .update(callback) + callback = conditions; + conditions = undefined; + doc = undefined; + options = undefined; + } else if (typeof conditions === 'object' && !doc && !options && !callback) { + // .update(doc) + doc = conditions; + conditions = undefined; + options = undefined; + callback = undefined; + } + + return _update(this, 'update', conditions, doc, options, callback); +}; + +/** + * Declare and/or execute this query as an updateMany() operation. Same as + * `update()`, except MongoDB will update _all_ documents that match + * `criteria` (as opposed to just the first one) regardless of the value of + * the `multi` option. + * + * **Note** updateMany will _not_ fire update middleware. Use `pre('updateMany')` + * and `post('updateMany')` instead. + * + * This function triggers the following middleware: + * - `updateMany()` + * + * @param {Object} [criteria] + * @param {Object} [doc] the update command + * @param {Object} [options] + @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. + * @param {Function} [callback] optional params are (error, writeOpResult) + * @return {Query} this + * @see Model.update #model_Model.update + * @see update http://docs.mongodb.org/manual/reference/method/db.collection.update/ + * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult + * @api public + */ + +Query.prototype.updateMany = function(conditions, doc, options, callback) { + if (typeof options === 'function') { + // .update(conditions, doc, callback) + callback = options; + options = null; + } else if (typeof doc === 'function') { + // .update(doc, callback); + callback = doc; + doc = conditions; + conditions = {}; + options = null; + } else if (typeof conditions === 'function') { + // .update(callback) + callback = conditions; + conditions = undefined; + doc = undefined; + options = undefined; + } else if (typeof conditions === 'object' && !doc && !options && !callback) { + // .update(doc) + doc = conditions; + conditions = undefined; + options = undefined; + callback = undefined; + } + + return _update(this, 'updateMany', conditions, doc, options, callback); +}; + +/** + * Declare and/or execute this query as an updateOne() operation. Same as + * `update()`, except MongoDB will update _only_ the first document that + * matches `criteria` regardless of the value of the `multi` option. + * + * **Note** updateOne will _not_ fire update middleware. Use `pre('updateOne')` + * and `post('updateOne')` instead. + * + * This function triggers the following middleware: + * - `updateOne()` * - * // single update by default - * Model.where({ email: 'address@example.com' }) - * .update({ $inc: { counter: 1 }}, callback) + * @param {Object} [criteria] + * @param {Object} [doc] the update command + * @param {Object} [options] + @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. + * @param {Function} [callback] params are (error, writeOpResult) + * @return {Query} this + * @see Model.update #model_Model.update + * @see update http://docs.mongodb.org/manual/reference/method/db.collection.update/ + * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult + * @api public + */ + +Query.prototype.updateOne = function(conditions, doc, options, callback) { + if (typeof options === 'function') { + // .update(conditions, doc, callback) + callback = options; + options = null; + } else if (typeof doc === 'function') { + // .update(doc, callback); + callback = doc; + doc = conditions; + conditions = {}; + options = null; + } else if (typeof conditions === 'function') { + // .update(callback) + callback = conditions; + conditions = undefined; + doc = undefined; + options = undefined; + } else if (typeof conditions === 'object' && !doc && !options && !callback) { + // .update(doc) + doc = conditions; + conditions = undefined; + options = undefined; + callback = undefined; + } + + return _update(this, 'updateOne', conditions, doc, options, callback); +}; + +/** + * Declare and/or execute this query as a replaceOne() operation. Same as + * `update()`, except MongoDB will replace the existing document and will + * not accept any atomic operators (`$set`, etc.) * - * API summary + * **Note** replaceOne will _not_ fire update middleware. Use `pre('replaceOne')` + * and `post('replaceOne')` instead. * - * update(criteria, doc, options, cb) // executes - * update(criteria, doc, options) - * update(criteria, doc, cb) // executes - * update(criteria, doc) - * update(doc, cb) // executes - * update(doc) - * update(cb) // executes - * update(true) // executes (unsafe write) - * update() + * This function triggers the following middleware: + * - `replaceOne()` * * @param {Object} [criteria] * @param {Object} [doc] the update command * @param {Object} [options] - * @param {Function} [callback] + * @param {Function} [callback] optional params are (error, writeOpResult) * @return {Query} this * @see Model.update #model_Model.update * @see update http://docs.mongodb.org/manual/reference/method/db.collection.update/ + * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult * @api public */ -Query.prototype.update = function(conditions, doc, options, callback) { - if ('function' === typeof options) { - // Scenario: update(conditions, doc, callback) +Query.prototype.replaceOne = function(conditions, doc, options, callback) { + if (typeof options === 'function') { + // .update(conditions, doc, callback) callback = options; options = null; - } else if ('function' === typeof doc) { - // Scenario: update(doc, callback); + } else if (typeof doc === 'function') { + // .update(doc, callback); callback = doc; doc = conditions; conditions = {}; options = null; - } else if ('function' === typeof conditions) { + } else if (typeof conditions === 'function') { + // .update(callback) callback = conditions; conditions = undefined; doc = undefined; options = undefined; + } else if (typeof conditions === 'object' && !doc && !options && !callback) { + // .update(doc) + doc = conditions; + conditions = undefined; + options = undefined; + callback = undefined; } + this.setOptions({ overwrite: true }); + return _update(this, 'replaceOne', conditions, doc, options, callback); +}; + +/*! + * Internal helper for update, updateMany, updateOne, replaceOne + */ + +function _update(query, op, conditions, doc, options, callback) { // make sure we don't send in the whole Document to merge() + query.op = op; conditions = utils.toObject(conditions); var oldCb = callback; if (oldCb) { - callback = function(error, result) { - oldCb(error, result ? result.result : { ok: 0, n: 0, nModified: 0 }); - }; + if (typeof oldCb === 'function') { + callback = function(error, result) { + oldCb(error, result ? result.result : {ok: 0, n: 0, nModified: 0}); + }; + } else { + throw new Error('Invalid callback() argument.'); + } } // strict is an option used in the update checking, make sure it gets set if (options) { if ('strict' in options) { - this._mongooseOptions.strict = options.strict; + query._mongooseOptions.strict = options.strict; } } @@ -2027,21 +2997,25 @@ Query.prototype.update = function(conditions, doc, options, callback) { // order to validate // This could also be somebody calling update() or update({}). Probably not a // common use case, check for _update to make sure we don't do anything bad - if (!doc && this._update) { - doc = this._updateForExec(); + if (!doc && query._update) { + doc = query._updateForExec(); } - if (mquery.canMerge(conditions)) { - this.merge(conditions); + if (!(conditions instanceof Query) && + conditions != null && + conditions.toString() !== '[object Object]') { + query.error(new ObjectParameterError(conditions, 'filter', op)); + } else { + query.merge(conditions); } // validate the selector part of the query - var castedQuery = castQuery(this); + var castedQuery = castQuery(query); if (castedQuery instanceof Error) { - this._castError = castedQuery; + query.error(castedQuery); if (callback) { callback(castedQuery); - return this; + return query; } else if (!options || !options.dontThrowCastError) { throw castedQuery; } @@ -2050,42 +3024,48 @@ Query.prototype.update = function(conditions, doc, options, callback) { // validate the update part of the query var castedDoc; try { - var $options = { retainKeyOrder: true }; + var $options = {retainKeyOrder: true}; if (options && options.minimize) { $options.minimize = true; } - castedDoc = this._castUpdate(utils.clone(doc, $options), - options && options.overwrite); + castedDoc = query._castUpdate(utils.clone(doc, $options), + (options && options.overwrite) || op === 'replaceOne'); } catch (err) { - this._castError = castedQuery; + query.error(castedQuery); if (callback) { callback(err); - return this; + return query; } else if (!options || !options.dontThrowCastError) { throw err; } } + castedDoc = setDefaultsOnInsert(query._conditions, query.schema, castedDoc, options); if (!castedDoc) { // Make sure promises know that this is still an update, see gh-2796 - this.op = 'update'; + query.op = op; callback && callback(null); - return this; + return query; } if (utils.isObject(options)) { - this.setOptions(options); + query.setOptions(options); } - if (!this._update) this._update = castedDoc; + if (!query._update) { + query._update = castedDoc; + } // Hooks if (callback) { - return this._execUpdate(castedQuery, castedDoc, options, callback); + if (op === 'update') { + return query._execUpdate(callback); + } + return query['_' + op](callback); } - return Query.base.update.call(this, castedQuery, castedDoc, options, callback); -}; + return Query.base[op].call(query, castedQuery, castedDoc, options, callback); +} /** * Executes the query @@ -2099,7 +3079,7 @@ Query.prototype.update = function(conditions, doc, options, callback) { * query.exec('find', callback); * * @param {String|Function} [operation] - * @param {Function} [callback] + * @param {Function} [callback] optional params depend on the function being called * @return {Promise} * @api public */ @@ -2108,69 +3088,80 @@ Query.prototype.exec = function exec(op, callback) { var Promise = PromiseProvider.get(); var _this = this; - if ('function' == typeof op) { + if (typeof op === 'function') { callback = op; op = null; - } else if ('string' == typeof op) { + } else if (typeof op === 'string') { this.op = op; } - return new Promise.ES6(function(resolve, reject) { + var _results; + var promise = new Promise.ES6(function(resolve, reject) { if (!_this.op) { - callback && callback(null, undefined); resolve(); return; } _this[_this.op].call(_this, function(error, res) { if (error) { - callback && callback(error); reject(error); return; } - callback && callback.apply(null, arguments); + _results = arguments; resolve(res); }); }); + + if (callback) { + promise.then( + function() { + callback.apply(null, _results); + return null; + }, + function(error) { + callback(error, null); + }). + catch(function(error) { + // If we made it here, we must have an error in the callback re: + // gh-4500, so we need to emit. + setImmediate(function() { + _this.model.emit('error', error); + }); + }); + } + + return promise; }; /** - * Finds the schema for `path`. This is different than - * calling `schema.path` as it also resolves paths with - * positional selectors (something.$.another.$.path). + * Executes the query returning a `Promise` which will be + * resolved with either the doc(s) or rejected with the error. * - * @param {String} path - * @api private + * @param {Function} [resolve] + * @param {Function} [reject] + * @return {Promise} + * @api public */ -Query.prototype._getSchema = function _getSchema(path) { - return this.model._getSchema(path); +Query.prototype.then = function(resolve, reject) { + return this.exec().then(resolve, reject); }; -/*! - * These operators require casting docs - * to real Documents for Update operations. +/** + * Executes the query returning a `Promise` which will be + * resolved with either the doc(s) or rejected with the error. + * Like `.then()`, but only takes a rejection handler. + * + * @param {Function} [reject] + * @return {Promise} + * @api public */ -var castOps = { - $push: 1, - $pushAll: 1, - $addToSet: 1, - $set: 1 +Query.prototype.catch = function(reject) { + return this.exec().then(null, reject); }; /*! - * These operators should be cast to numbers instead - * of their path schema type. - */ - -var numberOps = { - $pop: 1, - $unset: 1, - $inc: 1 -}; - -/** * Casts obj for an update command. * * @param {Object} obj @@ -2179,232 +3170,18 @@ var numberOps = { */ Query.prototype._castUpdate = function _castUpdate(obj, overwrite) { - if (!obj) return undefined; - - var ops = Object.keys(obj), - i = ops.length, - ret = {}, - hasKeys, - val; - - while (i--) { - var op = ops[i]; - // if overwrite is set, don't do any of the special $set stuff - if ('$' !== op[0] && !overwrite) { - // fix up $set sugar - if (!ret.$set) { - if (obj.$set) { - ret.$set = obj.$set; - } else { - ret.$set = {}; - } - } - ret.$set[op] = obj[op]; - ops.splice(i, 1); - if (!~ops.indexOf('$set')) ops.push('$set'); - } else if ('$set' === op) { - if (!ret.$set) { - ret[op] = obj[op]; - } - } else { - ret[op] = obj[op]; - } - } - - // cast each value - i = ops.length; - - // if we get passed {} for the update, we still need to respect that when it - // is an overwrite scenario - if (overwrite) { - hasKeys = true; - } - - while (i--) { - op = ops[i]; - val = ret[op]; - if (val && 'Object' === val.constructor.name && !overwrite) { - hasKeys |= this._walkUpdatePath(val, op); - } else if (overwrite && 'Object' === ret.constructor.name) { - // if we are just using overwrite, cast the query and then we will - // *always* return the value, even if it is an empty object. We need to - // set hasKeys above because we need to account for the case where the - // user passes {} and wants to clobber the whole document - // Also, _walkUpdatePath expects an operation, so give it $set since that - // is basically what we're doing - this._walkUpdatePath(ret.$set || ret, '$set'); - } else { - var msg = 'Invalid atomic update value for ' + op + '. ' - + 'Expected an object, received ' + typeof val; - throw new Error(msg); - } - } - - return hasKeys && ret; -}; - -/** - * Walk each path of obj and cast its values - * according to its schema. - * - * @param {Object} obj - part of a query - * @param {String} op - the atomic operator ($pull, $set, etc) - * @param {String} pref - path prefix (internal only) - * @return {Bool} true if this path has keys to update - * @api private - */ - -Query.prototype._walkUpdatePath = function _walkUpdatePath(obj, op, pref) { - var prefix = pref ? pref + '.' : '', - keys = Object.keys(obj), - i = keys.length, - hasKeys = false, - schema, - key, - val; - - var strict = 'strict' in this._mongooseOptions - ? this._mongooseOptions.strict - : this.model.schema.options.strict; - - while (i--) { - key = keys[i]; - val = obj[key]; - - if (val && 'Object' === val.constructor.name) { - // watch for embedded doc schemas - schema = this._getSchema(prefix + key); - if (schema && schema.caster && op in castOps) { - // embedded doc schema - hasKeys = true; - - if ('$each' in val) { - obj[key] = { - $each: this._castUpdateVal(schema, val.$each, op) - }; - - if (val.$slice != null) { - obj[key].$slice = val.$slice | 0; - } - - if (val.$sort) { - obj[key].$sort = val.$sort; - } - - if (!!val.$position || val.$position === 0) { - obj[key].$position = val.$position; - } - } else { - obj[key] = this._castUpdateVal(schema, val, op); - } - } else if (op === '$currentDate') { - // $currentDate can take an object - obj[key] = this._castUpdateVal(schema, val, op); - hasKeys = true; - } else if (op === '$set' && schema) { - obj[key] = this._castUpdateVal(schema, val, op); - hasKeys = true; - } else { - var pathToCheck = (prefix + key).replace(/\.\$$/, ''); - pathToCheck = pathToCheck.replace('.$.', '.0.'); - var positionalPath = (prefix + '0.' + key).replace(/\.\$$/, ''); - positionalPath = positionalPath.replace('.$.', '.0.'); - if (this.model.schema.pathType(pathToCheck) === 'adhocOrUndefined' && - !this.model.schema.hasMixedParent(pathToCheck) && - this.model.schema.pathType(positionalPath) === 'adhocOrUndefined' && - !this.model.schema.hasMixedParent(positionalPath)) { - if (strict === 'throw') { - throw new Error('Field `' + pathToCheck + '` is not in schema.'); - } else if (strict) { - delete obj[key]; - continue; - } - } - - // gh-2314 - // we should be able to set a schema-less field - // to an empty object literal - hasKeys |= this._walkUpdatePath(val, op, prefix + key) || - (utils.isObject(val) && Object.keys(val).length === 0); - } - } else { - schema = ('$each' === key || '$or' === key || '$and' === key) - ? this._getSchema(pref) - : this._getSchema(prefix + key); - - var skip = strict && - !schema && - !/real|nested/.test(this.model.schema.pathType(prefix + key)); - - if (skip) { - if ('throw' == strict) { - throw new Error('Field `' + prefix + key + '` is not in schema.'); - } else { - delete obj[key]; - } - } else { - // gh-1845 temporary fix: ignore $rename. See gh-3027 for tracking - // improving this. - if (op === '$rename') { - hasKeys = true; - return; - } - - hasKeys = true; - obj[key] = this._castUpdateVal(schema, val, op, key); - } - } - } - return hasKeys; -}; - -/** - * Casts `val` according to `schema` and atomic `op`. - * - * @param {Schema} schema - * @param {Object} val - * @param {String} op - the atomic operator ($pull, $set, etc) - * @param {String} [$conditional] - * @api private - */ - -Query.prototype._castUpdateVal = function _castUpdateVal(schema, val, op, $conditional) { - if (!schema) { - // non-existing schema path - return op in numberOps - ? Number(val) - : val; - } - - var cond = schema.caster && op in castOps && - (utils.isObject(val) || Array.isArray(val)); - if (cond) { - // Cast values for ops that add data to MongoDB. - // Ensures embedded documents get ObjectIds etc. - var tmp = schema.cast(val); - if (Array.isArray(val)) { - val = tmp; - } else if (schema.caster.$isSingleNested) { - val = tmp; - } else { - val = tmp[0]; - } - } - - if (op in numberOps) { - return Number(val); - } - if (op === '$currentDate') { - if (typeof val === 'object') { - return { $type: val.$type }; - } - return Boolean(val); - } - if (/^\$/.test($conditional)) { - return schema.castForQuery($conditional, val); + var strict; + if ('strict' in this._mongooseOptions) { + strict = this._mongooseOptions.strict; + } else if (this.schema && this.schema.options) { + strict = this.schema.options.strict; + } else { + strict = true; } - - return schema.castForQuery(val); + return castUpdate(this.schema, obj, { + overwrite: overwrite, + strict: strict + }, this); }; /*! @@ -2460,7 +3237,7 @@ function castDoc(query, overwrite) { * * @param {Object|String} path either the path to populate or an object specifying all parameters * @param {Object|String} [select] Field selection for the population query - * @param {Model} [model] The name of the model you wish to use for population. If not specified, the name is looked up from the Schema ref. + * @param {Model} [model] The model you wish to use for population. If not specified, populate will look up the model by the name in the Schema's `ref` field. * @param {Object} [match] Conditions for the population query * @param {Object} [options] Options for the population query (sort, etc) * @see population ./populate.html @@ -2471,15 +3248,38 @@ function castDoc(query, overwrite) { */ Query.prototype.populate = function() { + if (arguments.length === 0) { + return this; + } + + var i; + var res = utils.populate.apply(null, arguments); + + // Propagate readPreference from parent query, unless one already specified + if (this.options && this.options.readPreference != null) { + for (i = 0; i < res.length; ++i) { + if (!res[i].options || res[i].options.readPreference == null) { + res[i].options = res[i].options || {}; + res[i].options.readPreference = this.options.readPreference; + } + } + } + var opts = this._mongooseOptions; if (!utils.isObject(opts.populate)) { opts.populate = {}; } - for (var i = 0; i < res.length; ++i) { - opts.populate[res[i].path] = res[i]; + var pop = opts.populate; + + for (i = 0; i < res.length; ++i) { + var path = res[i].path; + if (pop[path] && pop[path].populate && res[i].populate) { + res[i].populate = pop[path].populate.concat(res[i].populate); + } + pop[res[i].path] = res[i]; } return this; @@ -2501,7 +3301,22 @@ Query.prototype.populate = function() { Query.prototype.cast = function(model, obj) { obj || (obj = this._conditions); - return cast(model.schema, obj); + try { + return cast(model.schema, obj, { + upsert: this.options && this.options.upsert, + strict: (this.options && 'strict' in this.options) ? + this.options.strict : + (model.schema.options && model.schema.options.strict), + strictQuery: (this.options && this.options.strictQuery) || + (model.schema.options && model.schema.options.strictQuery) + }, this); + } catch (err) { + // CastError, assign model + if (typeof err.setModel === 'function') { + err.setModel(model); + } + throw err; + } }; /** @@ -2564,96 +3379,9 @@ Query.prototype._castFields = function _castFields(fields) { */ Query.prototype._applyPaths = function applyPaths() { - // determine if query is selecting or excluding fields - - var fields = this._fields, - exclude, - keys, - ki; - - if (fields) { - keys = Object.keys(fields); - ki = keys.length; - - while (ki--) { - if ('+' == keys[ki][0]) continue; - exclude = 0 === fields[keys[ki]]; - break; - } - } - - // if selecting, apply default schematype select:true fields - // if excluding, apply schematype select:false fields - - var selected = [], - excluded = [], - seen = []; - - var analyzePath = function(path, type) { - if ('boolean' != typeof type.selected) return; - - var plusPath = '+' + path; - if (fields && plusPath in fields) { - // forced inclusion - delete fields[plusPath]; - - // if there are other fields being included, add this one - // if no other included fields, leave this out (implied inclusion) - if (false === exclude && keys.length > 1 && !~keys.indexOf(path)) { - fields[path] = 1; - } - - return; - } - - // check for parent exclusions - var root = path.split('.')[0]; - if (~excluded.indexOf(root)) return; - - (type.selected ? selected : excluded).push(path); - }; - - var analyzeSchema = function(schema, prefix) { - prefix || (prefix = ''); - - // avoid recursion - if (~seen.indexOf(schema)) return; - seen.push(schema); - - schema.eachPath(function(path, type) { - if (prefix) path = prefix + '.' + path; - - analyzePath(path, type); - - // array of subdocs? - if (type.schema) { - analyzeSchema(type.schema, path); - } - - }); - }; - - analyzeSchema(this.model.schema); - - switch (exclude) { - case true: - excluded.length && this.select('-' + excluded.join(' -')); - break; - case false: - if (this.model.schema && this.model.schema.paths['_id'] && - this.model.schema.paths['_id'].options && this.model.schema.paths['_id'].options.select === false) { - selected.push('-_id'); - } - selected.length && this.select(selected.join(' ')); - break; - case undefined: - // user didn't specify fields, implies returning all fields. - // only need to apply excluded fields - excluded.length && this.select('-' + excluded.join(' -')); - break; - } - - return seen = excluded = selected = keys = fields = null; + this._fields = this._fields || {}; + helpers.applyPaths(this._fields, this.model.schema); + selectPopulatedFields(this); }; /** @@ -2694,8 +3422,72 @@ Query.prototype._applyPaths = function applyPaths() { Query.prototype.stream = function stream(opts) { this._applyPaths(); this._fields = this._castFields(this._fields); + this._castConditions(); return new QueryStream(this, opts); }; +Query.prototype.stream = util.deprecate(Query.prototype.stream, 'Mongoose: ' + + 'Query.prototype.stream() is deprecated in mongoose >= 4.5.0, ' + + 'use Query.prototype.cursor() instead'); + +/** + * Returns a wrapper around a [mongodb driver cursor](http://mongodb.github.io/node-mongodb-native/2.1/api/Cursor.html). + * A QueryCursor exposes a [Streams3](https://strongloop.com/strongblog/whats-new-io-js-beta-streams3/)-compatible + * interface, as well as a `.next()` function. + * + * The `.cursor()` function triggers pre find hooks, but **not** post find hooks. + * + * ####Example + * + * // There are 2 ways to use a cursor. First, as a stream: + * Thing. + * find({ name: /^hello/ }). + * cursor(). + * on('data', function(doc) { console.log(doc); }). + * on('end', function() { console.log('Done!'); }); + * + * // Or you can use `.next()` to manually get the next doc in the stream. + * // `.next()` returns a promise, so you can use promises or callbacks. + * var cursor = Thing.find({ name: /^hello/ }).cursor(); + * cursor.next(function(error, doc) { + * console.log(doc); + * }); + * + * // Because `.next()` returns a promise, you can use co + * // to easily iterate through all documents without loading them + * // all into memory. + * co(function*() { + * const cursor = Thing.find({ name: /^hello/ }).cursor(); + * for (let doc = yield cursor.next(); doc != null; doc = yield cursor.next()) { + * console.log(doc); + * } + * }); + * + * ####Valid options + * + * - `transform`: optional function which accepts a mongoose document. The return value of the function will be emitted on `data` and returned by `.next()`. + * + * @return {QueryCursor} + * @param {Object} [options] + * @see QueryCursor + * @api public + */ + +Query.prototype.cursor = function cursor(opts) { + this._applyPaths(); + this._fields = this._castFields(this._fields); + this.setOptions({ fields: this._fieldsForExec() }); + if (opts) { + this.setOptions(opts); + } + + try { + this.cast(this.model); + } catch (err) { + return (new QueryCursor(this, this.options))._markError(err); + } + + return new QueryCursor(this, this.options); +}; // the rest of these are basically to support older Mongoose syntax with mquery @@ -2724,6 +3516,9 @@ Query.prototype.maxscan = Query.base.maxScan; * Cannot be used with `distinct()` * * @param {Boolean} bool defaults to true + * @param {Object} [opts] options to set + * @param {Number} [opts.numberOfRetries] if cursor is exhausted, retry this many times before giving up + * @param {Number} [opts.tailableRetryInterval] if cursor is exhausted, wait this many milliseconds before retrying * @see tailable http://docs.mongodb.org/manual/tutorial/create-tailable-cursor/ * @api public */ @@ -2731,7 +3526,7 @@ Query.prototype.maxscan = Query.base.maxScan; Query.prototype.tailable = function(val, opts) { // we need to support the tailable({ awaitdata : true }) as well as the // tailable(true, {awaitdata :true}) syntax that mquery does not support - if (val && val.constructor.name == 'Object') { + if (val && val.constructor.name === 'Object') { opts = val; val = true; } @@ -2867,12 +3662,12 @@ Query.prototype.near = function() { if (arguments.length === 1) { if (Array.isArray(arguments[0])) { - params.push({ center: arguments[0], spherical: sphere }); - } else if ('string' == typeof arguments[0]) { + params.push({center: arguments[0], spherical: sphere}); + } else if (typeof arguments[0] === 'string') { // just passing a path params.push(arguments[0]); } else if (utils.isObject(arguments[0])) { - if ('boolean' != typeof arguments[0].spherical) { + if (typeof arguments[0].spherical !== 'boolean') { arguments[0].spherical = sphere; } params.push(arguments[0]); @@ -2880,14 +3675,14 @@ Query.prototype.near = function() { throw new TypeError('invalid argument'); } } else if (arguments.length === 2) { - if ('number' == typeof arguments[0] && 'number' == typeof arguments[1]) { - params.push({ center: [arguments[0], arguments[1]], spherical: sphere}); - } else if ('string' == typeof arguments[0] && Array.isArray(arguments[1])) { + if (typeof arguments[0] === 'number' && typeof arguments[1] === 'number') { + params.push({center: [arguments[0], arguments[1]], spherical: sphere}); + } else if (typeof arguments[0] === 'string' && Array.isArray(arguments[1])) { params.push(arguments[0]); - params.push({ center: arguments[1], spherical: sphere }); - } else if ('string' == typeof arguments[0] && utils.isObject(arguments[1])) { + params.push({center: arguments[1], spherical: sphere}); + } else if (typeof arguments[0] === 'string' && utils.isObject(arguments[1])) { params.push(arguments[0]); - if ('boolean' != typeof arguments[1].spherical) { + if (typeof arguments[1].spherical !== 'boolean') { arguments[1].spherical = sphere; } params.push(arguments[1]); @@ -2895,10 +3690,10 @@ Query.prototype.near = function() { throw new TypeError('invalid argument'); } } else if (arguments.length === 3) { - if ('string' == typeof arguments[0] && 'number' == typeof arguments[1] - && 'number' == typeof arguments[2]) { + if (typeof arguments[0] === 'string' && typeof arguments[1] === 'number' + && typeof arguments[2] === 'number') { params.push(arguments[0]); - params.push({ center: [arguments[1], arguments[2]], spherical: sphere }); + params.push({center: [arguments[1], arguments[2]], spherical: sphere}); } else { throw new TypeError('invalid argument'); } @@ -3014,7 +3809,7 @@ Query.prototype.box = function(ll, ur) { * @return {Query} this * @see $center http://docs.mongodb.org/manual/reference/operator/center/ * @see $centerSphere http://docs.mongodb.org/manual/reference/operator/centerSphere/ - * @see $geoWithin http://docs.mongodb.org/manual/reference/operator/within/ + * @see $geoWithin http://docs.mongodb.org/manual/reference/operator/geoWithin/ * @see http://www.mongodb.org/display/DOCS/Geospatial+Indexing * @api public */ @@ -3052,11 +3847,11 @@ Query.prototype.center = Query.base.circle; */ Query.prototype.centerSphere = function() { - if (arguments[0] && arguments[0].constructor.name == 'Object') { + if (arguments[0] && arguments[0].constructor.name === 'Object') { arguments[0].spherical = true; } - if (arguments[1] && arguments[1].constructor.name == 'Object') { + if (arguments[1] && arguments[1].constructor.name === 'Object') { arguments[1].spherical = true; } @@ -3072,15 +3867,6 @@ Query.prototype.centerSphere = function() { * @api public */ -/** - * Executes this query and returns a promise - * - * @method then - * @memberOf Query - * @return {Promise} - * @api public - */ - /** * Determines if inclusive field selection has been made. * @@ -3094,6 +3880,10 @@ Query.prototype.centerSphere = function() { * @api public */ +Query.prototype.selectedInclusively = function selectedInclusively() { + return isInclusive(this._fields); +}; + /** * Determines if exclusive field selection has been made. * @@ -3108,6 +3898,29 @@ Query.prototype.centerSphere = function() { * @api public */ +Query.prototype.selectedExclusively = function selectedExclusively() { + if (!this._fields) { + return false; + } + + var keys = Object.keys(this._fields); + if (keys.length === 0) { + return false; + } + + for (var i = 0; i < keys.length; ++i) { + var key = keys[i]; + if (key === '_id') { + continue; + } + if (this._fields[key] === 0 || this._fields[key] === false) { + return true; + } + } + + return false; +}; + /*! * Export */ diff --git a/lib/queryhelpers.js b/lib/queryhelpers.js index 3e0b82d7d65..c9d99277cd4 100644 --- a/lib/queryhelpers.js +++ b/lib/queryhelpers.js @@ -3,6 +3,8 @@ * Module dependencies */ +var get = require('lodash.get'); +var isDefiningProjection = require('./services/projection/isDefiningProjection'); var utils = require('./utils'); /*! @@ -17,7 +19,9 @@ exports.preparePopulationOptions = function preparePopulationOptions(query, opti var pop = utils.object.vals(query.options.populate); // lean options should trickle through all queries - if (options.lean) pop.forEach(makeLean); + if (options.lean) { + pop.forEach(makeLean(options.lean)); + } return pop; }; @@ -35,7 +39,9 @@ exports.preparePopulationOptionsMQ = function preparePopulationOptionsMQ(query, var pop = utils.object.vals(query._mongooseOptions.populate); // lean options should trickle through all queries - if (options.lean) pop.forEach(makeLean); + if (options.lean) { + pop.forEach(makeLean(options.lean)); + } return pop; }; @@ -50,7 +56,7 @@ exports.preparePopulationOptionsMQ = function preparePopulationOptionsMQ(query, * * @return {Model} */ -exports.createModel = function createModel(model, doc, fields) { +exports.createModel = function createModel(model, doc, fields, userProvidedFields) { var discriminatorMapping = model.schema ? model.schema.discriminatorMapping : null; @@ -60,19 +66,154 @@ exports.createModel = function createModel(model, doc, fields) { : null; if (key && doc[key] && model.discriminators && model.discriminators[doc[key]]) { - return new model.discriminators[doc[key]](undefined, fields, true); + var discriminator = model.discriminators[doc[key]]; + var _fields = utils.clone(userProvidedFields); + exports.applyPaths(_fields, discriminator.schema); + return new model.discriminators[doc[key]](undefined, _fields, true); } return new model(undefined, fields, true); }; +/*! + * ignore + */ + +exports.applyPaths = function applyPaths(fields, schema) { + // determine if query is selecting or excluding fields + var exclude; + var keys; + var ki; + var field; + + if (fields) { + keys = Object.keys(fields); + ki = keys.length; + + while (ki--) { + if (keys[ki][0] === '+') { + continue; + } + field = fields[keys[ki]]; + // Skip `$meta` and `$slice` + if (!isDefiningProjection(field)) { + continue; + } + exclude = field === 0; + break; + } + } + + // if selecting, apply default schematype select:true fields + // if excluding, apply schematype select:false fields + + var selected = []; + var excluded = []; + var stack = []; + + var analyzePath = function(path, type) { + if (typeof type.selected !== 'boolean') return; + + var plusPath = '+' + path; + if (fields && plusPath in fields) { + // forced inclusion + delete fields[plusPath]; + + // if there are other fields being included, add this one + // if no other included fields, leave this out (implied inclusion) + if (exclude === false && keys.length > 1 && !~keys.indexOf(path)) { + fields[path] = 1; + } + + return; + } + + // check for parent exclusions + var pieces = path.split('.'); + var root = pieces[0]; + if (~excluded.indexOf(root)) { + return; + } + + // Special case: if user has included a parent path of a discriminator key, + // don't explicitly project in the discriminator key because that will + // project out everything else under the parent path + if (!exclude && get(type, 'options.$skipDiscriminatorCheck', false)) { + var cur = ''; + for (var i = 0; i < pieces.length; ++i) { + cur += (cur.length === 0 ? '' : '.') + pieces[i]; + var projection = get(fields, cur, false); + if (projection && typeof projection !== 'object') { + return; + } + } + } + + (type.selected ? selected : excluded).push(path); + }; + + var analyzeSchema = function(schema, prefix) { + prefix || (prefix = ''); + + // avoid recursion + if (stack.indexOf(schema) !== -1) { + return; + } + stack.push(schema); + + schema.eachPath(function(path, type) { + if (prefix) path = prefix + '.' + path; + + analyzePath(path, type); + + // array of subdocs? + if (type.schema) { + analyzeSchema(type.schema, path); + } + }); + + stack.pop(); + }; + + analyzeSchema(schema); + + var i; + switch (exclude) { + case true: + for (i = 0; i < excluded.length; ++i) { + fields[excluded[i]] = 0; + } + break; + case false: + if (schema && + schema.paths['_id'] && + schema.paths['_id'].options && + schema.paths['_id'].options.select === false) { + fields._id = 0; + } + for (i = 0; i < selected.length; ++i) { + fields[selected[i]] = 1; + } + break; + case undefined: + // user didn't specify fields, implies returning all fields. + // only need to apply excluded fields + for (i = 0; i < excluded.length; ++i) { + fields[excluded[i]] = 0; + } + break; + } +}; + /*! * Set each path query option to lean * * @param {Object} option */ -function makeLean(option) { - option.options || (option.options = {}); - option.options.lean = true; +function makeLean(val) { + return function(option) { + option.options || (option.options = {}); + option.options.lean = val; + }; } diff --git a/lib/querystream.js b/lib/querystream.js index 086a203622e..2918e25595a 100644 --- a/lib/querystream.js +++ b/lib/querystream.js @@ -7,7 +7,9 @@ var Stream = require('stream').Stream; var utils = require('./utils'); var helpers = require('./queryhelpers'); -var K = function(k) { return k; }; +var K = function(k) { + return k; +}; /** * Provides a Node.js 0.8 style [ReadStream](http://nodejs.org/docs/v0.8.21/api/stream.html#stream_readable_stream) interface for Queries. @@ -62,14 +64,14 @@ function QueryStream(query, options) { this._buffer = null; this._inline = T_INIT; this._running = false; - this._transform = options && 'function' == typeof options.transform - ? options.transform - : K; + this._transform = options && typeof options.transform === 'function' + ? options.transform + : K; // give time to hook up events - var self = this; + var _this = this; process.nextTick(function() { - self._init(); + _this._init(); }); } @@ -109,26 +111,30 @@ var T_CONT = 2; */ QueryStream.prototype._init = function() { - if (this._destroyed) return; + if (this._destroyed) { + return; + } var query = this.query, model = query.model, options = query._optionsForExec(model), - self = this; + _this = this; try { query.cast(model); } catch (err) { - return self.destroy(err); + return _this.destroy(err); } - self._fields = utils.clone(query._fields); - options.fields = query._castFields(self._fields); + _this._fields = utils.clone(query._fields); + options.fields = query._castFields(_this._fields); model.collection.find(query._conditions, options, function(err, cursor) { - if (err) return self.destroy(err); - self._cursor = cursor; - self._next(); + if (err) { + return _this.destroy(err); + } + _this._cursor = cursor; + _this._next(); }); }; @@ -141,21 +147,23 @@ QueryStream.prototype._init = function() { QueryStream.prototype._next = function _next() { if (this.paused || this._destroyed) { - return this._running = false; + this._running = false; + return this._running; } this._running = true; if (this._buffer && this._buffer.length) { var arg; - while (!this.paused && !this._destroyed && (arg = this._buffer.shift())) { + while (!this.paused && !this._destroyed && (arg = this._buffer.shift())) { // eslint-disable-line no-cond-assign this._onNextObject.apply(this, arg); } } // avoid stack overflows with large result sets. // trampoline instead of recursion. - while (this.__next()) {} + while (this.__next()) { + } }; /** @@ -166,26 +174,27 @@ QueryStream.prototype._next = function _next() { */ QueryStream.prototype.__next = function() { - if (this.paused || this._destroyed) - return this._running = false; + if (this.paused || this._destroyed) { + this._running = false; + return this._running; + } - var self = this; - self._inline = T_INIT; + var _this = this; + _this._inline = T_INIT; - self._cursor.nextObject(function cursorcb(err, doc) { - self._onNextObject(err, doc); + _this._cursor.nextObject(function cursorcb(err, doc) { + _this._onNextObject(err, doc); }); // if onNextObject() was already called in this tick // return ourselves to the trampoline. if (T_CONT === this._inline) { return true; - } else { - // onNextObject() hasn't fired yet. tell onNextObject - // that its ok to call _next b/c we are not within - // the trampoline anymore. - this._inline = T_IDLE; } + // onNextObject() hasn't fired yet. tell onNextObject + // that its ok to call _next b/c we are not within + // the trampoline anymore. + this._inline = T_IDLE; }; /** @@ -197,15 +206,20 @@ QueryStream.prototype.__next = function() { */ QueryStream.prototype._onNextObject = function _onNextObject(err, doc) { - if (this._destroyed) return; + if (this._destroyed) { + return; + } if (this.paused) { this._buffer || (this._buffer = []); this._buffer.push([err, doc]); - return this._running = false; + this._running = false; + return this._running; } - if (err) return this.destroy(err); + if (err) { + return this.destroy(err); + } // when doc is null we hit the end of the cursor if (!doc) { @@ -216,35 +230,40 @@ QueryStream.prototype._onNextObject = function _onNextObject(err, doc) { var opts = this.query._mongooseOptions; if (!opts.populate) { - return true === opts.lean ? - emit(this, doc) : - createAndEmit(this, null, doc); + return opts.lean === true ? + emit(this, doc) : + createAndEmit(this, null, doc); } - var self = this; - var pop = helpers.preparePopulationOptionsMQ(self.query, self.query._mongooseOptions); + var _this = this; + var pop = helpers.preparePopulationOptionsMQ(_this.query, _this.query._mongooseOptions); // Hack to work around gh-3108 pop.forEach(function(option) { delete option.model; }); - self.query.model.populate(doc, pop, function(err, doc) { - if (err) return self.destroy(err); - return true === opts.lean ? - emit(self, doc) : - createAndEmit(self, pop, doc); + pop.__noPromise = true; + _this.query.model.populate(doc, pop, function(err, doc) { + if (err) { + return _this.destroy(err); + } + return opts.lean === true ? + emit(_this, doc) : + createAndEmit(_this, pop, doc); }); }; function createAndEmit(self, populatedIds, doc) { var instance = helpers.createModel(self.query.model, doc, self._fields); var opts = populatedIds ? - { populated: populatedIds } : + {populated: populatedIds} : undefined; instance.init(doc, opts, function(err) { - if (err) return self.destroy(err); + if (err) { + return self.destroy(err); + } emit(self, instance); }); } @@ -303,14 +322,16 @@ QueryStream.prototype.resume = function() { }; /** - * Destroys the stream, closing the underlying cursor. No more events will be emitted. + * Destroys the stream, closing the underlying cursor, which emits the close event. No more events will be emitted after the close event. * * @param {Error} [err] * @api public */ QueryStream.prototype.destroy = function(err) { - if (this._destroyed) return; + if (this._destroyed) { + return; + } this._destroyed = true; this._running = false; this.readable = false; diff --git a/lib/schema.js b/lib/schema.js index fddc66ff8a4..ca4dc3c2d6a 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -8,16 +8,22 @@ var VirtualType = require('./virtualtype'); var utils = require('./utils'); var MongooseTypes; var Kareem = require('kareem'); -var async = require('async'); -var PromiseProvider = require('./promise_provider'); +var each = require('async/each'); +var SchemaType = require('./schematype'); +var mpath = require('mpath'); -var IS_QUERY_HOOK = { +var IS_KAREEM_HOOK = { + aggregate: true, count: true, find: true, findOne: true, findOneAndUpdate: true, findOneAndRemove: true, - update: true + insertMany: true, + replaceOne: true, + update: true, + updateMany: true, + updateOne: true }; /** @@ -49,26 +55,33 @@ var IS_QUERY_HOOK = { * - [toJSON](/docs/guide.html#toJSON) - object - no default * - [toObject](/docs/guide.html#toObject) - object - no default * - [typeKey](/docs/guide.html#typeKey) - string - defaults to 'type' + * - [useNestedStrict](/docs/guide.html#useNestedStrict) - boolean - defaults to false * - [validateBeforeSave](/docs/guide.html#validateBeforeSave) - bool - defaults to `true` - * - [versionKey](/docs/guide.html#versionKey): bool - defaults to "__v" + * - [versionKey](/docs/guide.html#versionKey): string - defaults to "__v" + * - [collation](/docs/guide.html#collation): object - defaults to null (which means use no collation) * * ####Note: * * _When nesting schemas, (`children` in the example above), always declare the child schema first before passing it into its parent._ * * @param {Object} definition + * @param {Object} [options] * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter * @event `init`: Emitted after the schema is compiled into a `Model`. * @api public */ function Schema(obj, options) { - if (!(this instanceof Schema)) + if (!(this instanceof Schema)) { return new Schema(obj, options); + } + this.obj = obj; this.paths = {}; + this.aliases = {}; this.subpaths = {}; this.virtuals = {}; + this.singleNestedPaths = {}; this.nested = {}; this.inherits = {}; this.callQueue = []; @@ -76,13 +89,13 @@ function Schema(obj, options) { this.methods = {}; this.statics = {}; this.tree = {}; - this._requiredpaths = undefined; - this.discriminatorMapping = undefined; - this._indexedpaths = undefined; + this.query = {}; + this.childSchemas = []; + this.plugins = []; this.s = { hooks: new Kareem(), - queryHooks: IS_QUERY_HOOK + kareemHooks: IS_KAREEM_HOOK }; this.options = this.defaultOptions(options); @@ -97,19 +110,12 @@ function Schema(obj, options) { // ensure the documents get an auto _id unless disabled var auto_id = !this.paths['_id'] && - (!this.options.noId && this.options._id) && !_idSubDoc; + (!this.options.noId && this.options._id) && !_idSubDoc; if (auto_id) { - obj = { _id: { auto: true } }; - obj._id[this.options.typeKey] = Schema.ObjectId; - this.add(obj); - } - - // ensure the documents receive an id getter unless disabled - var autoid = !this.paths['id'] && - (!this.options.noVirtualId && this.options.id); - if (autoid) { - this.virtual('id').get(idGetter); + var _obj = {_id: {auto: true}}; + _obj._id[this.options.typeKey] = Schema.ObjectId; + this.add(_obj); } for (var i = 0; i < this._defaultMiddleware.length; ++i) { @@ -117,74 +123,60 @@ function Schema(obj, options) { this[m.kind](m.hook, !!m.isAsync, m.fn); } - // adds updatedAt and createdAt timestamps to documents if enabled - var timestamps = this.options.timestamps; - if (timestamps) { - var createdAt = timestamps.createdAt || 'createdAt', - updatedAt = timestamps.updatedAt || 'updatedAt', - schemaAdditions = {}; - - schemaAdditions[updatedAt] = Date; - - if (!this.paths[createdAt]) { - schemaAdditions[createdAt] = Date; - } - - this.add(schemaAdditions); - - this.pre('save', function(next) { - var defaultTimestamp = new Date(); - - if (!this[createdAt]) { - this[createdAt] = auto_id ? this._id.getTimestamp() : defaultTimestamp; - } - - this[updatedAt] = this.isNew ? this[createdAt] : defaultTimestamp; - - next(); - }); - - var genUpdates = function() { - var now = new Date(); - var updates = {$set: {}, $setOnInsert: {}}; - updates.$set[updatedAt] = now; - updates.$setOnInsert[createdAt] = now; - - return updates; - }; - - this.pre('findOneAndUpdate', function(next) { - this.findOneAndUpdate({}, genUpdates()); - next(); - }); - - this.pre('update', function(next) { - this.update({}, genUpdates()); - next(); - }); + if (this.options.timestamps) { + this.setupTimestamp(this.options.timestamps); } + // Assign virtual properties based on alias option + aliasFields(this); } /*! - * Returns this documents _id cast to a string. + * Create virtual properties with alias field */ +function aliasFields(schema) { + for (var path in schema.paths) { + if (!schema.paths[path].options) continue; -function idGetter() { - if (this.$__._id) { - return this.$__._id; - } + var prop = schema.paths[path].path; + var alias = schema.paths[path].options.alias; + + if (alias) { + if ('string' === typeof alias && alias.length > 0) { + if (schema.aliases[alias]) { + throw new Error('Duplicate alias, alias ' + alias + ' is used more than once'); + } else { + schema.aliases[alias] = prop; + } - return this.$__._id = null == this._id - ? null - : String(this._id); + schema + .virtual(alias) + .get((function(p) { + return function() { + if (typeof this.get === 'function') { + return this.get(p); + } + return this[p]; + }; + })(prop)) + .set((function(p) { + return function(v) { + return this.set(p, v); + }; + })(prop)); + } else { + throw new Error('Invalid value for alias option on ' + prop + ', got ' + alias); + } + } + } } /*! * Inherit from EventEmitter. */ -Schema.prototype = Object.create( EventEmitter.prototype ); +Schema.prototype = Object.create(EventEmitter.prototype); Schema.prototype.constructor = Schema; +Schema.prototype.instanceOfSchema = true; /** * Default middleware attached to a schema. Cannot be changed. @@ -200,81 +192,75 @@ Object.defineProperty(Schema.prototype, '_defaultMiddleware', { configurable: false, enumerable: false, writable: false, - value: [{ - kind: 'pre', - hook: 'save', - fn: function(next, options) { - // Nested docs have their own presave - if (this.ownerDocument) { - return next(); - } - - var hasValidateBeforeSaveOption = options && - (typeof options === 'object') && - ('validateBeforeSave' in options); + value: [ + { + kind: 'pre', + hook: 'remove', + isAsync: true, + fn: function(next, done) { + if (this.ownerDocument) { + done(); + next(); + return; + } - var shouldValidate; - if (hasValidateBeforeSaveOption) { - shouldValidate = !!options.validateBeforeSave; - } else { - shouldValidate = this.schema.options.validateBeforeSave; - } + var subdocs = this.$__getAllSubdocs(); - // Validate - if (shouldValidate) { - // HACK: use $__original_validate to avoid promises so bluebird doesn't - // complain - if (this.$__original_validate) { - this.$__original_validate({ __noPromise: true }, function(error) { - next(error); - }); - } else { - this.validate({ __noPromise: true }, function(error) { - next(error); - }); + if (!subdocs.length) { + done(); + next(); + return; } - } else { - next(); - } - } - }, { - kind: 'pre', - hook: 'save', - isAsync: true, - fn: function(next, done) { - var Promise = PromiseProvider.get(), - subdocs = this.$__getAllSubdocs(); - - if (!subdocs.length || this.$__preSavingFromParent) { - done(); - next(); - return; - } - new Promise.ES6(function(resolve, reject) { - async.each(subdocs, function(subdoc, cb) { - subdoc.$__preSavingFromParent = true; - subdoc.save(function(err) { + each(subdocs, function(subdoc, cb) { + subdoc.remove({ noop: true }, function(err) { cb(err); }); }, function(error) { - for (var i = 0; i < subdocs.length; ++i) { - delete subdocs[i].$__preSavingFromParent; - } if (error) { - reject(error); + done(error); return; } - resolve(); + next(); + done(); }); - }).then(function() { - next(); - done(); - }, done); + } } - }] + ] +}); + +/** + * Array of child schemas (from document arrays and single nested subdocs) + * and their corresponding compiled models. Each element of the array is + * an object with 2 properties: `schema` and `model`. + * + * This property is typically only useful for plugin authors and advanced users. + * You do not need to interact with this property at all to use mongoose. + * + * @api public + * @property childSchemas + */ + +Object.defineProperty(Schema.prototype, 'childSchemas', { + configurable: false, + enumerable: true, + writable: true }); +/** + * The original object passed to the schema constructor + * + * ####Example: + * + * var schema = new Schema({ a: String }).add({ b: String }); + * schema.obj; // { a: String } + * + * @api public + * @property obj + */ + +Schema.prototype.obj; + /** * Schema as flat paths * @@ -307,6 +293,27 @@ Schema.prototype.paths; Schema.prototype.tree; +/** + * Returns a deep copy of the schema + * + * @return {Schema} the cloned schema + * @api public + */ + +Schema.prototype.clone = function() { + var s = new Schema(this.paths, this.options); + // Clone the call queue + var cloneOpts = { retainKeyOrder: true }; + s.callQueue = this.callQueue.map(function(f) { return f; }); + s.methods = utils.clone(this.methods, cloneOpts); + s.statics = utils.clone(this.statics, cloneOpts); + s.query = utils.clone(this.query, cloneOpts); + s.plugins = Array.prototype.slice.call(this.plugins); + s._indexes = utils.clone(this._indexes, cloneOpts); + s.s.hooks = this.s.hooks.clone(); + return s; +}; + /** * Returns default options for this schema, merged with `options`. * @@ -316,15 +323,19 @@ Schema.prototype.tree; */ Schema.prototype.defaultOptions = function(options) { - if (options && false === options.safe) { - options.safe = { w: 0 }; + if (options && options.safe === false) { + options.safe = {w: 0}; } - if (options && options.safe && 0 === options.safe.w) { + if (options && options.safe && options.safe.w === 0) { // if you turn off safe writes, then versioning goes off as well options.versionKey = false; } + this._userProvidedOptions = utils.clone(options, { + retainKeyOrder: true + }); + options = utils.options({ strict: true, bufferCommands: true, @@ -341,7 +352,8 @@ Schema.prototype.defaultOptions = function(options) { _id: true, noVirtualId: false, // deprecated, use { id: false } id: true, - typeKey: 'type' + typeKey: 'type', + retainKeyOrder: false }, options); if (options.read) { @@ -371,25 +383,31 @@ Schema.prototype.add = function add(obj, prefix) { for (var i = 0; i < keys.length; ++i) { var key = keys[i]; - if (null == obj[key]) { + if (obj[key] == null) { throw new TypeError('Invalid value for schema path `' + prefix + key + '`'); } - if (Array.isArray(obj[key]) && obj[key].length === 1 && null == obj[key][0]) { + if (Array.isArray(obj[key]) && obj[key].length === 1 && obj[key][0] == null) { throw new TypeError('Invalid value for schema Array path `' + prefix + key + '`'); } if (utils.isObject(obj[key]) && - (!obj[key].constructor || 'Object' == utils.getFunctionName(obj[key].constructor)) && + (!obj[key].constructor || utils.getFunctionName(obj[key].constructor) === 'Object') && (!obj[key][this.options.typeKey] || (this.options.typeKey === 'type' && obj[key].type.type))) { if (Object.keys(obj[key]).length) { // nested object { last: { name: String }} this.nested[prefix + key] = true; this.add(obj[key], prefix + key + '.'); } else { + if (prefix) { + this.nested[prefix.substr(0, prefix.length - 1)] = true; + } this.path(prefix + key, obj[key]); // mixed type } } else { + if (prefix) { + this.nested[prefix.substr(0, prefix.length - 1)] = true; + } this.path(prefix + key, obj[key]); } } @@ -410,10 +428,14 @@ Schema.prototype.add = function add(obj, prefix) { Schema.reserved = Object.create(null); var reserved = Schema.reserved; +// Core object +reserved['prototype'] = // EventEmitter reserved.emit = reserved.on = reserved.once = +reserved.listeners = +reserved.removeListener = // document properties and functions reserved.collection = reserved.db = @@ -425,19 +447,19 @@ reserved.get = reserved.modelName = reserved.save = reserved.schema = -reserved.set = reserved.toObject = reserved.validate = +reserved.remove = // hooks.js reserved._pres = reserved._posts = 1; -/** +/*! * Document keys to print warnings for */ var warnings = {}; warnings.increment = '`increment` should not be used as a schema path name ' + - 'unless you have disabled versioning.'; + 'unless you have disabled versioning.'; /** * Gets/sets schema paths. @@ -456,19 +478,26 @@ warnings.increment = '`increment` should not be used as a schema path name ' + */ Schema.prototype.path = function(path, obj) { - if (obj == undefined) { - if (this.paths[path]) return this.paths[path]; - if (this.subpaths[path]) return this.subpaths[path]; + if (obj === undefined) { + if (this.paths[path]) { + return this.paths[path]; + } + if (this.subpaths[path]) { + return this.subpaths[path]; + } + if (this.singleNestedPaths[path]) { + return this.singleNestedPaths[path]; + } // subpaths? return /\.\d+\.?.*$/.test(path) - ? getPositionalPath(this, path) - : undefined; + ? getPositionalPath(this, path) + : undefined; } // some path names conflict with document methods if (reserved[path]) { - throw new Error("`" + path + "` may not be used as a schema pathname"); + throw new Error('`' + path + '` may not be used as a schema pathname'); } if (warnings[path]) { @@ -481,13 +510,15 @@ Schema.prototype.path = function(path, obj) { branch = this.tree; subpaths.forEach(function(sub, i) { - if (!branch[sub]) branch[sub] = {}; - if ('object' != typeof branch[sub]) { + if (!branch[sub]) { + branch[sub] = {}; + } + if (typeof branch[sub] !== 'object') { var msg = 'Cannot set nested path `' + path + '`. ' - + 'Parent path `' - + subpaths.slice(0, i).concat([sub]).join('.') - + '` already set to type ' + branch[sub].name - + '.'; + + 'Parent path `' + + subpaths.slice(0, i).concat([sub]).join('.') + + '` already set to type ' + branch[sub].name + + '.'; throw new Error(msg); } branch = branch[sub]; @@ -496,6 +527,27 @@ Schema.prototype.path = function(path, obj) { branch[last] = utils.clone(obj); this.paths[path] = Schema.interpretAsType(path, obj, this.options); + + if (this.paths[path].$isSingleNested) { + for (var key in this.paths[path].schema.paths) { + this.singleNestedPaths[path + '.' + key] = + this.paths[path].schema.paths[key]; + } + for (key in this.paths[path].schema.singleNestedPaths) { + this.singleNestedPaths[path + '.' + key] = + this.paths[path].schema.singleNestedPaths[key]; + } + + this.childSchemas.push({ + schema: this.paths[path].schema, + model: this.paths[path].caster + }); + } else if (this.paths[path].$isMongooseDocumentArray) { + this.childSchemas.push({ + schema: this.paths[path].schema, + model: this.paths[path].casterConstructor + }); + } return this; }; @@ -508,9 +560,13 @@ Schema.prototype.path = function(path, obj) { */ Schema.interpretAsType = function(path, obj, options) { + if (obj instanceof SchemaType) { + return obj; + } + if (obj.constructor) { var constructorName = utils.getFunctionName(obj.constructor); - if (constructorName != 'Object') { + if (constructorName !== 'Object') { var oldObj = obj; obj = {}; obj[options.typeKey] = oldObj; @@ -521,36 +577,80 @@ Schema.interpretAsType = function(path, obj, options) { // and default to mixed if not specified. // { type: { type: String, default: 'freshcut' } } var type = obj[options.typeKey] && (options.typeKey !== 'type' || !obj.type.type) - ? obj[options.typeKey] - : {}; + ? obj[options.typeKey] + : {}; - if ('Object' == utils.getFunctionName(type.constructor) || 'mixed' == type) { + if (utils.getFunctionName(type.constructor) === 'Object' || type === 'mixed') { return new MongooseTypes.Mixed(path, obj); } - if (Array.isArray(type) || Array == type || 'array' == type) { + if (Array.isArray(type) || Array === type || type === 'array') { // if it was specified through { type } look for `cast` - var cast = (Array == type || 'array' == type) - ? obj.cast - : type[0]; + var cast = (Array === type || type === 'array') + ? obj.cast + : type[0]; - if (cast instanceof Schema) { + if (cast && cast.instanceOfSchema) { return new MongooseTypes.DocumentArray(path, cast, obj); } + if (cast && + cast[options.typeKey] && + cast[options.typeKey].instanceOfSchema) { + return new MongooseTypes.DocumentArray(path, cast[options.typeKey], cast); + } + + if (Array.isArray(cast)) { + return new MongooseTypes.Array(path, Schema.interpretAsType(path, cast, options), obj); + } - if ('string' == typeof cast) { + if (typeof cast === 'string') { cast = MongooseTypes[cast.charAt(0).toUpperCase() + cast.substring(1)]; } else if (cast && (!cast[options.typeKey] || (options.typeKey === 'type' && cast.type.type)) - && 'Object' == utils.getFunctionName(cast.constructor) - && Object.keys(cast).length) { - var opts = { minimize: options.minimize }; - return new MongooseTypes.DocumentArray(path, new Schema(cast, opts), obj); + && utils.getFunctionName(cast.constructor) === 'Object') { + if (Object.keys(cast).length) { + // The `minimize` and `typeKey` options propagate to child schemas + // declared inline, like `{ arr: [{ val: { $type: String } }] }`. + // See gh-3560 + var childSchemaOptions = {minimize: options.minimize}; + if (options.typeKey) { + childSchemaOptions.typeKey = options.typeKey; + } + //propagate 'strict' option to child schema + if (options.hasOwnProperty('strict')) { + childSchemaOptions.strict = options.strict; + } + //propagate 'runSettersOnQuery' option to child schema + if (options.hasOwnProperty('runSettersOnQuery')) { + childSchemaOptions.runSettersOnQuery = options.runSettersOnQuery; + } + var childSchema = new Schema(cast, childSchemaOptions); + childSchema.$implicitlyCreated = true; + return new MongooseTypes.DocumentArray(path, childSchema, obj); + } else { + // Special case: empty object becomes mixed + return new MongooseTypes.Array(path, MongooseTypes.Mixed, obj); + } + } + + if (cast) { + type = cast[options.typeKey] && (options.typeKey !== 'type' || !cast.type.type) + ? cast[options.typeKey] + : cast; + + name = typeof type === 'string' + ? type + : type.schemaName || utils.getFunctionName(type); + + if (!(name in MongooseTypes)) { + throw new TypeError('Undefined type `' + name + '` at array `' + path + + '`'); + } } - return new MongooseTypes.Array(path, cast || MongooseTypes.Mixed, obj); + return new MongooseTypes.Array(path, cast || MongooseTypes.Mixed, obj, options); } - if (type instanceof Schema) { + if (type && type.instanceOfSchema) { return new MongooseTypes.Embedded(type, path, obj); } @@ -558,11 +658,11 @@ Schema.interpretAsType = function(path, obj, options) { if (Buffer.isBuffer(type)) { name = 'Buffer'; } else { - name = 'string' == typeof type - ? type + name = typeof type === 'string' + ? type // If not string, `type` is a function. Outside of IE, function.name // gives you the function name. In IE, you need to compute it - : type.schemaName || utils.getFunctionName(type); + : type.schemaName || utils.getFunctionName(type); } if (name) { @@ -575,6 +675,10 @@ Schema.interpretAsType = function(path, obj, options) { 'You can only nest using refs or arrays.'); } + obj = utils.clone(obj, { retainKeyOrder: true }); + if (!('runSettersOnQuery' in obj)) { + obj.runSettersOnQuery = options.runSettersOnQuery; + } return new MongooseTypes[name](path, obj); }; @@ -608,7 +712,9 @@ Schema.prototype.eachPath = function(fn) { */ Schema.prototype.requiredPaths = function requiredPaths(invalidate) { - if (this._requiredpaths && !invalidate) return this._requiredpaths; + if (this._requiredpaths && !invalidate) { + return this._requiredpaths; + } var paths = Object.keys(this.paths), i = paths.length, @@ -616,10 +722,12 @@ Schema.prototype.requiredPaths = function requiredPaths(invalidate) { while (i--) { var path = paths[i]; - if (this.paths[path].isRequired) ret.push(path); + if (this.paths[path].isRequired) { + ret.push(path); + } } - - return this._requiredpaths = ret; + this._requiredpaths = ret; + return this._requiredpaths; }; /** @@ -630,9 +738,11 @@ Schema.prototype.requiredPaths = function requiredPaths(invalidate) { */ Schema.prototype.indexedPaths = function indexedPaths() { - if (this._indexedpaths) return this._indexedpaths; - - return this._indexedpaths = this.indexes(); + if (this._indexedpaths) { + return this._indexedpaths; + } + this._indexedpaths = this.indexes(); + return this._indexedpaths; }; /** @@ -646,16 +756,26 @@ Schema.prototype.indexedPaths = function indexedPaths() { */ Schema.prototype.pathType = function(path) { - if (path in this.paths) return 'real'; - if (path in this.virtuals) return 'virtual'; - if (path in this.nested) return 'nested'; - if (path in this.subpaths) return 'real'; + if (path in this.paths) { + return 'real'; + } + if (path in this.virtuals) { + return 'virtual'; + } + if (path in this.nested) { + return 'nested'; + } + if (path in this.subpaths) { + return 'real'; + } + if (path in this.singleNestedPaths) { + return 'real'; + } if (/\.\d+\.|\.\d+$/.test(path)) { return getPositionalPathType(this, path); - } else { - return 'adhocOrUndefined'; } + return 'adhocOrUndefined'; }; /** @@ -680,6 +800,227 @@ Schema.prototype.hasMixedParent = function(path) { return false; }; +/** + * Setup updatedAt and createdAt timestamps to documents if enabled + * + * @param {Boolean|Object} timestamps timestamps options + * @api private + */ +Schema.prototype.setupTimestamp = function(timestamps) { + if (timestamps) { + var createdAt = handleTimestampOption(timestamps, 'createdAt'); + var updatedAt = handleTimestampOption(timestamps, 'updatedAt'); + var schemaAdditions = {}; + + if (updatedAt && !this.paths[updatedAt]) { + schemaAdditions[updatedAt] = Date; + } + + if (createdAt && !this.paths[createdAt]) { + schemaAdditions[createdAt] = Date; + } + + this.add(schemaAdditions); + + this.pre('save', function(next) { + var defaultTimestamp = new Date(); + var auto_id = this._id && this._id.auto; + + if (createdAt != null && !this.get(createdAt) && this.isSelected(createdAt)) { + this.set(createdAt, auto_id ? this._id.getTimestamp() : defaultTimestamp); + } + + if (updatedAt != null && (this.isNew || this.isModified())) { + var ts = defaultTimestamp; + if (this.isNew) { + if (createdAt != null) { + ts = this.get(createdAt); + } else if (auto_id) { + ts = this._id.getTimestamp(); + } + } + this.set(updatedAt, ts); + } + + next(); + }); + + var genUpdates = function(currentUpdate, overwrite) { + var now = new Date(); + var updates = {}; + var _updates = updates; + if (overwrite) { + if (currentUpdate && currentUpdate.$set) { + currentUpdate = currentUpdate.$set; + updates.$set = {}; + _updates = updates.$set; + } + if (updatedAt != null && !currentUpdate[updatedAt]) { + _updates[updatedAt] = now; + } + if (createdAt != null && !currentUpdate[createdAt]) { + _updates[createdAt] = now; + } + return updates; + } + updates = { $set: {} }; + currentUpdate = currentUpdate || {}; + + if (updatedAt != null && + (!currentUpdate.$currentDate || !currentUpdate.$currentDate[updatedAt])) { + updates.$set[updatedAt] = now; + } + + if (createdAt != null) { + if (currentUpdate[createdAt]) { + delete currentUpdate[createdAt]; + } + if (currentUpdate.$set && currentUpdate.$set[createdAt]) { + delete currentUpdate.$set[createdAt]; + } + + updates.$setOnInsert = {}; + updates.$setOnInsert[createdAt] = now; + } + + return updates; + }; + + this.methods.initializeTimestamps = function() { + if (!this.get(createdAt)) { + this.set(createdAt, new Date()); + } + if (!this.get(updatedAt)) { + this.set(updatedAt, new Date()); + } + return this; + }; + + this.pre('findOneAndUpdate', function(next) { + var overwrite = this.options.overwrite; + this.findOneAndUpdate({}, genUpdates(this.getUpdate(), overwrite), { + overwrite: overwrite + }); + applyTimestampsToChildren(this); + next(); + }); + + this.pre('update', function(next) { + var overwrite = this.options.overwrite; + this.update({}, genUpdates(this.getUpdate(), overwrite), { + overwrite: overwrite + }); + applyTimestampsToChildren(this); + next(); + }); + } +}; + +/*! + * ignore + */ + +function handleTimestampOption(arg, prop) { + if (typeof arg === 'boolean') { + return prop; + } + if (typeof arg[prop] === 'boolean') { + return arg[prop] ? prop : null; + } + if (!(prop in arg)) { + return prop; + } + return arg[prop]; +} + +/*! + * ignore + */ + +function applyTimestampsToChildren(query) { + var now = new Date(); + var update = query.getUpdate(); + var keys = Object.keys(update); + var key; + var schema = query.model.schema; + var len; + var createdAt; + var updatedAt; + var timestamps; + var path; + + var hasDollarKey = keys.length && keys[0].charAt(0) === '$'; + + if (hasDollarKey) { + if (update.$push) { + for (key in update.$push) { + var $path = schema.path(key); + if (update.$push[key] && + $path && + $path.$isMongooseDocumentArray && + $path.schema.options.timestamps) { + timestamps = $path.schema.options.timestamps; + createdAt = handleTimestampOption(timestamps, 'createdAt'); + updatedAt = handleTimestampOption(timestamps, 'updatedAt'); + if (update.$push[key].$each) { + update.$push[key].$each.forEach(function(subdoc) { + if (updatedAt != null) { + subdoc[updatedAt] = now; + } + if (createdAt != null) { + subdoc[createdAt] = now; + } + }); + } else { + if (updatedAt != null) { + update.$push[key][updatedAt] = now; + } + if (createdAt != null) { + update.$push[key][createdAt] = now; + } + } + } + } + } + if (update.$set) { + for (key in update.$set) { + path = schema.path(key); + if (!path) { + continue; + } + if (Array.isArray(update.$set[key]) && path.$isMongooseDocumentArray) { + len = update.$set[key].length; + timestamps = schema.path(key).schema.options.timestamps; + if (timestamps) { + createdAt = handleTimestampOption(timestamps, 'createdAt'); + updatedAt = handleTimestampOption(timestamps, 'updatedAt'); + for (var i = 0; i < len; ++i) { + if (updatedAt != null) { + update.$set[key][i][updatedAt] = now; + } + if (createdAt != null) { + update.$set[key][i][createdAt] = now; + } + } + } + } else if (update.$set[key] && path.$isSingleNested) { + timestamps = schema.path(key).schema.options.timestamps; + if (timestamps) { + createdAt = handleTimestampOption(timestamps, 'createdAt'); + updatedAt = handleTimestampOption(timestamps, 'updatedAt'); + if (updatedAt != null) { + update.$set[key][updatedAt] = now; + } + if (createdAt != null) { + update.$set[key][createdAt] = now; + } + } + } + } + } + } +} + /*! * ignore */ @@ -692,7 +1033,9 @@ function getPositionalPathType(self, path) { var val = self.path(subpaths[0]); var isNested = false; - if (!val) return val; + if (!val) { + return val; + } var last = subpaths.length - 1, subpath, @@ -702,8 +1045,16 @@ function getPositionalPathType(self, path) { isNested = false; subpath = subpaths[i]; - if (i === last && val && !val.schema && !/\D/.test(subpath)) { - if (val instanceof MongooseTypes.Array) { + if (i === last && val && !/\D/.test(subpath)) { + if (val.$isMongooseDocumentArray) { + var oldVal = val; + val = new SchemaType(subpath); + val.cast = function(value, doc, init) { + return oldVal.cast(value, doc, init)[0]; + }; + val.caster = oldVal.caster; + val.schema = oldVal.schema; + } else if (val instanceof MongooseTypes.Array) { // StringSchema, NumberSchema, etc val = val.caster; } else { @@ -713,7 +1064,9 @@ function getPositionalPathType(self, path) { } // ignore if its just a position segment: path.0.subpath - if (!/\D/.test(subpath)) continue; + if (!/\D/.test(subpath)) { + continue; + } if (!(val && val.schema)) { val = undefined; @@ -771,7 +1124,7 @@ Schema.prototype.queue = function(name, args) { * }) * * toySchema.pre('validate', function (next) { - * if (this.name != 'Woody') this.name = 'Woody'; + * if (this.name !== 'Woody') this.name = 'Woody'; * next(); * }) * @@ -783,7 +1136,7 @@ Schema.prototype.queue = function(name, args) { Schema.prototype.pre = function() { var name = arguments[0]; - if (IS_QUERY_HOOK[name]) { + if (IS_KAREEM_HOOK[name]) { this.s.hooks.pre.apply(this.s.hooks, arguments); return this; } @@ -793,28 +1146,36 @@ Schema.prototype.pre = function() { /** * Defines a post hook for the document * - * Post hooks fire `on` the event emitted from document instances of Models compiled from this schema. - * * var schema = new Schema(..); * schema.post('save', function (doc) { * console.log('this fired after a document was saved'); * }); * + * schema.post('find', function(docs) { + * console.log('this fired after you run a find query'); + * }); + * * var Model = mongoose.model('Model', schema); * * var m = new Model(..); - * m.save(function (err) { + * m.save(function(err) { * console.log('this fires after the `post` hook'); * }); * + * m.find(function(err, docs) { + * console.log('this fires after the post find hook'); + * }); + * * @param {String} method name of the method to hook * @param {Function} fn callback - * @see hooks.js https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3 + * @see middleware http://mongoosejs.com/docs/middleware.html + * @see hooks.js https://www.npmjs.com/package/hooks-fixed + * @see kareem http://npmjs.org/package/kareem * @api public */ Schema.prototype.post = function(method, fn) { - if (IS_QUERY_HOOK[method]) { + if (IS_KAREEM_HOOK[method]) { this.s.hooks.post.apply(this.s.hooks, arguments); return this; } @@ -825,13 +1186,18 @@ Schema.prototype.post = function(method, fn) { }]); } + if (fn.length === 3) { + this.s.hooks.post(method + ':error', fn); + return this; + } + return this.queue('post', [arguments[0], function(next) { // wrap original function so that the callback goes last, // for compatibility with old code that is using synchronous post hooks - var self = this; + var _this = this; var args = Array.prototype.slice.call(arguments, 1); fn.call(this, this, function(err) { - return next.apply(self, [err].concat(args)); + return next.apply(_this, [err].concat(args)); }); }]); }; @@ -846,6 +1212,21 @@ Schema.prototype.post = function(method, fn) { */ Schema.prototype.plugin = function(fn, opts) { + if (typeof fn !== 'function') { + throw new Error('First param to `schema.plugin()` must be a function, ' + + 'got "' + (typeof fn) + '"'); + } + + if (opts && + opts.deduplicate) { + for (var i = 0; i < this.plugins.length; ++i) { + if (this.plugins[i].fn === fn) { + return this; + } + } + } + this.plugins.push({ fn: fn, opts: opts }); + fn(this, opts); return this; }; @@ -883,11 +1264,13 @@ Schema.prototype.plugin = function(fn, opts) { */ Schema.prototype.method = function(name, fn) { - if ('string' != typeof name) - for (var i in name) + if (typeof name !== 'string') { + for (var i in name) { this.methods[i] = name[i]; - else + } + } else { this.methods[name] = fn; + } return this; }; @@ -908,17 +1291,19 @@ Schema.prototype.method = function(name, fn) { * * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as statics. * - * @param {String} name - * @param {Function} fn + * @param {String|Object} name + * @param {Function} [fn] * @api public */ Schema.prototype.static = function(name, fn) { - if ('string' != typeof name) - for (var i in name) + if (typeof name !== 'string') { + for (var i in name) { this.statics[i] = name[i]; - else + } + } else { this.statics[name] = fn; + } return this; }; @@ -938,8 +1323,9 @@ Schema.prototype.static = function(name, fn) { Schema.prototype.index = function(fields, options) { options || (options = {}); - if (options.expires) + if (options.expires) { utils.expires(options); + } this._indexes.push([fields, options]); return this; @@ -961,7 +1347,7 @@ Schema.prototype.index = function(fields, options) { */ Schema.prototype.set = function(key, value, _tags) { - if (1 === arguments.length) { + if (arguments.length === 1) { return this.options[key]; } @@ -970,9 +1356,13 @@ Schema.prototype.set = function(key, value, _tags) { this.options[key] = readPref(value, _tags); break; case 'safe': - this.options[key] = false === value - ? { w: 0 } - : value; + this.options[key] = value === false + ? {w: 0} + : value; + break; + case 'timestamps': + this.setupTimestamp(value); + this.options[key] = value; break; default: this.options[key] = value; @@ -1003,12 +1393,17 @@ Schema.prototype.get = function(key) { var indexTypes = '2d 2dsphere hashed text'.split(' '); Object.defineProperty(Schema, 'indexTypes', { - get: function() { return indexTypes; }, - set: function() { throw new Error('Cannot overwrite Schema.indexTypes'); } + get: function() { + return indexTypes; + }, + set: function() { + throw new Error('Cannot overwrite Schema.indexTypes'); + } }); /** - * Compiles indexes from fields and schema-level indexes + * Returns a list of indexes that this schema declares, via `schema.index()` + * or by `index: true` in a path's options. * * @api public */ @@ -1017,13 +1412,15 @@ Schema.prototype.indexes = function() { 'use strict'; var indexes = []; - var seenPrefix = {}; + var schemaStack = []; var collectIndexes = function(schema, prefix) { - if (seenPrefix[prefix]) { + // Ignore infinitely nested schemas, if we've already seen this schema + // along this path there must be a cycle + if (schemaStack.indexOf(schema) !== -1) { return; } - seenPrefix[prefix] = true; + schemaStack.push(schema); prefix = prefix || ''; var key, path, index, field, isObject, options, type; @@ -1033,21 +1430,26 @@ Schema.prototype.indexes = function() { key = keys[i]; path = schema.paths[key]; - if (path instanceof MongooseTypes.DocumentArray) { - collectIndexes(path.schema, key + '.'); + if ((path instanceof MongooseTypes.DocumentArray) || path.$isSingleNested) { + if (path.options.excludeIndexes !== true) { + collectIndexes(path.schema, prefix + key + '.'); + } } else { - index = path._index; + index = path._index || (path.caster && path.caster._index); - if (false !== index && null != index) { + if (index !== false && index !== null && index !== undefined) { field = {}; isObject = utils.isObject(index); options = isObject ? index : {}; - type = 'string' == typeof index ? index : - isObject ? index.type : - false; + type = typeof index === 'string' ? index : + isObject ? index.type : + false; if (type && ~Schema.indexTypes.indexOf(type)) { field[prefix + key] = type; + } else if (options.text) { + field[prefix + key] = 'text'; + delete options.text; } else { field[prefix + key] = 1; } @@ -1062,15 +1464,18 @@ Schema.prototype.indexes = function() { } } + schemaStack.pop(); + if (prefix) { fixSubIndexPaths(schema, prefix); } else { schema._indexes.forEach(function(index) { - if (!('background' in index[1])) index[1].background = true; + if (!('background' in index[1])) { + index[1].background = true; + } }); indexes = indexes.concat(schema._indexes); } - }; collectIndexes(this); @@ -1120,16 +1525,134 @@ Schema.prototype.indexes = function() { */ Schema.prototype.virtual = function(name, options) { + if (options && options.ref) { + if (!options.localField) { + throw new Error('Reference virtuals require `localField` option'); + } + + if (!options.foreignField) { + throw new Error('Reference virtuals require `foreignField` option'); + } + + this.pre('init', function(next, obj) { + if (mpath.has(name, obj)) { + var _v = mpath.get(name, obj); + if (!this.$$populatedVirtuals) { + this.$$populatedVirtuals = {}; + } + + if (options.justOne) { + this.$$populatedVirtuals[name] = Array.isArray(_v) ? + _v[0] : + _v; + } else { + this.$$populatedVirtuals[name] = Array.isArray(_v) ? + _v : + _v == null ? [] : [_v]; + } + + mpath.unset(name, obj); + } + if (this.ownerDocument) { + next(); + return this; + } else { + next(); + } + }); + + var virtual = this.virtual(name); + virtual.options = options; + return virtual. + get(function() { + if (!this.$$populatedVirtuals) { + this.$$populatedVirtuals = {}; + } + if (name in this.$$populatedVirtuals) { + return this.$$populatedVirtuals[name]; + } + return null; + }). + set(function(_v) { + if (!this.$$populatedVirtuals) { + this.$$populatedVirtuals = {}; + } + + if (options.justOne) { + this.$$populatedVirtuals[name] = Array.isArray(_v) ? + _v[0] : + _v; + + if (typeof this.$$populatedVirtuals[name] !== 'object') { + this.$$populatedVirtuals[name] = null; + } + } else { + this.$$populatedVirtuals[name] = Array.isArray(_v) ? + _v : + _v == null ? [] : [_v]; + + this.$$populatedVirtuals[name] = this.$$populatedVirtuals[name].filter(function(doc) { + return doc && typeof doc === 'object'; + }); + } + }); + } + var virtuals = this.virtuals; var parts = name.split('.'); - return virtuals[name] = parts.reduce(function(mem, part, i) { + + if (this.pathType(name) === 'real') { + throw new Error('Virtual path "' + name + '"' + + ' conflicts with a real path in the schema'); + } + + virtuals[name] = parts.reduce(function(mem, part, i) { mem[part] || (mem[part] = (i === parts.length - 1) - ? new VirtualType(options, name) - : {}); + ? new VirtualType(options, name) + : {}); return mem[part]; }, this.tree); + + return virtuals[name]; +}; + +/*! + * ignore + */ + +Schema.prototype._getVirtual = function(name) { + return _getVirtual(this, name); }; +/*! + * ignore + */ + +function _getVirtual(schema, name) { + if (schema.virtuals[name]) { + return schema.virtuals[name]; + } + var parts = name.split('.'); + var cur = ''; + var nestedSchemaPath = ''; + for (var i = 0; i < parts.length; ++i) { + cur += (cur.length > 0 ? '.' : '') + parts[i]; + if (schema.virtuals[cur]) { + if (i === parts.length - 1) { + schema.virtuals[cur].$nestedSchemaPath = nestedSchemaPath; + return schema.virtuals[cur]; + } + continue; + } else if (schema.paths[cur] && schema.paths[cur].schema) { + schema = schema.paths[cur].schema; + nestedSchemaPath += (nestedSchemaPath.length > 0 ? '.' : '') + cur; + cur = ''; + } else { + return null; + } + } +} + /** * Returns the virtual type with the given `name`. * @@ -1156,9 +1679,66 @@ Schema.prototype.remove = function(path) { path.forEach(function(name) { if (this.path(name)) { delete this.paths[name]; + + var pieces = name.split('.'); + var last = pieces.pop(); + var branch = this.tree; + for (var i = 0; i < pieces.length; ++i) { + branch = branch[pieces[i]]; + } + delete branch[last]; + } + }, this); + } +}; + +/** + * Loads an ES6 class into a schema. Maps setters + getters, static methods, and instance methods to schema virtuals, statics, and methods. + * + * @param {Function} model + */ +Schema.prototype.loadClass = function(model, virtualsOnly) { + if (model === Object.prototype || + model === Function.prototype || + model.prototype.hasOwnProperty('$isMongooseModelPrototype')) { + return this; + } + + this.loadClass(Object.getPrototypeOf(model)); + + // Add static methods + if (!virtualsOnly) { + Object.getOwnPropertyNames(model).forEach(function(name) { + if (name.match(/^(length|name|prototype)$/)) { + return; + } + var method = Object.getOwnPropertyDescriptor(model, name); + if (typeof method.value === 'function') { + this.static(name, method.value); } }, this); } + + // Add methods and virtuals + Object.getOwnPropertyNames(model.prototype).forEach(function(name) { + if (name.match(/^(constructor)$/)) { + return; + } + var method = Object.getOwnPropertyDescriptor(model.prototype, name); + if (!virtualsOnly) { + if (typeof method.value === 'function') { + this.method(name, method.value); + } + } + if (typeof method.get === 'function') { + this.virtual(name).get(method.get); + } + if (typeof method.set === 'function') { + this.virtual(name).set(method.set); + } + }, this); + + return this; }; /*! @@ -1166,15 +1746,95 @@ Schema.prototype.remove = function(path) { */ Schema.prototype._getSchema = function(path) { - var schema = this; - var pathschema = schema.path(path); + var _this = this; + var pathschema = _this.path(path); + var resultPath = []; if (pathschema) { + pathschema.$fullPath = path; return pathschema; } + function search(parts, schema) { + var p = parts.length + 1; + var foundschema; + var trypath; + + while (p--) { + trypath = parts.slice(0, p).join('.'); + foundschema = schema.path(trypath); + if (foundschema) { + resultPath.push(trypath); + + if (foundschema.caster) { + // array of Mixed? + if (foundschema.caster instanceof MongooseTypes.Mixed) { + foundschema.caster.$fullPath = resultPath.join('.'); + return foundschema.caster; + } + + // Now that we found the array, we need to check if there + // are remaining document paths to look up for casting. + // Also we need to handle array.$.path since schema.path + // doesn't work for that. + // If there is no foundschema.schema we are dealing with + // a path like array.$ + if (p !== parts.length && foundschema.schema) { + var ret; + if (parts[p] === '$') { + if (p + 1 === parts.length) { + // comments.$ + return foundschema; + } + // comments.$.comments.$.title + ret = search(parts.slice(p + 1), foundschema.schema); + if (ret) { + ret.$isUnderneathDocArray = ret.$isUnderneathDocArray || + !foundschema.schema.$isSingleNested; + } + return ret; + } + // this is the last path of the selector + ret = search(parts.slice(p), foundschema.schema); + if (ret) { + ret.$isUnderneathDocArray = ret.$isUnderneathDocArray || + !foundschema.schema.$isSingleNested; + } + return ret; + } + } + + foundschema.$fullPath = resultPath.join('.'); + + return foundschema; + } + } + } + // look for arrays - return (function search(parts, schema) { + var parts = path.split('.'); + for (var i = 0; i < parts.length; ++i) { + if (parts[i] === '$') { + // Re: gh-5628, because `schema.path()` doesn't take $ into account. + parts[i] = '0'; + } + } + return search(parts, _this); +}; + +/*! + * ignore + */ + +Schema.prototype._getPathType = function(path) { + var _this = this; + var pathschema = _this.path(path); + + if (pathschema) { + return 'real'; + } + + function search(parts, schema) { var p = parts.length + 1, foundschema, trypath; @@ -1186,7 +1846,7 @@ Schema.prototype._getSchema = function(path) { if (foundschema.caster) { // array of Mixed? if (foundschema.caster instanceof MongooseTypes.Mixed) { - return foundschema.caster; + return { schema: foundschema, pathType: 'mixed' }; } // Now that we found the array, we need to check if there @@ -1196,21 +1856,34 @@ Schema.prototype._getSchema = function(path) { // If there is no foundschema.schema we are dealing with // a path like array.$ if (p !== parts.length && foundschema.schema) { - if ('$' === parts[p]) { + if (parts[p] === '$') { + if (p === parts.length - 1) { + return { schema: foundschema, pathType: 'nested' }; + } // comments.$.comments.$.title return search(parts.slice(p + 1), foundschema.schema); - } else { - // this is the last path of the selector - return search(parts.slice(p), foundschema.schema); } + // this is the last path of the selector + return search(parts.slice(p), foundschema.schema); } + return { + schema: foundschema, + pathType: foundschema.$isSingleNested ? 'nested' : 'array' + }; } - return foundschema; + return { schema: foundschema, pathType: 'real' }; + } else if (p === parts.length && schema.nested[trypath]) { + return { schema: schema, pathType: 'nested' }; } } - })(path.split('.'), schema); + return { schema: foundschema || schema, pathType: 'undefined' }; + } + + // look for arrays + return search(path.split('.'), _this); }; + /*! * Module exports. */ diff --git a/lib/schema/array.js b/lib/schema/array.js index 945fb2debb9..2679c7d71a5 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -2,22 +2,28 @@ * Module dependencies. */ -var SchemaType = require('../schematype'), - CastError = SchemaType.CastError, - Types = { - Boolean: require('./boolean'), - Date: require('./date'), - Number: require('./number'), - String: require('./string'), - ObjectId: require('./objectid'), - Buffer: require('./buffer') - }, - MongooseArray = require('../types').Array, - EmbeddedDoc = require('../types').Embedded, - Mixed = require('./mixed'), - cast = require('../cast'), - utils = require('../utils'), - isMongooseObject = utils.isMongooseObject; +var $exists = require('./operators/exists'); +var $type = require('./operators/type'); +var SchemaType = require('../schematype'); +var CastError = SchemaType.CastError; +var Types = { + Array: SchemaArray, + Boolean: require('./boolean'), + Date: require('./date'), + Number: require('./number'), + String: require('./string'), + ObjectId: require('./objectid'), + Buffer: require('./buffer') +}; +var Mixed = require('./mixed'); +var cast = require('../cast'); +var util = require('util'); +var utils = require('../utils'); +var castToNumber = require('./operators/helpers').castToNumber; +var geospatial = require('./operators/geospatial'); + +var MongooseArray; +var EmbeddedDoc; /** * Array SchemaType constructor @@ -26,76 +32,100 @@ var SchemaType = require('../schematype'), * @param {SchemaType} cast * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ -function SchemaArray(key, cast, options) { +function SchemaArray(key, cast, options, schemaOptions) { + // lazy load + EmbeddedDoc || (EmbeddedDoc = require('../types').Embedded); + + var typeKey = 'type'; + if (schemaOptions && schemaOptions.typeKey) { + typeKey = schemaOptions.typeKey; + } + if (cast) { var castOptions = {}; - if ('Object' === utils.getFunctionName(cast.constructor)) { - if (cast.type) { + if (utils.getFunctionName(cast.constructor) === 'Object') { + if (cast[typeKey]) { // support { type: Woot } castOptions = utils.clone(cast); // do not alter user arguments - delete castOptions.type; - cast = cast.type; + delete castOptions[typeKey]; + cast = cast[typeKey]; } else { cast = Mixed; } } // support { type: 'String' } - var name = 'string' == typeof cast - ? cast - : utils.getFunctionName(cast); + var name = typeof cast === 'string' + ? cast + : utils.getFunctionName(cast); var caster = name in Types - ? Types[name] - : cast; + ? Types[name] + : cast; this.casterConstructor = caster; - this.caster = new caster(null, castOptions); + if (typeof caster === 'function' && !caster.$isArraySubdocument) { + this.caster = new caster(null, castOptions); + } else { + this.caster = caster; + } + if (!(this.caster instanceof EmbeddedDoc)) { this.caster.path = key; } } + this.$isMongooseArray = true; + SchemaType.call(this, key, options, 'Array'); - var self = this, - defaultArr, - fn; + var defaultArr; + var fn; - if (this.defaultValue) { + if (this.defaultValue != null) { defaultArr = this.defaultValue; - fn = 'function' == typeof defaultArr; + fn = typeof defaultArr === 'function'; } - this.default(function() { - var arr = fn ? defaultArr() : defaultArr || []; - return new MongooseArray(arr, self.path, this); - }); + if (!('defaultValue' in this) || this.defaultValue !== void 0) { + this.default(function() { + var arr = []; + if (fn) { + arr = defaultArr(); + } else if (defaultArr != null) { + arr = arr.concat(defaultArr); + } + // Leave it up to `cast()` to convert the array + return arr; + }); + } } /** * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaArray.schemaName = 'Array'; /*! * Inherits from SchemaType. */ -SchemaArray.prototype = Object.create( SchemaType.prototype ); +SchemaArray.prototype = Object.create(SchemaType.prototype); SchemaArray.prototype.constructor = SchemaArray; /** - * Check required + * Check if the given value satisfies a required validator. The given value + * must be not null nor undefined, and have a positive length. * - * @param {Array} value - * @api private + * @param {Any} value + * @return {Boolean} + * @api public */ SchemaArray.prototype.checkRequired = function(value) { @@ -129,14 +159,16 @@ SchemaArray.prototype.applyGetters = function(value, scope) { */ SchemaArray.prototype.cast = function(value, doc, init) { - if (Array.isArray(value)) { + // lazy load + MongooseArray || (MongooseArray = require('../types').Array); + if (Array.isArray(value)) { if (!value.length && doc) { var indexes = doc.schema.indexedPaths(); for (var i = 0, l = indexes.length; i < l; ++i) { var pathIndex = indexes[i][0][this.path]; - if ('2dsphere' === pathIndex || '2d' === pathIndex) { + if (pathIndex === '2dsphere' || pathIndex === '2d') { return; } } @@ -144,6 +176,10 @@ SchemaArray.prototype.cast = function(value, doc, init) { if (!(value && value.isMongooseArray)) { value = new MongooseArray(value, this.path, doc); + } else if (value && value.isMongooseArray) { + // We need to create a new array, otherwise change tracking will + // update the old doc (gh-4449) + value = new MongooseArray(value, this.path, doc); } if (this.caster) { @@ -153,19 +189,18 @@ SchemaArray.prototype.cast = function(value, doc, init) { } } catch (e) { // rethrow - throw new CastError(e.type, value, this.path); + throw new CastError('[' + e.kind + ']', util.inspect(value), this.path, e); } } return value; - } else { - // gh-2442: if we're loading this from the db and its not an array, mark - // the whole array as modified. - if (!!doc && !!init) { - doc.markModified(this.path); - } - return this.cast([value], doc, init); } + // gh-2442: if we're loading this from the db and its not an array, mark + // the whole array as modified. + if (!!doc && !!init) { + doc.markModified(this.path); + } + return this.cast([value], doc, init); }; /** @@ -184,16 +219,27 @@ SchemaArray.prototype.castForQuery = function($conditional, value) { handler = this.$conditionalHandlers[$conditional]; if (!handler) { - throw new Error("Can't use " + $conditional + " with Array."); + throw new Error('Can\'t use ' + $conditional + ' with Array.'); } val = handler.call(this, value); - } else { - val = $conditional; + var Constructor = this.casterConstructor; + + if (val && + Constructor.discriminators && + Constructor.schema.options.discriminatorKey && + typeof val[Constructor.schema.options.discriminatorKey] === 'string' && + Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]]) { + Constructor = Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]]; + } + var proto = this.casterConstructor.prototype; - var method = proto.castForQuery || proto.cast; + var method = proto && (proto.castForQuery || proto.cast); + if (!method && Constructor.castForQuery) { + method = Constructor.castForQuery; + } var caster = this.caster; if (Array.isArray(val)) { @@ -201,112 +247,25 @@ SchemaArray.prototype.castForQuery = function($conditional, value) { if (utils.isObject(v) && v.$elemMatch) { return v; } - if (method) v = method.call(caster, v); - return isMongooseObject(v) ? - v.toObject({ virtuals: false }) : - v; + if (method) { + v = method.call(caster, v); + return v; + } + if (v != null) { + v = new Constructor(v); + return v; + } + return v; }); - } else if (method) { val = method.call(caster, val); + } else if (val != null) { + val = new Constructor(val); } } - return val && isMongooseObject(val) ? - val.toObject({ virtuals: false }) : - val; -}; - -/*! - * @ignore - * - * $atomic cast helpers - */ - -function castToNumber(val) { - return Types.Number.prototype.cast.call(this, val); -} - -function castArraysOfNumbers(arr, self) { - self || (self = this); - - arr.forEach(function(v, i) { - if (Array.isArray(v)) { - castArraysOfNumbers(v, self); - } else { - arr[i] = castToNumber.call(self, v); - } - }); -} - -function cast$near(val) { - if (Array.isArray(val)) { - castArraysOfNumbers(val, this); - return val; - } - - if (val && val.$geometry) { - return cast$geometry(val, this); - } - - return SchemaArray.prototype.castForQuery.call(this, val); -} - -function cast$geometry(val, self) { - switch (val.$geometry.type) { - case 'Polygon': - case 'LineString': - case 'Point': - castArraysOfNumbers(val.$geometry.coordinates, self); - break; - default: - // ignore unknowns - break; - } - - if (val.$maxDistance) { - val.$maxDistance = castToNumber.call(self, val.$maxDistance); - } - return val; -} - -function cast$within(val) { - var self = this; - - if (val.$maxDistance) { - val.$maxDistance = castToNumber.call(self, val.$maxDistance); - } - - if (val.$box || val.$polygon) { - var type = val.$box ? '$box' : '$polygon'; - val[type].forEach(function(arr) { - if (!Array.isArray(arr)) { - var msg = 'Invalid $within $box argument. ' - + 'Expected an array, received ' + arr; - throw new TypeError(msg); - } - arr.forEach(function(v, i) { - arr[i] = castToNumber.call(this, v); - }); - }); - } else if (val.$center || val.$centerSphere) { - type = val.$center ? '$center' : '$centerSphere'; - val[type].forEach(function(item, i) { - if (Array.isArray(item)) { - item.forEach(function(v, j) { - item[j] = castToNumber.call(this, v); - }); - } else { - val[type][i] = castToNumber.call(this, item); - } - }); - } else if (val.$geometry) { - cast$geometry(val, this); - } - - return val; -} +}; function cast$all(val) { if (!Array.isArray(val)) { @@ -341,20 +300,12 @@ function cast$elemMatch(val) { return cast(this.casterConstructor.schema, val); } -function cast$geoIntersects(val) { - var geo = val.$geometry; - if (!geo) return; - - cast$geometry(val, this); - return val; -} - var handle = SchemaArray.prototype.$conditionalHandlers = {}; handle.$all = cast$all; handle.$options = String; handle.$elemMatch = cast$elemMatch; -handle.$geoIntersects = cast$geoIntersects; +handle.$geoIntersects = geospatial.cast$geoIntersects; handle.$or = handle.$and = function(val) { if (!Array.isArray(val)) { throw new TypeError('conditional $or/$and require array'); @@ -369,14 +320,18 @@ handle.$or = handle.$and = function(val) { }; handle.$near = -handle.$nearSphere = cast$near; +handle.$nearSphere = geospatial.cast$near; handle.$within = -handle.$geoWithin = cast$within; +handle.$geoWithin = geospatial.cast$within; handle.$size = +handle.$minDistance = handle.$maxDistance = castToNumber; +handle.$exists = $exists; +handle.$type = $type; + handle.$eq = handle.$gt = handle.$gte = diff --git a/lib/schema/boolean.js b/lib/schema/boolean.js index 971ab157dc1..f90c86a6c2f 100644 --- a/lib/schema/boolean.js +++ b/lib/schema/boolean.js @@ -5,6 +5,7 @@ var utils = require('../utils'); var SchemaType = require('../schematype'); +var CastError = SchemaType.CastError; /** * Boolean SchemaType constructor. @@ -12,7 +13,7 @@ var SchemaType = require('../schematype'); * @param {String} path * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function SchemaBoolean(path, options) { @@ -23,20 +24,24 @@ function SchemaBoolean(path, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaBoolean.schemaName = 'Boolean'; /*! * Inherits from SchemaType. */ -SchemaBoolean.prototype = Object.create( SchemaType.prototype ); +SchemaBoolean.prototype = Object.create(SchemaType.prototype); SchemaBoolean.prototype.constructor = SchemaBoolean; /** - * Required validator + * Check if the given value satisfies a required validator. For a boolean + * to satisfy a required validator, it must be strictly equal to true or to + * false. * - * @api private + * @param {Any} value + * @return {Boolean} + * @api public */ SchemaBoolean.prototype.checkRequired = function(value) { @@ -47,19 +52,41 @@ SchemaBoolean.prototype.checkRequired = function(value) { * Casts to boolean * * @param {Object} value + * @param {Object} model - this value is optional * @api private */ -SchemaBoolean.prototype.cast = function(value) { - if (null === value) return value; - if ('0' === value) return false; - if ('true' === value) return true; - if ('false' === value) return false; - return !!value; +SchemaBoolean.prototype.cast = function(value, model) { + if (value === null) { + return value; + } + + if (this.options.strictBool || (model && model.schema.options.strictBool && this.options.strictBool !== false)) { + // strict mode (throws if value is not a boolean, instead of converting) + if (value === true || value === 'true' || value === 1 || value === '1') { + return true; + } + if (value === false || value === 'false' || value === 0 || value === '0') { + return false; + } + throw new CastError('boolean', value, this.path); + } else { + // legacy mode + if (value === '0') { + return false; + } + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + return !!value; + } }; SchemaBoolean.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, {}); + utils.options(SchemaType.prototype.$conditionalHandlers, {}); /** * Casts contents for queries. @@ -71,17 +98,17 @@ SchemaBoolean.$conditionalHandlers = SchemaBoolean.prototype.castForQuery = function($conditional, val) { var handler; - if (2 === arguments.length) { + if (arguments.length === 2) { handler = SchemaBoolean.$conditionalHandlers[$conditional]; if (handler) { return handler.call(this, val); } - return this.cast(val); + return this._castForQuery(val); } - return this.cast($conditional); + return this._castForQuery($conditional); }; /*! diff --git a/lib/schema/buffer.js b/lib/schema/buffer.js index 159a28ab9d2..f0dcf7e442a 100644 --- a/lib/schema/buffer.js +++ b/lib/schema/buffer.js @@ -2,9 +2,10 @@ * Module dependencies. */ +var handleBitwiseOperator = require('./operators/bitwise'); var utils = require('../utils'); -var MongooseBuffer = require('../types').Buffer; +var MongooseBuffer = require('../types/buffer'); var SchemaType = require('../schematype'); var Binary = MongooseBuffer.Binary; @@ -15,9 +16,9 @@ var Document; * Buffer SchemaType constructor * * @param {String} key - * @param {SchemaType} cast + * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function SchemaBuffer(key, options) { @@ -28,28 +29,32 @@ function SchemaBuffer(key, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaBuffer.schemaName = 'Buffer'; /*! * Inherits from SchemaType. */ -SchemaBuffer.prototype = Object.create( SchemaType.prototype ); +SchemaBuffer.prototype = Object.create(SchemaType.prototype); SchemaBuffer.prototype.constructor = SchemaBuffer; /** - * Check required + * Check if the given value satisfies a required validator. To satisfy a + * required validator, a buffer must not be null or undefined and have + * non-zero length. * - * @api private + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ SchemaBuffer.prototype.checkRequired = function(value, doc) { if (SchemaType._isRef(this, value, doc, true)) { - return null != value; - } else { - return !!(value && value.length); + return !!value; } + return !!(value && value.length); }; /** @@ -66,7 +71,7 @@ SchemaBuffer.prototype.cast = function(value, doc, init) { if (SchemaType._isRef(this, value, doc, init)) { // wait! we may need to cast this to a document - if (null == value) { + if (value === null || value === undefined) { return value; } @@ -108,6 +113,9 @@ SchemaBuffer.prototype.cast = function(value, doc, init) { if (Buffer.isBuffer(value)) { if (!value || !value.isMongooseBuffer) { value = new MongooseBuffer(value, [this.path, doc]); + if (this.options.subtype != null) { + value._subtype = this.options.subtype; + } } return value; @@ -120,17 +128,46 @@ SchemaBuffer.prototype.cast = function(value, doc, init) { return ret; } - if (null === value) return value; + if (value === null) { + return value; + } var type = typeof value; - if ('string' == type || 'number' == type || Array.isArray(value)) { + if (type === 'string' || type === 'number' || Array.isArray(value)) { + if (type === 'number') { + value = [value]; + } ret = new MongooseBuffer(value, [this.path, doc]); + if (this.options.subtype != null) { + ret._subtype = this.options.subtype; + } return ret; } throw new CastError('buffer', value, this.path); }; +/** + * Sets the default [subtype](https://studio3t.com/whats-new/best-practices-uuid-mongodb/) + * for this buffer. You can find a [list of allowed subtypes here](http://api.mongodb.com/python/current/api/bson/binary.html). + * + * ####Example: + * + * var s = new Schema({ uuid: { type: Buffer, subtype: 4 }); + * var M = db.model('M', s); + * var m = new M({ uuid: 'test string' }); + * m.uuid._subtype; // 4 + * + * @param {Number} subtype the default subtype + * @return {SchemaType} this + * @api public + */ + +SchemaBuffer.prototype.subtype = function(subtype) { + this.options.subtype = subtype; + return this; +}; + /*! * ignore */ @@ -139,12 +176,16 @@ function handleSingle(val) { } SchemaBuffer.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$gt' : handleSingle, - '$gte': handleSingle, - '$lt' : handleSingle, - '$lte': handleSingle - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $bitsAllClear: handleBitwiseOperator, + $bitsAnyClear: handleBitwiseOperator, + $bitsAllSet: handleBitwiseOperator, + $bitsAnySet: handleBitwiseOperator, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle + }); /** * Casts contents for queries. @@ -158,13 +199,14 @@ SchemaBuffer.prototype.castForQuery = function($conditional, val) { var handler; if (arguments.length === 2) { handler = this.$conditionalHandlers[$conditional]; - if (!handler) - throw new Error("Can't use " + $conditional + " with Buffer."); + if (!handler) { + throw new Error('Can\'t use ' + $conditional + ' with Buffer.'); + } return handler.call(this, val); - } else { - val = $conditional; - return this.cast(val).toObject(); } + val = $conditional; + var casted = this._castForQuery(val); + return casted ? casted.toObject({ transform: false, virtuals: false }) : casted; }; /*! diff --git a/lib/schema/date.js b/lib/schema/date.js index 57d36e9ab16..9f97096b5ba 100644 --- a/lib/schema/date.js +++ b/lib/schema/date.js @@ -2,7 +2,7 @@ * Module requirements. */ -var errorMessages = require('../error').messages; +var MongooseError = require('../error'); var utils = require('../utils'); var SchemaType = require('../schematype'); @@ -15,7 +15,7 @@ var CastError = SchemaType.CastError; * @param {String} key * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function SchemaDate(key, options) { @@ -26,20 +26,20 @@ function SchemaDate(key, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaDate.schemaName = 'Date'; /*! * Inherits from SchemaType. */ -SchemaDate.prototype = Object.create( SchemaType.prototype ); +SchemaDate.prototype = Object.create(SchemaType.prototype); SchemaDate.prototype.constructor = SchemaDate; /** * Declares a TTL index (rounded to the nearest second) for _Date_ types only. * - * This sets the `expiresAfterSeconds` index option available in MongoDB >= 2.1.2. + * This sets the `expireAfterSeconds` index option available in MongoDB >= 2.1.2. * This index type is only compatible with Date types. * * ####Example: @@ -68,7 +68,7 @@ SchemaDate.prototype.constructor = SchemaDate; */ SchemaDate.prototype.expires = function(when) { - if (!this._index || 'Object' !== this._index.constructor.name) { + if (!this._index || this._index.constructor.name !== 'Object') { this._index = {}; } @@ -78,9 +78,13 @@ SchemaDate.prototype.expires = function(when) { }; /** - * Required validator for date + * Check if the given value satisfies a required validator. To satisfy + * a required validator, the given value must be an instance of `Date`. * - * @api private + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ SchemaDate.prototype.checkRequired = function(value) { @@ -121,17 +125,17 @@ SchemaDate.prototype.checkRequired = function(value) { SchemaDate.prototype.min = function(value, message) { if (this.minValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.minValidator; + return v.validator !== this.minValidator; }, this); } if (value) { - var msg = message || errorMessages.Date.min; + var msg = message || MongooseError.messages.Date.min; msg = msg.replace(/{MIN}/, (value === Date.now ? 'Date.now()' : this.cast(value).toString())); - var self = this; + var _this = this; this.validators.push({ validator: this.minValidator = function(val) { - var min = (value === Date.now ? value() : self.cast(value)); + var min = (value === Date.now ? value() : _this.cast(value)); return val === null || val.valueOf() >= min.valueOf(); }, message: msg, @@ -177,17 +181,17 @@ SchemaDate.prototype.min = function(value, message) { SchemaDate.prototype.max = function(value, message) { if (this.maxValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.maxValidator; + return v.validator !== this.maxValidator; }, this); } if (value) { - var msg = message || errorMessages.Date.max; + var msg = message || MongooseError.messages.Date.max; msg = msg.replace(/{MAX}/, (value === Date.now ? 'Date.now()' : this.cast(value).toString())); - var self = this; + var _this = this; this.validators.push({ validator: this.maxValidator = function(val) { - var max = (value === Date.now ? value() : self.cast(value)); + var max = (value === Date.now ? value() : _this.cast(value)); return val === null || val.valueOf() <= max.valueOf(); }, message: msg, @@ -208,27 +212,35 @@ SchemaDate.prototype.max = function(value, message) { SchemaDate.prototype.cast = function(value) { // If null or undefined - if (value == null || value === '') + if (value === null || value === void 0 || value === '') { return null; + } + + if (value instanceof Date) { + if (isNaN(value.valueOf())) { + throw new CastError('date', value, this.path); + } - if (value instanceof Date) return value; + } var date; - // support for timestamps - if (typeof value !== 'undefined') { - if (value instanceof Number || 'number' == typeof value - || String(value) == Number(value)) { - date = new Date(Number(value)); - } else if (value.toString) { - // support for date strings - date = new Date(value.toString()); - } + if (typeof value === 'boolean') { + throw new CastError('date', value, this.path); + } - if (date.toString() != 'Invalid Date') { - return date; - } + if (value instanceof Number || typeof value === 'number' + || String(value) == Number(value)) { + // support for timestamps + date = new Date(Number(value)); + } else if (value.valueOf) { + // support for moment.js + date = new Date(value.valueOf()); + } + + if (!isNaN(date.valueOf())) { + return date; } throw new CastError('date', value, this.path); @@ -245,12 +257,12 @@ function handleSingle(val) { } SchemaDate.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$gt': handleSingle, - '$gte': handleSingle, - '$lt': handleSingle, - '$lte': handleSingle - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle + }); /** @@ -264,14 +276,14 @@ SchemaDate.prototype.$conditionalHandlers = SchemaDate.prototype.castForQuery = function($conditional, val) { var handler; - if (2 !== arguments.length) { - return this.cast($conditional); + if (arguments.length !== 2) { + return this._castForQuery($conditional); } handler = this.$conditionalHandlers[$conditional]; if (!handler) { - throw new Error("Can't use " + $conditional + " with Date."); + throw new Error('Can\'t use ' + $conditional + ' with Date.'); } return handler.call(this, val); diff --git a/lib/schema/decimal128.js b/lib/schema/decimal128.js new file mode 100644 index 00000000000..0fba739cee0 --- /dev/null +++ b/lib/schema/decimal128.js @@ -0,0 +1,150 @@ +/* eslint no-empty: 1 */ + +/*! + * Module dependencies. + */ + +var SchemaType = require('../schematype'); +var CastError = SchemaType.CastError; +var Decimal128Type = require('../types/decimal128'); +var utils = require('../utils'); +var Document; + +/** + * Decimal128 SchemaType constructor. + * + * @param {String} key + * @param {Object} options + * @inherits SchemaType + * @api public + */ + +function Decimal128(key, options) { + SchemaType.call(this, key, options, 'Decimal128'); +} + +/** + * This schema type's name, to defend against minifiers that mangle + * function names. + * + * @api public + */ +Decimal128.schemaName = 'Decimal128'; + +/*! + * Inherits from SchemaType. + */ +Decimal128.prototype = Object.create(SchemaType.prototype); +Decimal128.prototype.constructor = Decimal128; + +/** + * Check if the given value satisfies a required validator. + * + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public + */ + +Decimal128.prototype.checkRequired = function checkRequired(value, doc) { + if (SchemaType._isRef(this, value, doc, true)) { + return !!value; + } + return value instanceof Decimal128Type; +}; + +/** + * Casts to Decimal128 + * + * @param {Object} value + * @param {Object} doc + * @param {Boolean} init whether this is an initialization cast + * @api private + */ + +Decimal128.prototype.cast = function(value, doc, init) { + if (SchemaType._isRef(this, value, doc, init)) { + // wait! we may need to cast this to a document + + if (value === null || value === undefined) { + return value; + } + + // lazy load + Document || (Document = require('./../document')); + + if (value instanceof Document) { + value.$__.wasPopulated = true; + return value; + } + + // setting a populated path + if (value instanceof Decimal128Type) { + return value; + } else if (Buffer.isBuffer(value) || !utils.isObject(value)) { + throw new CastError('Decimal128', value, this.path); + } + + // Handle the case where user directly sets a populated + // path to a plain object; cast to the Model used in + // the population query. + var path = doc.$__fullPath(this.path); + var owner = doc.ownerDocument ? doc.ownerDocument() : doc; + var pop = owner.populated(path, true); + var ret = value; + if (!doc.$__.populated || + !doc.$__.populated[path] || + !doc.$__.populated[path].options || + !doc.$__.populated[path].options.options || + !doc.$__.populated[path].options.options.lean) { + ret = new pop.options.model(value); + ret.$__.wasPopulated = true; + } + + return ret; + } + + if (value == null) { + return value; + } + + if (typeof value === 'object' && typeof value.$numberDecimal === 'string') { + return Decimal128Type.fromString(value.$numberDecimal); + } + + if (value instanceof Decimal128Type) { + return value; + } + + if (typeof value === 'string') { + return Decimal128Type.fromString(value); + } + + if (Buffer.isBuffer(value)) { + return new Decimal128Type(value); + } + + throw new CastError('Decimal128', value, this.path); +}; + +/*! + * ignore + */ + +function handleSingle(val) { + return this.cast(val); +} + +Decimal128.prototype.$conditionalHandlers = + utils.options(SchemaType.prototype.$conditionalHandlers, { + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle + }); + +/*! + * Module exports. + */ + +module.exports = Decimal128; diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index d51b6de5244..ddfde8aa695 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -6,9 +6,15 @@ var ArrayType = require('./array'); var CastError = require('../error/cast'); -var MongooseDocumentArray = require('../types/documentarray'); +var Document = require('../document'); +var EventEmitter = require('events').EventEmitter; var SchemaType = require('../schematype'); -var Subdocument = require('../types/embedded'); +var discriminator = require('../services/model/discriminator'); +var util = require('util'); +var utils = require('../utils'); + +var MongooseDocumentArray; +var Subdocument; /** * SubdocsArray SchemaType constructor @@ -17,10 +23,52 @@ var Subdocument = require('../types/embedded'); * @param {Schema} schema * @param {Object} options * @inherits SchemaArray - * @api private + * @api public */ function DocumentArray(key, schema, options) { + var EmbeddedDocument = _createConstructor(schema, options); + EmbeddedDocument.prototype.$basePath = key; + + ArrayType.call(this, key, EmbeddedDocument, options); + + this.schema = schema; + this.$isMongooseDocumentArray = true; + var fn = this.defaultValue; + + if (!('defaultValue' in this) || fn !== void 0) { + this.default(function() { + var arr = fn.call(this); + if (!Array.isArray(arr)) { + arr = [arr]; + } + // Leave it up to `cast()` to convert this to a documentarray + return arr; + }); + } +} + +/** + * This schema type's name, to defend against minifiers that mangle + * function names. + * + * @api public + */ +DocumentArray.schemaName = 'DocumentArray'; + +/*! + * Inherits from ArrayType. + */ +DocumentArray.prototype = Object.create(ArrayType.prototype); +DocumentArray.prototype.constructor = DocumentArray; + +/*! + * Ignore + */ + +function _createConstructor(schema, options) { + Subdocument || (Subdocument = require('../types/embedded')); + // compile an embedded document for this schema function EmbeddedDocument() { Subdocument.apply(this, arguments); @@ -29,44 +77,54 @@ function DocumentArray(key, schema, options) { EmbeddedDocument.prototype = Object.create(Subdocument.prototype); EmbeddedDocument.prototype.$__setSchema(schema); EmbeddedDocument.schema = schema; + EmbeddedDocument.prototype.constructor = EmbeddedDocument; + EmbeddedDocument.$isArraySubdocument = true; // apply methods - for (var i in schema.methods) + for (var i in schema.methods) { EmbeddedDocument.prototype[i] = schema.methods[i]; + } // apply statics - for (i in schema.statics) + for (i in schema.statics) { EmbeddedDocument[i] = schema.statics[i]; + } - EmbeddedDocument.options = options; - this.schema = schema; - - ArrayType.call(this, key, EmbeddedDocument, options); + for (i in EventEmitter.prototype) { + EmbeddedDocument[i] = EventEmitter.prototype[i]; + } - this.schema = schema; - var path = this.path; - var fn = this.defaultValue; + EmbeddedDocument.options = options; - this.default(function() { - var arr = fn.call(this); - if (!Array.isArray(arr)) arr = [arr]; - return new MongooseDocumentArray(arr, path, this); - }); + return EmbeddedDocument; } -/** - * This schema type's name, to defend against minifiers that mangle - * function names. - * - * @api private - */ -DocumentArray.schemaName = 'DocumentArray'; - /*! - * Inherits from ArrayType. + * Ignore */ -DocumentArray.prototype = Object.create( ArrayType.prototype ); -DocumentArray.prototype.constructor = DocumentArray; + +DocumentArray.prototype.discriminator = function(name, schema) { + if (typeof name === 'function') { + name = utils.getFunctionName(name); + } + + schema = discriminator(this.casterConstructor, name, schema); + + var EmbeddedDocument = _createConstructor(schema); + EmbeddedDocument.baseCasterConstructor = this.casterConstructor; + + try { + Object.defineProperty(EmbeddedDocument, 'name', { + value: name + }); + } catch (error) { + // Ignore error, only happens on old versions of node + } + + this.casterConstructor.discriminators[name] = EmbeddedDocument; + + return this.casterConstructor.discriminators[name]; +}; /** * Performs local validations first, then validations on each embedded doc @@ -74,7 +132,11 @@ DocumentArray.prototype.constructor = DocumentArray; * @api private */ -DocumentArray.prototype.doValidate = function(array, fn, scope) { +DocumentArray.prototype.doValidate = function(array, fn, scope, options) { + // lazy load + MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray')); + + var _this = this; SchemaType.prototype.doValidate.call(this, array, function(err) { if (err) { return fn(err); @@ -83,12 +145,27 @@ DocumentArray.prototype.doValidate = function(array, fn, scope) { var count = array && array.length; var error; - if (!count) return fn(); + if (!count) { + return fn(); + } + if (options && options.updateValidator) { + return fn(); + } + if (!array.isMongooseDocumentArray) { + array = new MongooseDocumentArray(array, _this.path, scope); + } // handle sparse arrays, do not use array.forEach which does not // iterate over sparse elements yet reports array.length including // them :( + function callback(err) { + if (err) { + error = err; + } + --count || fn(error); + } + for (var i = 0, len = count; i < len; ++i) { // sidestep sparse entries var doc = array[i]; @@ -97,12 +174,20 @@ DocumentArray.prototype.doValidate = function(array, fn, scope) { continue; } - doc.validate(function(err) { - if (err) { - error = err; - } - --count || fn(error); - }); + // If you set the array index directly, the doc might not yet be + // a full fledged mongoose subdoc, so make it into one. + if (!(doc instanceof Subdocument)) { + doc = array[i] = new _this.casterConstructor(doc, array, undefined, + undefined, i); + } + + // HACK: use $__original_validate to avoid promises so bluebird doesn't + // complain + if (doc.$__original_validate) { + doc.$__original_validate({__noPromise: true}, callback); + } else { + doc.validate({__noPromise: true}, callback); + } } }, scope); }; @@ -120,12 +205,16 @@ DocumentArray.prototype.doValidate = function(array, fn, scope) { DocumentArray.prototype.doValidateSync = function(array, scope) { var schemaTypeError = SchemaType.prototype.doValidateSync.call(this, array, scope); - if (schemaTypeError) return schemaTypeError; + if (schemaTypeError) { + return schemaTypeError; + } var count = array && array.length, resultError = null; - if (!count) return; + if (!count) { + return; + } // handle sparse arrays, do not use array.forEach which does not // iterate over sparse elements yet reports array.length including @@ -133,10 +222,21 @@ DocumentArray.prototype.doValidateSync = function(array, scope) { for (var i = 0, len = count; i < len; ++i) { // only first error - if ( resultError ) break; + if (resultError) { + break; + } // sidestep sparse entries var doc = array[i]; - if (!doc) continue; + if (!doc) { + continue; + } + + // If you set the array index directly, the doc might not yet be + // a full fledged mongoose subdoc, so make it into one. + if (!(doc instanceof Subdocument)) { + doc = array[i] = new this.casterConstructor(doc, array, undefined, + undefined, i); + } var subdocValidateError = doc.validateSync(); @@ -156,10 +256,14 @@ DocumentArray.prototype.doValidateSync = function(array, scope) { * @api private */ -DocumentArray.prototype.cast = function(value, doc, init, prev) { - var selected, - subdoc, - i; +DocumentArray.prototype.cast = function(value, doc, init, prev, options) { + // lazy load + MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray')); + + var selected; + var subdoc; + var i; + var _opts = { transform: false, virtuals: false }; if (!Array.isArray(value)) { // gh-2442 mark whole array as modified if we're initializing a doc from @@ -170,44 +274,71 @@ DocumentArray.prototype.cast = function(value, doc, init, prev) { return this.cast([value], doc, init, prev); } - if (!(value && value.isMongooseDocumentArray)) { + if (!(value && value.isMongooseDocumentArray) && + (!options || !options.skipDocumentArrayCast)) { value = new MongooseDocumentArray(value, this.path, doc); if (prev && prev._handlers) { for (var key in prev._handlers) { doc.removeListener(key, prev._handlers[key]); } } + } else if (value && value.isMongooseDocumentArray) { + // We need to create a new array, otherwise change tracking will + // update the old doc (gh-4449) + value = new MongooseDocumentArray(value, this.path, doc); } i = value.length; while (i--) { + if (!value[i]) { + continue; + } + + var Constructor = this.casterConstructor; + if (Constructor.discriminators && + typeof value[i][Constructor.schema.options.discriminatorKey] === 'string' && + Constructor.discriminators[value[i][Constructor.schema.options.discriminatorKey]]) { + Constructor = Constructor.discriminators[value[i][Constructor.schema.options.discriminatorKey]]; + } + + // Check if the document has a different schema (re gh-3701) + if ((value[i] instanceof Document) && + value[i].schema !== Constructor.schema) { + value[i] = value[i].toObject({ transform: false, virtuals: false }); + } if (!(value[i] instanceof Subdocument) && value[i]) { if (init) { - selected || (selected = scopePaths(this, doc.$__.selected, init)); - subdoc = new this.casterConstructor(null, value, true, selected, i); + if (doc) { + selected || (selected = scopePaths(this, doc.$__.selected, init)); + } else { + selected = true; + } + + subdoc = new Constructor(null, value, true, selected, i); value[i] = subdoc.init(value[i]); } else { - try { + if (prev && (subdoc = prev.id(value[i]._id))) { subdoc = prev.id(value[i]._id); - } catch (e) {} + } - if (prev && subdoc) { - // handle resetting doc with existing id but differing data - // doc.array = [{ doc: 'val' }] + if (prev && subdoc && utils.deepEqual(subdoc.toObject(_opts), value[i])) { + // handle resetting doc with existing id and same data subdoc.set(value[i]); // if set() is hooked it will have no return value // see gh-746 value[i] = subdoc; } else { try { - subdoc = new this.casterConstructor(value[i], value, undefined, - undefined, i); + subdoc = new Constructor(value[i], value, undefined, + undefined, i); // if set() is hooked it will have no return value // see gh-746 value[i] = subdoc; } catch (error) { - throw new CastError('embedded', value[i], value._path); + var valueInErrorMessage = util.inspect(value[i]); + throw new CastError('embedded', valueInErrorMessage, + value._path, error); } } } @@ -227,20 +358,30 @@ DocumentArray.prototype.cast = function(value, doc, init, prev) { */ function scopePaths(array, fields, init) { - if (!(init && fields)) return undefined; + if (!(init && fields)) { + return undefined; + } - var path = array.path + '.', - keys = Object.keys(fields), - i = keys.length, - selected = {}, - hasKeys, - key; + var path = array.path + '.'; + var keys = Object.keys(fields); + var i = keys.length; + var selected = {}; + var hasKeys; + var key; + var sub; while (i--) { key = keys[i]; - if (0 === key.indexOf(path)) { + if (key.indexOf(path) === 0) { + sub = key.substring(path.length); + if (sub === '$') { + continue; + } + if (sub.indexOf('$.') === 0) { + sub = sub.substr(2); + } hasKeys || (hasKeys = true); - selected[key.substring(path.length)] = fields[key]; + selected[sub] = fields[key]; } } diff --git a/lib/schema/embedded.js b/lib/schema/embedded.js index 283d79a8abf..89ba08ddf9f 100644 --- a/lib/schema/embedded.js +++ b/lib/schema/embedded.js @@ -1,5 +1,17 @@ +'use strict'; + +/*! + * Module dependencies. + */ + +var $exists = require('./operators/exists'); +var EventEmitter = require('events').EventEmitter; var SchemaType = require('../schematype'); -var Subdocument = require('../types/subdocument'); +var castToNumber = require('./operators/helpers').castToNumber; +var discriminator = require('../services/model/discriminator'); +var geospatial = require('./operators/geospatial'); + +var Subdocument; module.exports = Embedded; @@ -10,26 +22,113 @@ module.exports = Embedded; * @param {String} key * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function Embedded(schema, path, options) { - var _embedded = function() { + this.caster = _createConstructor(schema); + this.caster.prototype.$basePath = path; + this.schema = schema; + this.$isSingleNested = true; + SchemaType.call(this, path, options, 'Embedded'); +} + +/*! + * ignore + */ + +Embedded.prototype = Object.create(SchemaType.prototype); + +/*! + * ignore + */ + +function _createConstructor(schema) { + // lazy load + Subdocument || (Subdocument = require('../types/subdocument')); + + var _embedded = function SingleNested(value, path, parent) { + var _this = this; + + this.$parent = parent; Subdocument.apply(this, arguments); + + if (parent) { + parent.on('save', function() { + _this.emit('save', _this); + _this.constructor.emit('save', _this); + }); + + parent.on('isNew', function(val) { + _this.isNew = val; + _this.emit('isNew', val); + _this.constructor.emit('isNew', val); + }); + } }; _embedded.prototype = Object.create(Subdocument.prototype); _embedded.prototype.$__setSchema(schema); + _embedded.prototype.constructor = _embedded; _embedded.schema = schema; _embedded.$isSingleNested = true; - _embedded.prototype.$basePath = path; + _embedded.prototype.toBSON = function() { + return this.toObject({ + transform: false, + retainKeyOrder: true, + virtuals: false, + _skipDepopulateTopLevel: true, + depopulate: true, + flattenDecimals: false + }); + }; - this.caster = _embedded; - this.schema = schema; - this.$isSingleNested = true; - SchemaType.call(this, path, options, 'Embedded'); + // apply methods + for (var i in schema.methods) { + _embedded.prototype[i] = schema.methods[i]; + } + + // apply statics + for (i in schema.statics) { + _embedded[i] = schema.statics[i]; + } + + for (i in EventEmitter.prototype) { + _embedded[i] = EventEmitter.prototype[i]; + } + + return _embedded; } -Embedded.prototype = Object.create(SchemaType.prototype); +/*! + * Special case for when users use a common location schema to represent + * locations for use with $geoWithin. + * https://docs.mongodb.org/manual/reference/operator/query/geoWithin/ + * + * @param {Object} val + * @api private + */ + +Embedded.prototype.$conditionalHandlers.$geoWithin = function handle$geoWithin(val) { + return { $geometry: this.castForQuery(val.$geometry) }; +}; + +/*! + * ignore + */ + +Embedded.prototype.$conditionalHandlers.$near = +Embedded.prototype.$conditionalHandlers.$nearSphere = geospatial.cast$near; + +Embedded.prototype.$conditionalHandlers.$within = +Embedded.prototype.$conditionalHandlers.$geoWithin = geospatial.cast$within; + +Embedded.prototype.$conditionalHandlers.$geoIntersects = + geospatial.cast$geoIntersects; + +Embedded.prototype.$conditionalHandlers.$minDistance = castToNumber; +Embedded.prototype.$conditionalHandlers.$maxDistance = castToNumber; + +Embedded.prototype.$conditionalHandlers.$exists = $exists; /** * Casts contents @@ -38,10 +137,34 @@ Embedded.prototype = Object.create(SchemaType.prototype); * @api private */ -Embedded.prototype.cast = function(val, doc) { - var subdoc = new this.caster(); - subdoc = subdoc.init(val); - subdoc.$parent = doc; +Embedded.prototype.cast = function(val, doc, init, priorVal) { + if (val && val.$isSingleNested) { + return val; + } + + var Constructor = this.caster; + var discriminatorKey = Constructor.schema.options.discriminatorKey; + if (val != null && + Constructor.discriminators && + typeof val[discriminatorKey] === 'string' && + Constructor.discriminators[val[discriminatorKey]]) { + Constructor = Constructor.discriminators[val[discriminatorKey]]; + } + + var subdoc; + if (init) { + subdoc = new Constructor(void 0, doc ? doc.$__.selected : void 0, doc); + subdoc.init(val); + } else { + if (Object.keys(val).length === 0) { + return new Constructor({}, doc ? doc.$__.selected : void 0, doc); + } + + return new Constructor(val, doc ? doc.$__.selected : void 0, doc, undefined, { + priorDoc: priorVal + }); + } + return subdoc; }; @@ -61,17 +184,79 @@ Embedded.prototype.castForQuery = function($conditional, val) { throw new Error('Can\'t use ' + $conditional); } return handler.call(this, val); - } else { - val = $conditional; - return new this.caster(val). - toObject({ virtuals: false }); } + val = $conditional; + if (val == null) { + return val; + } + + if (this.options.runSetters) { + val = this._applySetters(val); + } + + return new this.caster(val); }; -Embedded.prototype.doValidate = function(value, fn) { - value.validate(fn, { __noPromise: true }); +/** + * Async validation on this single nested doc. + * + * @api private + */ + +Embedded.prototype.doValidate = function(value, fn, scope) { + var Constructor = this.caster; + var discriminatorKey = Constructor.schema.options.discriminatorKey; + if (value != null && + Constructor.discriminators && + typeof value[discriminatorKey] === 'string' && + Constructor.discriminators[value[discriminatorKey]]) { + Constructor = Constructor.discriminators[value[discriminatorKey]]; + } + + SchemaType.prototype.doValidate.call(this, value, function(error) { + if (error) { + return fn(error); + } + if (!value) { + return fn(null); + } + + if (!(value instanceof Constructor)) { + value = new Constructor(value); + } + value.validate({__noPromise: true}, fn); + }, scope); }; -Embedded.prototype.doValidateSync = function(value) { +/** + * Synchronously validate this single nested doc + * + * @api private + */ + +Embedded.prototype.doValidateSync = function(value, scope) { + var schemaTypeError = SchemaType.prototype.doValidateSync.call(this, value, scope); + if (schemaTypeError) { + return schemaTypeError; + } + if (!value) { + return; + } return value.validateSync(); }; + +/** + * Adds a discriminator to this property + * + * @param {String} name + * @param {Schema} schema fields to add to the schema for instances of this sub-class + * @api public + */ + +Embedded.prototype.discriminator = function(name, schema) { + discriminator(this.caster, name, schema); + + this.caster.discriminators[name] = _createConstructor(schema); + + return this.caster.discriminators[name]; +}; diff --git a/lib/schema/index.js b/lib/schema/index.js index f2935c15e2c..dc2a5645928 100644 --- a/lib/schema/index.js +++ b/lib/schema/index.js @@ -23,6 +23,8 @@ exports.ObjectId = require('./objectid'); exports.Mixed = require('./mixed'); +exports.Decimal128 = exports.Decimal = require('./decimal128'); + // alias exports.Oid = exports.ObjectId; diff --git a/lib/schema/mixed.js b/lib/schema/mixed.js index ff2173a0675..e4c23caaa80 100644 --- a/lib/schema/mixed.js +++ b/lib/schema/mixed.js @@ -1,4 +1,3 @@ - /*! * Module dependencies. */ @@ -12,18 +11,16 @@ var utils = require('../utils'); * @param {String} path * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function Mixed(path, options) { if (options && options.default) { var def = options.default; - if (Array.isArray(def) && 0 === def.length) { + if (Array.isArray(def) && def.length === 0) { // make sure empty array defaults are handled options.default = Array; - } else if (!options.shared && - utils.isObject(def) && - 0 === Object.keys(def).length) { + } else if (!options.shared && utils.isObject(def) && Object.keys(def).length === 0) { // prevent odd "shared" objects between documents options.default = function() { return {}; @@ -38,26 +35,16 @@ function Mixed(path, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ Mixed.schemaName = 'Mixed'; /*! * Inherits from SchemaType. */ -Mixed.prototype = Object.create( SchemaType.prototype ); +Mixed.prototype = Object.create(SchemaType.prototype); Mixed.prototype.constructor = Mixed; -/** - * Required validator - * - * @api private - */ - -Mixed.prototype.checkRequired = function(val) { - return (val !== undefined) && (val !== null); -}; - /** * Casts `val` for Mixed. * @@ -80,7 +67,9 @@ Mixed.prototype.cast = function(val) { */ Mixed.prototype.castForQuery = function($cond, val) { - if (arguments.length === 2) return val; + if (arguments.length === 2) { + return val; + } return $cond; }; diff --git a/lib/schema/number.js b/lib/schema/number.js index b9a65dcf948..e5b9bc7fa62 100644 --- a/lib/schema/number.js +++ b/lib/schema/number.js @@ -2,11 +2,12 @@ * Module requirements. */ -var SchemaType = require('../schematype'), - CastError = SchemaType.CastError, - errorMessages = require('../error').messages, - utils = require('../utils'), - Document; +var SchemaType = require('../schematype'); +var CastError = SchemaType.CastError; +var handleBitwiseOperator = require('./operators/bitwise'); +var MongooseError = require('../error'); +var utils = require('../utils'); +var Document; /** * Number SchemaType constructor. @@ -14,7 +15,7 @@ var SchemaType = require('../schematype'), * @param {String} key * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function SchemaNumber(key, options) { @@ -25,28 +26,30 @@ function SchemaNumber(key, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaNumber.schemaName = 'Number'; /*! * Inherits from SchemaType. */ -SchemaNumber.prototype = Object.create( SchemaType.prototype ); +SchemaNumber.prototype = Object.create(SchemaType.prototype); SchemaNumber.prototype.constructor = SchemaNumber; /** - * Required validator for number + * Check if the given value satisfies a required validator. * - * @api private + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ SchemaNumber.prototype.checkRequired = function checkRequired(value, doc) { if (SchemaType._isRef(this, value, doc, true)) { - return null != value; - } else { - return typeof value == 'number' || value instanceof Number; + return !!value; } + return typeof value === 'number' || value instanceof Number; }; /** @@ -83,16 +86,16 @@ SchemaNumber.prototype.checkRequired = function checkRequired(value, doc) { SchemaNumber.prototype.min = function(value, message) { if (this.minValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.minValidator; + return v.validator !== this.minValidator; }, this); } - if (null != value) { - var msg = message || errorMessages.Number.min; + if (value !== null && value !== undefined) { + var msg = message || MongooseError.messages.Number.min; msg = msg.replace(/{MIN}/, value); this.validators.push({ validator: this.minValidator = function(v) { - return v === null || v >= value; + return v == null || v >= value; }, message: msg, type: 'min', @@ -137,16 +140,16 @@ SchemaNumber.prototype.min = function(value, message) { SchemaNumber.prototype.max = function(value, message) { if (this.maxValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.maxValidator; + return v.validator !== this.maxValidator; }, this); } - if (null != value) { - var msg = message || errorMessages.Number.max; + if (value !== null && value !== undefined) { + var msg = message || MongooseError.messages.Number.max; msg = msg.replace(/{MAX}/, value); this.validators.push({ validator: this.maxValidator = function(v) { - return v === null || v <= value; + return v == null || v <= value; }, message: msg, type: 'max', @@ -170,7 +173,7 @@ SchemaNumber.prototype.cast = function(value, doc, init) { if (SchemaType._isRef(this, value, doc, init)) { // wait! we may need to cast this to a document - if (null == value) { + if (value === null || value === undefined) { return value; } @@ -183,7 +186,7 @@ SchemaNumber.prototype.cast = function(value, doc, init) { } // setting a populated path - if ('number' == typeof value) { + if (typeof value === 'number') { return value; } else if (Buffer.isBuffer(value) || !utils.isObject(value)) { throw new CastError('number', value, this.path); @@ -200,20 +203,27 @@ SchemaNumber.prototype.cast = function(value, doc, init) { return ret; } - var val = value && value._id - ? value._id // documents - : value; + var val = value && typeof value._id !== 'undefined' ? + value._id : // documents + value; if (!isNaN(val)) { - if (null === val) return val; - if ('' === val) return null; + if (val === null) { + return val; + } + if (val === '') { + return null; + } if (typeof val === 'string' || typeof val === 'boolean') { val = Number(val); } - if (val instanceof Number) return val; - if ('number' == typeof val) return val; - if (val.toString && !Array.isArray(val) && - val.toString() == Number(val)) { + if (val instanceof Number) { + return val; + } + if (typeof val === 'number') { + return val; + } + if (val.toString && !Array.isArray(val) && val.toString() == Number(val)) { return new Number(val); } } @@ -230,23 +240,27 @@ function handleSingle(val) { } function handleArray(val) { - var self = this; + var _this = this; if (!Array.isArray(val)) { return [this.cast(val)]; } return val.map(function(m) { - return self.cast(m); + return _this.cast(m); }); } SchemaNumber.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$gt' : handleSingle, - '$gte': handleSingle, - '$lt' : handleSingle, - '$lte': handleSingle, - '$mod': handleArray - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $bitsAllClear: handleBitwiseOperator, + $bitsAnyClear: handleBitwiseOperator, + $bitsAllSet: handleBitwiseOperator, + $bitsAnySet: handleBitwiseOperator, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle, + $mod: handleArray + }); /** * Casts contents for queries. @@ -260,13 +274,13 @@ SchemaNumber.prototype.castForQuery = function($conditional, val) { var handler; if (arguments.length === 2) { handler = this.$conditionalHandlers[$conditional]; - if (!handler) - throw new Error("Can't use " + $conditional + " with Number."); + if (!handler) { + throw new Error('Can\'t use ' + $conditional + ' with Number.'); + } return handler.call(this, val); - } else { - val = this.cast($conditional); - return val == null ? val : val; } + val = this._castForQuery($conditional); + return val; }; /*! diff --git a/lib/schema/objectid.js b/lib/schema/objectid.js index 26f6621c175..83c0d1ab510 100644 --- a/lib/schema/objectid.js +++ b/lib/schema/objectid.js @@ -16,10 +16,19 @@ var SchemaType = require('../schematype'), * @param {String} key * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function ObjectId(key, options) { + var isKeyHexStr = typeof key === 'string' && key.length === 24 && /^a-f0-9$/i.test(key); + var suppressWarning = options && options.suppressWarning; + if ((isKeyHexStr || typeof key === 'undefined') && !suppressWarning) { + console.warn('mongoose: To create a new ObjectId please try ' + + '`Mongoose.Types.ObjectId` instead of using ' + + '`Mongoose.Schema.ObjectId`. Set the `suppressWarning` option if ' + + 'you\'re trying to create a hex char path in your schema.'); + console.trace(); + } SchemaType.call(this, key, options, 'ObjectID'); } @@ -27,14 +36,14 @@ function ObjectId(key, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ ObjectId.schemaName = 'ObjectId'; /*! * Inherits from SchemaType. */ -ObjectId.prototype = Object.create( SchemaType.prototype ); +ObjectId.prototype = Object.create(SchemaType.prototype); ObjectId.prototype.constructor = ObjectId; /** @@ -54,17 +63,19 @@ ObjectId.prototype.auto = function(turnOn) { }; /** - * Check required + * Check if the given value satisfies a required validator. * - * @api private + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ ObjectId.prototype.checkRequired = function checkRequired(value, doc) { if (SchemaType._isRef(this, value, doc, true)) { - return null != value; - } else { - return value instanceof oid; + return !!value; } + return value instanceof oid; }; /** @@ -80,7 +91,7 @@ ObjectId.prototype.cast = function(value, doc, init) { if (SchemaType._isRef(this, value, doc, init)) { // wait! we may need to cast this to a document - if (null == value) { + if (value === null || value === undefined) { return value; } @@ -95,6 +106,8 @@ ObjectId.prototype.cast = function(value, doc, init) { // setting a populated path if (value instanceof oid) { return value; + } else if ((value.constructor.name || '').toLowerCase() === 'objectid') { + return new oid(value.toHexString()); } else if (Buffer.isBuffer(value) || !utils.isObject(value)) { throw new CastError('ObjectId', value, this.path); } @@ -105,16 +118,26 @@ ObjectId.prototype.cast = function(value, doc, init) { var path = doc.$__fullPath(this.path); var owner = doc.ownerDocument ? doc.ownerDocument() : doc; var pop = owner.populated(path, true); - var ret = new pop.options.model(value); - ret.$__.wasPopulated = true; + var ret = value; + if (!doc.$__.populated || + !doc.$__.populated[path] || + !doc.$__.populated[path].options || + !doc.$__.populated[path].options.options || + !doc.$__.populated[path].options.options.lean) { + ret = new pop.options.model(value); + ret.$__.wasPopulated = true; + } + return ret; } - // If null or undefined - if (value == null) return value; + if (value === null || value === undefined) { + return value; + } - if (value instanceof oid) + if (value instanceof oid) { return value; + } if (value._id) { if (value._id instanceof oid) { @@ -122,14 +145,15 @@ ObjectId.prototype.cast = function(value, doc, init) { } if (value._id.toString instanceof Function) { try { - return oid.createFromHexString(value._id.toString()); - } catch (e) {} + return new oid(value._id.toString()); + } catch (e) { + } } } if (value.toString instanceof Function) { try { - return oid.createFromHexString(value.toString()); + return new oid(value.toString()); } catch (err) { throw new CastError('ObjectId', value, this.path); } @@ -147,12 +171,12 @@ function handleSingle(val) { } ObjectId.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$gt': handleSingle, - '$gte': handleSingle, - '$lt': handleSingle, - '$lte': handleSingle - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle + }); /** * Casts contents for queries. @@ -166,12 +190,12 @@ ObjectId.prototype.castForQuery = function($conditional, val) { var handler; if (arguments.length === 2) { handler = this.$conditionalHandlers[$conditional]; - if (!handler) - throw new Error("Can't use " + $conditional + " with ObjectId."); + if (!handler) { + throw new Error('Can\'t use ' + $conditional + ' with ObjectId.'); + } return handler.call(this, val); - } else { - return this.cast($conditional); } + return this._castForQuery($conditional); }; /*! @@ -183,7 +207,17 @@ function defaultId() { } function resetId(v) { - this.$__._id = null; + Document || (Document = require('./../document')); + + if (v === void 0) { + var _v = new oid; + this.$__._id = _v; + return _v; + } + + if (this instanceof Document) { + this.$__._id = v; + } return v; } diff --git a/lib/schema/operators/bitwise.js b/lib/schema/operators/bitwise.js new file mode 100644 index 00000000000..c1fdd34e5ab --- /dev/null +++ b/lib/schema/operators/bitwise.js @@ -0,0 +1,36 @@ +/*! + * Module requirements. + */ + +var CastError = require('../../error/cast'); + +/*! + * ignore + */ + +function handleBitwiseOperator(val) { + var _this = this; + if (Array.isArray(val)) { + return val.map(function(v) { + return _castNumber(_this.path, v); + }); + } else if (Buffer.isBuffer(val)) { + return val; + } + // Assume trying to cast to number + return _castNumber(_this.path, val); +} + +/*! + * ignore + */ + +function _castNumber(path, num) { + var v = Number(num); + if (isNaN(v)) { + throw new CastError('number', num, path); + } + return v; +} + +module.exports = handleBitwiseOperator; diff --git a/lib/schema/operators/exists.js b/lib/schema/operators/exists.js new file mode 100644 index 00000000000..6716112e6b6 --- /dev/null +++ b/lib/schema/operators/exists.js @@ -0,0 +1,13 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function(val) { + if (typeof val !== 'boolean') { + throw new Error('$exists parameter must be a boolean!'); + } + + return val; +}; diff --git a/lib/schema/operators/geospatial.js b/lib/schema/operators/geospatial.js new file mode 100644 index 00000000000..b490d2db2ec --- /dev/null +++ b/lib/schema/operators/geospatial.js @@ -0,0 +1,100 @@ +/*! + * Module requirements. + */ + +var castArraysOfNumbers = require('./helpers').castArraysOfNumbers; +var castToNumber = require('./helpers').castToNumber; + +/*! + * ignore + */ + +exports.cast$geoIntersects = cast$geoIntersects; +exports.cast$near = cast$near; +exports.cast$within = cast$within; + +function cast$near(val) { + var SchemaArray = require('../array'); + + if (Array.isArray(val)) { + castArraysOfNumbers(val, this); + return val; + } + + _castMinMaxDistance(this, val); + + if (val && val.$geometry) { + return cast$geometry(val, this); + } + + return SchemaArray.prototype.castForQuery.call(this, val); +} + +function cast$geometry(val, self) { + switch (val.$geometry.type) { + case 'Polygon': + case 'LineString': + case 'Point': + castArraysOfNumbers(val.$geometry.coordinates, self); + break; + default: + // ignore unknowns + break; + } + + _castMinMaxDistance(this, val); + + return val; +} + +function cast$within(val) { + _castMinMaxDistance(this, val); + + if (val.$box || val.$polygon) { + var type = val.$box ? '$box' : '$polygon'; + val[type].forEach(function(arr) { + if (!Array.isArray(arr)) { + var msg = 'Invalid $within $box argument. ' + + 'Expected an array, received ' + arr; + throw new TypeError(msg); + } + arr.forEach(function(v, i) { + arr[i] = castToNumber.call(this, v); + }); + }); + } else if (val.$center || val.$centerSphere) { + type = val.$center ? '$center' : '$centerSphere'; + val[type].forEach(function(item, i) { + if (Array.isArray(item)) { + item.forEach(function(v, j) { + item[j] = castToNumber.call(this, v); + }); + } else { + val[type][i] = castToNumber.call(this, item); + } + }); + } else if (val.$geometry) { + cast$geometry(val, this); + } + + return val; +} + +function cast$geoIntersects(val) { + var geo = val.$geometry; + if (!geo) { + return; + } + + cast$geometry(val, this); + return val; +} + +function _castMinMaxDistance(self, val) { + if (val.$maxDistance) { + val.$maxDistance = castToNumber.call(self, val.$maxDistance); + } + if (val.$minDistance) { + val.$minDistance = castToNumber.call(self, val.$minDistance); + } +} diff --git a/lib/schema/operators/helpers.js b/lib/schema/operators/helpers.js new file mode 100644 index 00000000000..850354a8b57 --- /dev/null +++ b/lib/schema/operators/helpers.js @@ -0,0 +1,34 @@ +'use strict'; + +/*! + * Module requirements. + */ + +var Types = { + Number: require('../number') +}; + +/*! + * @ignore + */ + +exports.castToNumber = castToNumber; +exports.castArraysOfNumbers = castArraysOfNumbers; + +/*! + * @ignore + */ + +function castToNumber(val) { + return Types.Number.prototype.cast.call(this, val); +} + +function castArraysOfNumbers(arr, self) { + arr.forEach(function(v, i) { + if (Array.isArray(v)) { + castArraysOfNumbers(v, self); + } else { + arr[i] = castToNumber.call(self, v); + } + }); +} diff --git a/lib/schema/operators/type.js b/lib/schema/operators/type.js new file mode 100644 index 00000000000..c8e391ac2ae --- /dev/null +++ b/lib/schema/operators/type.js @@ -0,0 +1,13 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function(val) { + if (typeof val !== 'number' && typeof val !== 'string') { + throw new Error('$type parameter must be number or string'); + } + + return val; +}; diff --git a/lib/schema/string.js b/lib/schema/string.js index 799fcc5808d..8b4a1842728 100644 --- a/lib/schema/string.js +++ b/lib/schema/string.js @@ -1,13 +1,12 @@ - /*! * Module dependencies. */ -var SchemaType = require('../schematype'), - CastError = SchemaType.CastError, - errorMessages = require('../error').messages, - utils = require('../utils'), - Document; +var SchemaType = require('../schematype'); +var CastError = SchemaType.CastError; +var MongooseError = require('../error'); +var utils = require('../utils'); +var Document; /** * String SchemaType constructor. @@ -15,7 +14,7 @@ var SchemaType = require('../schematype'), * @param {String} key * @param {Object} options * @inherits SchemaType - * @api private + * @api public */ function SchemaString(key, options) { @@ -28,14 +27,14 @@ function SchemaString(key, options) { * This schema type's name, to defend against minifiers that mangle * function names. * - * @api private + * @api public */ SchemaString.schemaName = 'String'; /*! * Inherits from SchemaType. */ -SchemaString.prototype = Object.create( SchemaType.prototype ); +SchemaString.prototype = Object.create(SchemaType.prototype); SchemaString.prototype.constructor = SchemaString; /** @@ -43,7 +42,7 @@ SchemaString.prototype.constructor = SchemaString; * * ####Example: * - * var states = 'opening open closing closed'.split(' ') + * var states = ['opening', 'open', 'closing', 'closed'] * var s = new Schema({ state: { type: String, enum: states }}) * var M = db.model('M', s) * var m = new M({ state: 'invalid' }) @@ -54,11 +53,11 @@ SchemaString.prototype.constructor = SchemaString; * }) * * // or with custom error messages - * var enu = { - * values: 'opening open closing closed'.split(' '), + * var enum = { + * values: ['opening', 'open', 'closing', 'closed'], * message: 'enum validator failed for path `{PATH}` with value `{VALUE}`' * } - * var s = new Schema({ state: { type: String, enum: enu }) + * var s = new Schema({ state: { type: String, enum: enum }) * var M = db.model('M', s) * var m = new M({ state: 'invalid' }) * m.save(function (err) { @@ -76,12 +75,12 @@ SchemaString.prototype.constructor = SchemaString; SchemaString.prototype.enum = function() { if (this.enumValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.enumValidator; + return v.validator !== this.enumValidator; }, this); this.enumValidator = false; } - if (undefined === arguments[0] || false === arguments[0]) { + if (arguments[0] === void 0 || arguments[0] === false) { return this; } @@ -93,7 +92,7 @@ SchemaString.prototype.enum = function() { errorMessage = arguments[0].message; } else { values = arguments; - errorMessage = errorMessages.String.enum; + errorMessage = MongooseError.messages.String.enum; } for (var i = 0; i < values.length; i++) { @@ -117,7 +116,7 @@ SchemaString.prototype.enum = function() { }; /** - * Adds a lowercase setter. + * Adds a lowercase [setter](http://mongoosejs.com/docs/api.html#schematype_SchemaType-set). * * ####Example: * @@ -126,20 +125,33 @@ SchemaString.prototype.enum = function() { * var m = new M({ email: 'SomeEmail@example.COM' }); * console.log(m.email) // someemail@example.com * + * NOTE: Setters do not run on queries by default. Use the `runSettersOnQuery` option: + * + * // Must use `runSettersOnQuery` as shown below, otherwise `email` will + * // **not** be lowercased. + * M.updateOne({}, { $set: { email: 'SomeEmail@example.COM' } }, { runSettersOnQuery: true }); + * * @api public * @return {SchemaType} this */ -SchemaString.prototype.lowercase = function() { +SchemaString.prototype.lowercase = function(shouldApply) { + if (arguments.length > 0 && !shouldApply) { + return this; + } return this.set(function(v, self) { - if ('string' != typeof v) v = self.cast(v); - if (v) return v.toLowerCase(); + if (typeof v !== 'string') { + v = self.cast(v); + } + if (v) { + return v.toLowerCase(); + } return v; }); }; /** - * Adds an uppercase setter. + * Adds an uppercase [setter](http://mongoosejs.com/docs/api.html#schematype_SchemaType-set). * * ####Example: * @@ -148,20 +160,33 @@ SchemaString.prototype.lowercase = function() { * var m = new M({ caps: 'an example' }); * console.log(m.caps) // AN EXAMPLE * + * NOTE: Setters do not run on queries by default. Use the `runSettersOnQuery` option: + * + * // Must use `runSettersOnQuery` as shown below, otherwise `email` will + * // **not** be lowercased. + * M.updateOne({}, { $set: { email: 'SomeEmail@example.COM' } }, { runSettersOnQuery: true }); + * * @api public * @return {SchemaType} this */ -SchemaString.prototype.uppercase = function() { +SchemaString.prototype.uppercase = function(shouldApply) { + if (arguments.length > 0 && !shouldApply) { + return this; + } return this.set(function(v, self) { - if ('string' != typeof v) v = self.cast(v); - if (v) return v.toUpperCase(); + if (typeof v !== 'string') { + v = self.cast(v); + } + if (v) { + return v.toUpperCase(); + } return v; }); }; /** - * Adds a trim setter. + * Adds a trim [setter](http://mongoosejs.com/docs/api.html#schematype_SchemaType-set). * * The string value will be trimmed when set. * @@ -174,14 +199,27 @@ SchemaString.prototype.uppercase = function() { * var m = new M({ name: string }) * console.log(m.name.length) // 9 * + * NOTE: Setters do not run on queries by default. Use the `runSettersOnQuery` option: + * + * // Must use `runSettersOnQuery` as shown below, otherwise `email` will + * // **not** be lowercased. + * M.updateOne({}, { $set: { email: 'SomeEmail@example.COM' } }, { runSettersOnQuery: true }); + * * @api public * @return {SchemaType} this */ -SchemaString.prototype.trim = function() { +SchemaString.prototype.trim = function(shouldTrim) { + if (arguments.length > 0 && !shouldTrim) { + return this; + } return this.set(function(v, self) { - if ('string' != typeof v) v = self.cast(v); - if (v) return v.trim(); + if (typeof v !== 'string') { + v = self.cast(v); + } + if (v) { + return v.trim(); + } return v; }); }; @@ -220,12 +258,12 @@ SchemaString.prototype.trim = function() { SchemaString.prototype.minlength = function(value, message) { if (this.minlengthValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.minlengthValidator; + return v.validator !== this.minlengthValidator; }, this); } - if (null != value) { - var msg = message || errorMessages.String.minlength; + if (value !== null && value !== undefined) { + var msg = message || MongooseError.messages.String.minlength; msg = msg.replace(/{MINLENGTH}/, value); this.validators.push({ validator: this.minlengthValidator = function(v) { @@ -274,12 +312,12 @@ SchemaString.prototype.minlength = function(value, message) { SchemaString.prototype.maxlength = function(value, message) { if (this.maxlengthValidator) { this.validators = this.validators.filter(function(v) { - return v.validator != this.maxlengthValidator; + return v.validator !== this.maxlengthValidator; }, this); } - if (null != value) { - var msg = message || errorMessages.String.maxlength; + if (value !== null && value !== undefined) { + var msg = message || MongooseError.messages.String.maxlength; msg = msg.replace(/{MAXLENGTH}/, value); this.validators.push({ validator: this.maxlengthValidator = function(v) { @@ -335,16 +373,16 @@ SchemaString.prototype.maxlength = function(value, message) { SchemaString.prototype.match = function match(regExp, message) { // yes, we allow multiple match validators - var msg = message || errorMessages.String.match; + var msg = message || MongooseError.messages.String.match; var matchValidator = function(v) { if (!regExp) { return false; } - var ret = ((null != v && '' !== v) - ? regExp.test(v) - : true); + var ret = ((v != null && v !== '') + ? regExp.test(v) + : true); return ret; }; @@ -358,18 +396,22 @@ SchemaString.prototype.match = function match(regExp, message) { }; /** - * Check required - * - * @param {String|null|undefined} value - * @api private + * Check if the given value satisfies the `required` validator. The value is + * considered valid if it is a string (that is, not `null` or `undefined`) and + * has positive length. The `required` validator **will** fail for empty + * strings. + * + * @param {Any} value + * @param {Document} doc + * @return {Boolean} + * @api public */ SchemaString.prototype.checkRequired = function checkRequired(value, doc) { if (SchemaType._isRef(this, value, doc, true)) { - return null != value; - } else { - return (value instanceof String || typeof value == 'string') && value.length; + return !!value; } + return (value instanceof String || typeof value === 'string') && value.length; }; /** @@ -382,7 +424,7 @@ SchemaString.prototype.cast = function(value, doc, init) { if (SchemaType._isRef(this, value, doc, init)) { // wait! we may need to cast this to a document - if (null == value) { + if (value === null || value === undefined) { return value; } @@ -395,7 +437,7 @@ SchemaString.prototype.cast = function(value, doc, init) { } // setting a populated path - if ('string' == typeof value) { + if (typeof value === 'string') { return value; } else if (Buffer.isBuffer(value) || !utils.isObject(value)) { throw new CastError('string', value, this.path); @@ -413,13 +455,13 @@ SchemaString.prototype.cast = function(value, doc, init) { } // If null or undefined - if (value == null) { + if (value === null || value === undefined) { return value; } - if ('undefined' !== typeof value) { + if (typeof value !== 'undefined') { // handle documents being passed - if (value._id && 'string' == typeof value._id) { + if (value._id && typeof value._id === 'string') { return value._id; } @@ -443,25 +485,26 @@ function handleSingle(val) { } function handleArray(val) { - var self = this; + var _this = this; if (!Array.isArray(val)) { return [this.castForQuery(val)]; } return val.map(function(m) { - return self.castForQuery(m); + return _this.castForQuery(m); }); } SchemaString.prototype.$conditionalHandlers = - utils.options(SchemaType.prototype.$conditionalHandlers, { - '$all': handleArray, - '$gt' : handleSingle, - '$gte': handleSingle, - '$lt' : handleSingle, - '$lte': handleSingle, - '$options': handleSingle, - '$regex': handleSingle - }); + utils.options(SchemaType.prototype.$conditionalHandlers, { + $all: handleArray, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle, + $options: handleSingle, + $regex: handleSingle, + $not: handleSingle + }); /** * Casts contents for queries. @@ -475,14 +518,17 @@ SchemaString.prototype.castForQuery = function($conditional, val) { var handler; if (arguments.length === 2) { handler = this.$conditionalHandlers[$conditional]; - if (!handler) - throw new Error("Can't use " + $conditional + " with String."); + if (!handler) { + throw new Error('Can\'t use ' + $conditional + ' with String.'); + } return handler.call(this, val); - } else { - val = $conditional; - if (val instanceof RegExp) return val; - return this.cast(val); } + val = $conditional; + if (Object.prototype.toString.call(val) === '[object RegExp]') { + return val; + } + + return this._castForQuery(val); }; /*! diff --git a/lib/schematype.js b/lib/schematype.js index 0cfe08b1ec6..96a38579874 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -2,11 +2,12 @@ * Module dependencies. */ +var $exists = require('./schema/operators/exists'); +var $type = require('./schema/operators/type'); var utils = require('./utils'); -var error = require('./error'); -var errorMessages = error.messages; -var CastError = error.CastError; -var ValidatorError = error.ValidatorError; +var MongooseError = require('./error'); +var CastError = MongooseError.CastError; +var ValidatorError = MongooseError.ValidatorError; /** * SchemaType constructor @@ -27,18 +28,32 @@ function SchemaType(path, options, instance) { this._index = null; this.selected; - for (var i in options) { - if (this[i] && 'function' == typeof this[i]) { + for (var prop in options) { + if (this[prop] && typeof this[prop] === 'function') { // { unique: true, index: true } - if ('index' == i && this._index) continue; + if (prop === 'index' && this._index) { + continue; + } + + var val = options[prop]; + // Special case so we don't screw up array defaults, see gh-5780 + if (prop === 'default') { + this.default(val); + continue; + } - var opts = Array.isArray(options[i]) - ? options[i] - : [options[i]]; + var opts = Array.isArray(val) ? val : [val]; - this[i].apply(this, opts); + this[prop].apply(this, opts); } } + + Object.defineProperty(this, '$$context', { + enumerable: false, + configurable: false, + writable: true, + value: null + }); } /** @@ -85,11 +100,13 @@ function SchemaType(path, options, instance) { */ SchemaType.prototype.default = function(val) { - if (1 === arguments.length) { - this.defaultValue = typeof val === 'function' - ? val - : this.cast(val); - return this; + if (arguments.length === 1) { + if (val === void 0) { + this.defaultValue = void 0; + return void 0; + } + this.defaultValue = val; + return this.defaultValue; } else if (arguments.length > 1) { this.defaultValue = utils.args(arguments); } @@ -143,10 +160,17 @@ SchemaType.prototype.index = function(options) { */ SchemaType.prototype.unique = function(bool) { - if (null == this._index || 'boolean' == typeof this._index) { + if (this._index === false) { + if (!bool) { + return; + } + throw new Error('Path "' + this.path + '" may not have `index` set to ' + + 'false and `unique` set to true'); + } + if (this._index == null || this._index === true) { this._index = {}; - } else if ('string' == typeof this._index) { - this._index = { type: this._index }; + } else if (typeof this._index === 'string') { + this._index = {type: this._index}; } this._index.unique = bool; @@ -160,16 +184,17 @@ SchemaType.prototype.unique = function(bool) { * * var s = new Schema({name : {type: String, text : true }) * Schema.path('name').index({text : true}); - * @param bool + * @param {Boolean} bool * @return {SchemaType} this * @api public */ SchemaType.prototype.text = function(bool) { - if (null == this._index || 'boolean' == typeof this._index) { + if (this._index === null || this._index === undefined || + typeof this._index === 'boolean') { this._index = {}; - } else if ('string' == typeof this._index) { - this._index = { type: this._index }; + } else if (typeof this._index === 'string') { + this._index = {type: this._index}; } this._index.text = bool; @@ -190,10 +215,11 @@ SchemaType.prototype.text = function(bool) { */ SchemaType.prototype.sparse = function(bool) { - if (null == this._index || 'boolean' == typeof this._index) { + if (this._index === null || this._index === undefined || + typeof this._index === 'boolean') { this._index = {}; - } else if ('string' == typeof this._index) { - this._index = { type: this._index }; + } else if (typeof this._index === 'string') { + this._index = {type: this._index}; } this._index.sparse = bool; @@ -206,7 +232,7 @@ SchemaType.prototype.sparse = function(bool) { * ####Example: * * function capitalize (val) { - * if ('string' != typeof val) val = ''; + * if (typeof val !== 'string') val = ''; * return val.charAt(0).toUpperCase() + val.substring(1); * } * @@ -223,25 +249,37 @@ SchemaType.prototype.sparse = function(bool) { * * You can set up email lower case normalization easily via a Mongoose setter. * - * function toLower (v) { + * function toLower(v) { * return v.toLowerCase(); * } * * var UserSchema = new Schema({ * email: { type: String, set: toLower } - * }) + * }); * - * var User = db.model('User', UserSchema) + * var User = db.model('User', UserSchema); * - * var user = new User({email: 'AVENUE@Q.COM'}) + * var user = new User({email: 'AVENUE@Q.COM'}); * console.log(user.email); // 'avenue@q.com' * * // or - * var user = new User - * user.email = 'Avenue@Q.com' - * console.log(user.email) // 'avenue@q.com' + * var user = new User(); + * user.email = 'Avenue@Q.com'; + * console.log(user.email); // 'avenue@q.com' + * + * As you can see above, setters allow you to transform the data before it stored in MongoDB. + * + * NOTE: setters by default do **not** run on queries by default. * - * As you can see above, setters allow you to transform the data before it gets to the raw mongodb document and is set as a value on an actual key. + * // Will **not** run the `toLower()` setter by default. + * User.updateOne({ _id: _id }, { $set: { email: 'AVENUE@Q.COM' } }); + * + * Use the `runSettersOnQuery` option to opt-in to running setters on `User.update()`: + * + * // Turn on `runSettersOnQuery` to run the setters from your schema. + * User.updateOne({ _id: _id }, { $set: { email: 'AVENUE@Q.COM' } }, { + * runSettersOnQuery: true + * }); * * _NOTE: we could have also just used the built-in `lowercase: true` SchemaType option instead of defining our own function._ * @@ -274,8 +312,9 @@ SchemaType.prototype.sparse = function(bool) { */ SchemaType.prototype.set = function(fn) { - if ('function' != typeof fn) + if (typeof fn !== 'function') { throw new TypeError('A setter must be a function.'); + } this.setters.push(fn); return this; }; @@ -343,8 +382,9 @@ SchemaType.prototype.set = function(fn) { */ SchemaType.prototype.get = function(fn) { - if ('function' != typeof fn) + if (typeof fn !== 'function') { throw new TypeError('A getter must be a function.'); + } this.getters.push(fn); return this; }; @@ -384,27 +424,31 @@ SchemaType.prototype.get = function(fn) { * * ####Error message templates: * - * From the examples above, you may have noticed that error messages support baseic templating. There are a few other template keywords besides `{PATH}` and `{VALUE}` too. To find out more, details are available [here](#error_messages_MongooseError-messages) + * From the examples above, you may have noticed that error messages support basic templating. There are a few other template keywords besides `{PATH}` and `{VALUE}` too. To find out more, details are available [here](#error_messages_MongooseError.messages) * * ####Asynchronous validation: * * Passing a validator function that receives two arguments tells mongoose that the validator is an asynchronous validator. The first argument passed to the validator function is the value being validated. The second argument is a callback function that must called when you finish validating the value and passed either `true` or `false` to communicate either success or failure respectively. * - * schema.path('name').validate(function (value, respond) { - * doStuff(value, function () { - * ... - * respond(false); // validation failed - * }) - * }, '{PATH} failed validation.'); - * - * // or with dynamic message + * schema.path('name').validate({ + * isAsync: true, + * validator: function (value, respond) { + * doStuff(value, function () { + * ... + * respond(false); // validation failed + * }); + * }, + * message: 'Custom error message!' // Optional + * }); * - * schema.path('name').validate(function (value, respond) { - * doStuff(value, function () { - * ... - * respond(false, 'this message gets to the validation error'); - * }); - * }, 'this message does not matter'); + * // Can also return a promise + * schema.path('name').validate({ + * validator: function (value) { + * return new Promise(function (resolve, reject) { + * resolve(false); // validation failed + * }); + * } + * }); * * You might use asynchronous validators to retreive other documents from the database to validate against or to meet other I/O bound validation needs. * @@ -432,7 +476,7 @@ SchemaType.prototype.get = function(fn) { */ SchemaType.prototype.validate = function(obj, message, type) { - if ('function' == typeof obj || obj && 'RegExp' === utils.getFunctionName(obj.constructor)) { + if (typeof obj === 'function' || obj && utils.getFunctionName(obj.constructor) === 'RegExp') { var properties; if (message instanceof Object && !type) { properties = utils.clone(message); @@ -442,9 +486,13 @@ SchemaType.prototype.validate = function(obj, message, type) { properties.validator = obj; properties.type = properties.type || 'user defined'; } else { - if (!message) message = errorMessages.general.default; - if (!type) type = 'user defined'; - properties = { message: message, type: type, validator: obj }; + if (!message) { + message = MongooseError.messages.general.default; + } + if (!type) { + type = 'user defined'; + } + properties = {message: message, type: type, validator: obj}; } this.validators.push(properties); return this; @@ -456,10 +504,10 @@ SchemaType.prototype.validate = function(obj, message, type) { for (i = 0, length = arguments.length; i < length; i++) { arg = arguments[i]; - if (!(arg && 'Object' === utils.getFunctionName(arg.constructor))) { + if (!(arg && utils.getFunctionName(arg.constructor) === 'Object')) { var msg = 'Invalid validator. Received (' + typeof arg + ') ' - + arg - + '. See http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate'; + + arg + + '. See http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate'; throw new Error(msg); } @@ -470,8 +518,8 @@ SchemaType.prototype.validate = function(obj, message, type) { }; /** - * Adds a required validator to this schematype. The required validator is added - * to the front of the validators array using `unshift()`. + * Adds a required validator to this SchemaType. The validator gets added + * to the front of this SchemaType's validators array using `unshift()`. * * ####Example: * @@ -481,6 +529,28 @@ SchemaType.prototype.validate = function(obj, message, type) { * * var s = new Schema({ born: { type: Date, required: '{PATH} is required!' }) * + * // or with a function + * + * var s = new Schema({ + * userId: ObjectId, + * username: { + * type: String, + * required: function() { return this.userId != null; } + * } + * }) + * + * // or with a function and a custom message + * var s = new Schema({ + * userId: ObjectId, + * username: { + * type: String, + * required: [ + * function() { return this.userId != null; }, + * 'username is required if id is specified' + * ] + * } + * }) + * * // or through the path API * * Schema.path('name').required(true); @@ -489,49 +559,74 @@ SchemaType.prototype.validate = function(obj, message, type) { * * Schema.path('name').required(true, 'grrr :( '); * + * // or make a path conditionally required based on a function + * var isOver18 = function() { return this.age >= 18; }; + * Schema.path('voterRegistrationId').required(isOver18); * - * @param {Boolean} required enable/disable the validator + * The required validator uses the SchemaType's `checkRequired` function to + * determine whether a given value satisfies the required validator. By default, + * a value satisfies the required validator if `val != null` (that is, if + * the value is not null nor undefined). However, most built-in mongoose schema + * types override the default `checkRequired` function: + * + * @param {Boolean|Function|Object} required enable/disable the validator, or function that returns required boolean, or options object + * @param {Boolean|Function} [options.isRequired] enable/disable the validator, or function that returns required boolean + * @param {Function} [options.ErrorConstructor] custom error constructor. The constructor receives 1 parameter, an object containing the validator properties. * @param {String} [message] optional custom error message * @return {SchemaType} this * @see Customized Error Messages #error_messages_MongooseError-messages + * @see SchemaArray#checkRequired #schema_array_SchemaArray.checkRequired + * @see SchemaBoolean#checkRequired #schema_boolean_SchemaBoolean-checkRequired + * @see SchemaBuffer#checkRequired #schema_buffer_SchemaBuffer.schemaName + * @see SchemaNumber#checkRequired #schema_number_SchemaNumber-min + * @see SchemaObjectId#checkRequired #schema_objectid_ObjectId-auto + * @see SchemaString#checkRequired #schema_string_SchemaString-checkRequired * @api public */ SchemaType.prototype.required = function(required, message) { - if (false === required) { + var customOptions = {}; + if (typeof required === 'object') { + customOptions = required; + message = customOptions.message || message; + required = required.isRequired; + } + + if (required === false) { this.validators = this.validators.filter(function(v) { - return v.validator != this.requiredValidator; + return v.validator !== this.requiredValidator; }, this); this.isRequired = false; return this; } - var self = this; + var _this = this; this.isRequired = true; this.requiredValidator = function(v) { // in here, `this` refers to the validating document. // no validation when this path wasn't selected in the query. - if ('isSelected' in this && - !this.isSelected(self.path) && - !this.isModified(self.path)) return true; + if ('isSelected' in this && !this.isSelected(_this.path) && !this.isModified(_this.path)) { + return true; + } - return (('function' === typeof required) && !required.apply(this)) || - self.checkRequired(v, this); + return ((typeof required === 'function') && !required.apply(this)) || + _this.checkRequired(v, this); }; + this.originalRequiredValue = required; - if ('string' == typeof required) { + if (typeof required === 'string') { message = required; required = undefined; } - var msg = message || errorMessages.general.required; - this.validators.unshift({ + var msg = message || MongooseError.messages.general.required; + this.validators.unshift(utils.assign({}, customOptions, { validator: this.requiredValidator, message: msg, type: 'required' - }); + })); return this; }; @@ -545,31 +640,35 @@ SchemaType.prototype.required = function(required, message) { */ SchemaType.prototype.getDefault = function(scope, init) { - var ret = 'function' === typeof this.defaultValue - ? this.defaultValue.call(scope) - : this.defaultValue; - - if (null !== ret && undefined !== ret) { - return this.cast(ret, scope, init); - } else { - return ret; + var ret = typeof this.defaultValue === 'function' + ? this.defaultValue.call(scope) + : this.defaultValue; + + if (ret !== null && ret !== undefined) { + if (typeof ret === 'object' && (!this.options || !this.options.shared)) { + ret = utils.clone(ret, { retainKeyOrder: true }); + } + + var casted = this.cast(ret, scope, init); + if (casted && casted.$isSingleNested) { + casted.$parent = scope; + } + return casted; } + return ret; }; -/** - * Applies setters +/*! + * Applies setters without casting * - * @param {Object} value - * @param {Object} scope - * @param {Boolean} init * @api private */ -SchemaType.prototype.applySetters = function(value, scope, init, priorVal) { - var v = value, - setters = this.setters, - len = setters.length, - caster = this.caster; +SchemaType.prototype._applySetters = function(value, scope, init, priorVal) { + var v = value; + var setters = this.setters; + var len = setters.length; + var caster = this.caster; while (len--) { v = setters[len].call(scope, v, this); @@ -583,10 +682,27 @@ SchemaType.prototype.applySetters = function(value, scope, init, priorVal) { v = newVal; } - if (null === v || undefined === v) return v; + return v; +}; + +/** + * Applies setters + * + * @param {Object} value + * @param {Object} scope + * @param {Boolean} init + * @api private + */ + +SchemaType.prototype.applySetters = function(value, scope, init, priorVal, options) { + var v = this._applySetters(value, scope, init, priorVal, options); + + if (v == null) { + return v; + } // do not cast until all setters are applied #665 - v = this.cast(v, scope, init, priorVal); + v = this.cast(v, scope, init, priorVal, options); return v; }; @@ -647,29 +763,36 @@ SchemaType.prototype.select = function select(val) { */ SchemaType.prototype.doValidate = function(value, fn, scope) { - var err = false, - path = this.path, - count = this.validators.length; + var err = false; + var path = this.path; + var count = this.validators.length; - if (!count) return fn(null); + if (!count) { + return fn(null); + } var validate = function(ok, validatorProperties) { - if (err) return; + if (err) { + return; + } if (ok === undefined || ok) { --count || fn(null); } else { - err = new ValidatorError(validatorProperties); + var ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError; + err = new ErrorConstructor(validatorProperties); + err.$isValidatorError = true; fn(err); } }; - var self = this; + var _this = this; this.validators.forEach(function(v) { if (err) { return; } var validator = v.validator; + var ok; var validatorProperties = utils.clone(v); validatorProperties.path = path; @@ -677,25 +800,89 @@ SchemaType.prototype.doValidate = function(value, fn, scope) { if (validator instanceof RegExp) { validate(validator.test(value), validatorProperties); - } else if ('function' === typeof validator) { - if (value === undefined && !self.isRequired) { + } else if (typeof validator === 'function') { + if (value === undefined && validator !== _this.requiredValidator) { validate(true, validatorProperties); return; } - if (2 === validator.length) { - validator.call(scope, value, function(ok, customMsg) { - if (customMsg) { - validatorProperties.message = customMsg; - } - validate(ok, validatorProperties); - }); + if (validatorProperties.isAsync) { + asyncValidate(validator, scope, value, validatorProperties, validate); + } else if (validator.length === 2 && !('isAsync' in validatorProperties)) { + legacyAsyncValidate(validator, scope, value, validatorProperties, + validate); } else { - validate(validator.call(scope, value), validatorProperties); + try { + ok = validator.call(scope, value); + } catch (error) { + ok = false; + validatorProperties.reason = error; + } + if (ok && typeof ok.then === 'function') { + ok.then( + function(ok) { validate(ok, validatorProperties); }, + function(error) { + validatorProperties.reason = error; + ok = false; + validate(ok, validatorProperties); + }); + } else { + validate(ok, validatorProperties); + } } } }); }; +/*! + * Handle async validators + */ + +function asyncValidate(validator, scope, value, props, cb) { + var called = false; + var returnVal = validator.call(scope, value, function(ok, customMsg) { + if (called) { + return; + } + called = true; + if (typeof returnVal === 'boolean') { + return; + } + if (customMsg) { + props.message = customMsg; + } + cb(ok, props); + }); + if (typeof returnVal === 'boolean') { + called = true; + cb(returnVal, props); + } else if (returnVal && typeof returnVal.then === 'function') { + // Promise + returnVal.then( + function(ok) { + if (called) { + return; + } + called = true; + cb(ok, props); + }, + function(error) { + if (called) { + return; + } + called = true; + + props.reason = error; + cb(false, props); + }); + } +} + +var legacyAsyncValidate = require('util').deprecate(asyncValidate, + 'Implicit async custom validators (custom validators that take 2 ' + + 'arguments) are deprecated in mongoose >= 4.9.0. See ' + + 'http://mongoosejs.com/docs/validation.html#async-custom-validators for ' + + 'more info.'); + /** * Performs a validation of `value` using the validators declared for this SchemaType. * @@ -714,21 +901,31 @@ SchemaType.prototype.doValidateSync = function(value, scope) { path = this.path, count = this.validators.length; - if (!count) return null; + if (!count) { + return null; + } var validate = function(ok, validatorProperties) { - if (err) return; + if (err) { + return; + } if (ok !== undefined && !ok) { - err = new ValidatorError(validatorProperties); + var ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError; + err = new ErrorConstructor(validatorProperties); + err.$isValidatorError = true; } }; - var self = this; - if (value === undefined && !self.isRequired) { - return null; + var validators = this.validators; + if (value === void 0) { + if (this.validators.length > 0 && this.validators[0].type === 'required') { + validators = [this.validators[0]]; + } else { + return null; + } } - this.validators.forEach(function(v) { + validators.forEach(function(v) { if (err) { return; } @@ -737,13 +934,20 @@ SchemaType.prototype.doValidateSync = function(value, scope) { var validatorProperties = utils.clone(v); validatorProperties.path = path; validatorProperties.value = value; + var ok; if (validator instanceof RegExp) { validate(validator.test(value), validatorProperties); - } else if ('function' === typeof validator) { + } else if (typeof validator === 'function') { // if not async validators - if (2 !== validator.length) { - validate(validator.call(scope, value), validatorProperties); + if (validator.length !== 2 && !validatorProperties.isAsync) { + try { + ok = validator.call(scope, value); + } catch (error) { + ok = false; + validatorProperties.reason = error; + } + validate(ok, validatorProperties); } } }); @@ -776,11 +980,13 @@ SchemaType._isRef = function(self, value, doc, init) { } if (ref) { - if (null == value) return true; + if (value == null) { + return true; + } if (!Buffer.isBuffer(value) && // buffers are objects too - 'Binary' != value._bsontype // raw binary value from the db + value._bsontype !== 'Binary' // raw binary value from the db && utils.isObject(value) // might have deselected _id in population query - ) { + ) { return true; } } @@ -815,11 +1021,28 @@ function handleArray(val) { */ SchemaType.prototype.$conditionalHandlers = { - '$all': handleArray, - '$eq': handleSingle, - '$in' : handleArray, - '$ne' : handleSingle, - '$nin': handleArray + $all: handleArray, + $eq: handleSingle, + $in: handleArray, + $ne: handleSingle, + $nin: handleArray, + $exists: $exists, + $type: $type +}; + +/*! + * Wraps `castForQuery` to handle context + */ + +SchemaType.prototype.castForQueryWrapper = function(params) { + this.$$context = params.context; + if ('$conditional' in params) { + return this.castForQuery(params.$conditional, params.val); + } + if (params.$skipQueryCastForUpdate) { + return this._castForQuery(params.val); + } + return this.castForQuery(params.val); }; /** @@ -838,10 +1061,42 @@ SchemaType.prototype.castForQuery = function($conditional, val) { throw new Error('Can\'t use ' + $conditional); } return handler.call(this, val); - } else { - val = $conditional; - return this.cast(val); } + val = $conditional; + return this._castForQuery(val); +}; + +/*! + * Internal switch for runSetters + * + * @api private + */ + +SchemaType.prototype._castForQuery = function(val) { + var runSettersOnQuery = false; + if (this.$$context && + this.$$context.options && + 'runSettersOnQuery' in this.$$context.options) { + runSettersOnQuery = this.$$context.options.runSettersOnQuery; + } else if (this.options && 'runSettersOnQuery' in this.options) { + runSettersOnQuery = this.options.runSettersOnQuery; + } + + if (runSettersOnQuery) { + return this.applySetters(val, this.$$context); + } + return this.cast(val); +}; + +/** + * Default check for if this path satisfies the `required` validator. + * + * @param {any} val + * @api private + */ + +SchemaType.prototype.checkRequired = function(val) { + return val != null; }; /*! diff --git a/lib/services/common.js b/lib/services/common.js new file mode 100644 index 00000000000..066c5a9855d --- /dev/null +++ b/lib/services/common.js @@ -0,0 +1,87 @@ +'use strict'; + +/*! + * Module dependencies. + */ + +var ObjectId = require('../types/objectid'); +var utils = require('../utils'); + +exports.flatten = flatten; +exports.modifiedPaths = modifiedPaths; + +/*! + * ignore + */ + +function flatten(update, path, options) { + var keys; + if (update && utils.isMongooseObject(update) && !Buffer.isBuffer(update)) { + keys = Object.keys(update.toObject({ transform: false, virtuals: false })); + } else { + keys = Object.keys(update || {}); + } + + var numKeys = keys.length; + var result = {}; + path = path ? path + '.' : ''; + + for (var i = 0; i < numKeys; ++i) { + var key = keys[i]; + var val = update[key]; + result[path + key] = val; + if (shouldFlatten(val)) { + if (options && options.skipArrays && Array.isArray(val)) { + continue; + } + var flat = flatten(val, path + key, options); + for (var k in flat) { + result[k] = flat[k]; + } + if (Array.isArray(val)) { + result[path + key] = val; + } + } + } + + return result; +} + +/*! + * ignore + */ + +function modifiedPaths(update, path, result) { + var keys = Object.keys(update || {}); + var numKeys = keys.length; + result = result || {}; + path = path ? path + '.' : ''; + + for (var i = 0; i < numKeys; ++i) { + var key = keys[i]; + var val = update[key]; + + result[path + key] = true; + if (utils.isMongooseObject(val) && !Buffer.isBuffer(val)) { + val = val.toObject({ transform: false, virtuals: false }); + } + if (shouldFlatten(val)) { + modifiedPaths(val, path + key, result); + } + } + + return result; +} + +/*! + * ignore + */ + +function shouldFlatten(val) { + return val && + typeof val === 'object' && + !(val instanceof Date) && + !(val instanceof ObjectId) && + (!Array.isArray(val) || val.length > 0) && + !(val instanceof Buffer); +} diff --git a/lib/services/cursor/eachAsync.js b/lib/services/cursor/eachAsync.js new file mode 100644 index 00000000000..251fe9c8252 --- /dev/null +++ b/lib/services/cursor/eachAsync.js @@ -0,0 +1,79 @@ +'use strict'; + +/*! + * Module dependencies. + */ + +var PromiseProvider = require('../../promise_provider'); +var async = require('async'); + +/** + * Execute `fn` for every document in the cursor. If `fn` returns a promise, + * will wait for the promise to resolve before iterating on to the next one. + * Returns a promise that resolves when done. + * + * @param {Function} next the thunk to call to get the next document + * @param {Function} fn + * @param {Object} options + * @param {Function} [callback] executed when all docs have been processed + * @return {Promise} + * @api public + * @method eachAsync + */ + +module.exports = function eachAsync(next, fn, options, callback) { + var Promise = PromiseProvider.get(); + var parallel = options.parallel || 1; + + var handleNextResult = function(doc, callback) { + var promise = fn(doc); + if (promise && typeof promise.then === 'function') { + promise.then( + function() { callback(null); }, + function(error) { callback(error || new Error('`eachAsync()` promise rejected without error')); }); + } else { + callback(null); + } + }; + + var iterate = function(callback) { + var drained = false; + var nextQueue = async.queue(function(task, cb) { + if (drained) return cb(); + next(function(err, doc) { + if (err) return cb(err); + if (!doc) drained = true; + cb(null, doc); + }); + }, 1); + + var getAndRun = function(cb) { + nextQueue.push({}, function(err, doc) { + if (err) return cb(err); + if (!doc) return cb(); + handleNextResult(doc, function(err) { + if (err) return cb(err); + // Make sure to clear the stack re: gh-4697 + setTimeout(function() { + getAndRun(cb); + }, 0); + }); + }); + }; + + async.times(parallel, function(n, cb) { + getAndRun(cb); + }, callback); + }; + + return new Promise.ES6(function(resolve, reject) { + iterate(function(error) { + if (error) { + callback && callback(error); + return reject(error); + } + callback && callback(null); + return resolve(); + }); + }); +}; diff --git a/lib/services/document/cleanModifiedSubpaths.js b/lib/services/document/cleanModifiedSubpaths.js new file mode 100644 index 00000000000..ec34eb97ddc --- /dev/null +++ b/lib/services/document/cleanModifiedSubpaths.js @@ -0,0 +1,18 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function cleanModifiedSubpaths(doc, path) { + var _modifiedPaths = Object.keys(doc.$__.activePaths.states.modify); + var _numModifiedPaths = _modifiedPaths.length; + var deleted = 0; + for (var j = 0; j < _numModifiedPaths; ++j) { + if (_modifiedPaths[j].indexOf(path + '.') === 0) { + delete doc.$__.activePaths.states.modify[_modifiedPaths[j]]; + ++deleted; + } + } + return deleted; +}; diff --git a/lib/services/document/compile.js b/lib/services/document/compile.js new file mode 100644 index 00000000000..be12f5f647f --- /dev/null +++ b/lib/services/document/compile.js @@ -0,0 +1,164 @@ +'use strict'; + +var Document; +var utils = require('../../utils'); + +/*! + * exports + */ + +exports.compile = compile; +exports.defineKey = defineKey; + +/*! + * Compiles schemas. + */ + +function compile(tree, proto, prefix, options) { + Document = Document || require('../../document'); + var keys = Object.keys(tree); + var i = keys.length; + var len = keys.length; + var limb; + var key; + + if (options.retainKeyOrder) { + for (i = 0; i < len; ++i) { + key = keys[i]; + limb = tree[key]; + + defineKey(key, + ((utils.getFunctionName(limb.constructor) === 'Object' + && Object.keys(limb).length) + && (!limb[options.typeKey] || (options.typeKey === 'type' && limb.type.type)) + ? limb + : null) + , proto + , prefix + , keys + , options); + } + } else { + while (i--) { + key = keys[i]; + limb = tree[key]; + + defineKey(key, + ((utils.getFunctionName(limb.constructor) === 'Object' + && Object.keys(limb).length) + && (!limb[options.typeKey] || (options.typeKey === 'type' && limb.type.type)) + ? limb + : null) + , proto + , prefix + , keys + , options); + } + } +} + +/*! + * Defines the accessor named prop on the incoming prototype. + */ + +function defineKey(prop, subprops, prototype, prefix, keys, options) { + Document = Document || require('../../document'); + var path = (prefix ? prefix + '.' : '') + prop; + prefix = prefix || ''; + + if (subprops) { + Object.defineProperty(prototype, prop, { + enumerable: true, + configurable: true, + get: function() { + var _this = this; + if (!this.$__.getters) { + this.$__.getters = {}; + } + + if (!this.$__.getters[path]) { + var nested = Object.create(Document.prototype, getOwnPropertyDescriptors(this)); + + // save scope for nested getters/setters + if (!prefix) { + nested.$__.scope = this; + } + nested.$__.nestedPath = path; + + Object.defineProperty(nested, 'schema', { + enumerable: false, + configurable: true, + writable: false, + value: prototype.schema + }); + + Object.defineProperty(nested, 'toObject', { + enumerable: false, + configurable: true, + writable: false, + value: function() { + return utils.clone(_this.get(path), { retainKeyOrder: true }); + } + }); + + Object.defineProperty(nested, 'toJSON', { + enumerable: false, + configurable: true, + writable: false, + value: function() { + return _this.get(path); + } + }); + + Object.defineProperty(nested, '$__isNested', { + enumerable: false, + configurable: true, + writable: false, + value: true + }); + + compile(subprops, nested, path, options); + this.$__.getters[path] = nested; + } + + return this.$__.getters[path]; + }, + set: function(v) { + if (v instanceof Document) { + v = v.toObject({ transform: false }); + } + var doc = this.$__.scope || this; + return doc.$set(path, v); + } + }); + } else { + Object.defineProperty(prototype, prop, { + enumerable: true, + configurable: true, + get: function() { + return this.get.call(this.$__.scope || this, path); + }, + set: function(v) { + return this.$set.call(this.$__.scope || this, path, v); + } + }); + } +} + +// gets descriptors for all properties of `object` +// makes all properties non-enumerable to match previous behavior to #2211 +function getOwnPropertyDescriptors(object) { + var result = {}; + + Object.getOwnPropertyNames(object).forEach(function(key) { + result[key] = Object.getOwnPropertyDescriptor(object, key); + // Assume these are schema paths, ignore them re: #5470 + if (result[key].get) { + delete result[key]; + return; + } + result[key].enumerable = ['isNew', '$__', 'errors', '_doc'].indexOf(key) === -1; + }); + + return result; +} diff --git a/lib/services/model/applyHooks.js b/lib/services/model/applyHooks.js new file mode 100644 index 00000000000..eb85bce4657 --- /dev/null +++ b/lib/services/model/applyHooks.js @@ -0,0 +1,200 @@ +'use strict'; + +var PromiseProvider = require('../../promise_provider'); +var VersionError = require('../../error').VersionError; + +module.exports = applyHooks; + +/*! + * Register hooks for this model + * + * @param {Model} model + * @param {Schema} schema + */ + +function applyHooks(model, schema) { + var q = schema && schema.callQueue; + var toWrapEl; + var len; + var i; + var j; + var pointCut; + var keys; + var newName; + + model.$appliedHooks = true; + for (i = 0; i < schema.childSchemas.length; ++i) { + var childModel = schema.childSchemas[i].model; + if (childModel.$appliedHooks) { + continue; + } + applyHooks(childModel, schema.childSchemas[i].schema); + if (childModel.discriminators != null) { + keys = Object.keys(childModel.discriminators); + for (j = 0; j < keys.length; ++j) { + applyHooks(childModel.discriminators[keys[j]], + childModel.discriminators[keys[j]].schema); + } + } + } + + if (!q.length) { + return; + } + + // we are only interested in 'pre' hooks, and group by point-cut + var toWrap = { post: [] }; + var pair; + + for (i = 0; i < q.length; ++i) { + pair = q[i]; + if (pair[0] !== 'pre' && pair[0] !== 'post' && pair[0] !== 'on') { + continue; + } + var args = [].slice.call(pair[1]); + pointCut = pair[0] === 'on' ? 'post' : args[0]; + if (!(pointCut in toWrap)) { + toWrap[pointCut] = {post: [], pre: []}; + } + if (pair[0] === 'post') { + toWrap[pointCut].post.push(args); + } else if (pair[0] === 'on') { + toWrap[pointCut].push(args); + } else { + toWrap[pointCut].pre.push(args); + } + } + + // 'post' hooks are simpler + len = toWrap.post.length; + toWrap.post.forEach(function(args) { + model.on.apply(model, args); + }); + delete toWrap.post; + + // 'init' should be synchronous on subdocuments + if (toWrap.init && (model.$isSingleNested || model.$isArraySubdocument)) { + if (toWrap.init.pre) { + toWrap.init.pre.forEach(function(args) { + model.prototype.$pre.apply(model.prototype, args); + }); + } + if (toWrap.init.post) { + toWrap.init.post.forEach(function(args) { + model.prototype.$post.apply(model.prototype, args); + }); + } + delete toWrap.init; + } + if (toWrap.set) { + // Set hooks also need to be sync re: gh-3479 + newName = '$__original_set'; + model.prototype[newName] = model.prototype.set; + if (toWrap.set.pre) { + toWrap.set.pre.forEach(function(args) { + model.prototype.$pre.apply(model.prototype, args); + }); + } + if (toWrap.set.post) { + toWrap.set.post.forEach(function(args) { + model.prototype.$post.apply(model.prototype, args); + }); + } + delete toWrap.set; + } + + toWrap.validate = toWrap.validate || { pre: [], post: [] }; + + keys = Object.keys(toWrap); + len = keys.length; + for (i = 0; i < len; ++i) { + pointCut = keys[i]; + // this is so we can wrap everything into a promise; + newName = ('$__original_' + pointCut); + if (!model.prototype[pointCut]) { + continue; + } + if (model.prototype[pointCut].$isWrapped) { + continue; + } + if (!model.prototype[pointCut].$originalFunction) { + model.prototype[newName] = model.prototype[pointCut]; + } + model.prototype[pointCut] = (function(_newName) { + return function wrappedPointCut() { + var Promise = PromiseProvider.get(); + + var _this = this; + var args = [].slice.call(arguments); + var lastArg = args.pop(); + var fn; + var originalError = new Error(); + var $results; + if (lastArg && typeof lastArg !== 'function') { + args.push(lastArg); + } else { + fn = lastArg; + } + + var promise = new Promise.ES6(function(resolve, reject) { + args.push(function(error) { + if (error) { + // gh-2633: since VersionError is very generic, take the + // stack trace of the original save() function call rather + // than the async trace + if (error instanceof VersionError) { + error.stack = originalError.stack; + } + if (!fn) { + _this.$__handleReject(error); + } + reject(error); + return; + } + + // There may be multiple results and promise libs other than + // mpromise don't support passing multiple values to `resolve()` + $results = Array.prototype.slice.call(arguments, 1); + resolve.apply(promise, $results); + }); + + _this[_newName].apply(_this, args); + }); + if (fn) { + if (this.constructor.$wrapCallback) { + fn = this.constructor.$wrapCallback(fn); + } + promise.then( + function() { + process.nextTick(function() { + fn.apply(null, [null].concat($results)); + }); + }, + function(error) { + process.nextTick(function() { + fn(error); + }); + }); + } + return promise; + }; + })(newName); + model.prototype[pointCut].$originalFunction = newName; + model.prototype[pointCut].$isWrapped = true; + + toWrapEl = toWrap[pointCut]; + var _len = toWrapEl.pre.length; + for (j = 0; j < _len; ++j) { + args = toWrapEl.pre[j]; + args[0] = newName; + model.prototype.$pre.apply(model.prototype, args); + } + + _len = toWrapEl.post.length; + for (j = 0; j < _len; ++j) { + args = toWrapEl.post[j]; + args[0] = newName; + model.prototype.$post.apply(model.prototype, args); + } + } +} diff --git a/lib/services/model/applyMethods.js b/lib/services/model/applyMethods.js new file mode 100644 index 00000000000..73e51f1cb3b --- /dev/null +++ b/lib/services/model/applyMethods.js @@ -0,0 +1,43 @@ +'use strict'; + +/*! + * Register methods for this model + * + * @param {Model} model + * @param {Schema} schema + */ + +module.exports = function applyMethods(model, schema) { + function apply(method, schema) { + Object.defineProperty(model.prototype, method, { + get: function() { + var h = {}; + for (var k in schema.methods[method]) { + h[k] = schema.methods[method][k].bind(this); + } + return h; + }, + configurable: true + }); + } + for (var method in schema.methods) { + if (schema.tree.hasOwnProperty(method)) { + throw new Error('You have a method and a property in your schema both ' + + 'named "' + method + '"'); + } + if (typeof schema.methods[method] === 'function') { + model.prototype[method] = schema.methods[method]; + } else { + apply(method, schema); + } + } + + // Recursively call `applyMethods()` on child schemas + model.$appliedMethods = true; + for (var i = 0; i < schema.childSchemas.length; ++i) { + if (schema.childSchemas[i].model.$appliedMethods) { + continue; + } + applyMethods(schema.childSchemas[i].model, schema.childSchemas[i].schema); + } +}; diff --git a/lib/services/model/applyStatics.js b/lib/services/model/applyStatics.js new file mode 100644 index 00000000000..1c24dbc18c7 --- /dev/null +++ b/lib/services/model/applyStatics.js @@ -0,0 +1,12 @@ +'use strict'; + +/*! + * Register statics for this model + * @param {Model} model + * @param {Schema} schema + */ +module.exports = function applyStatics(model, schema) { + for (var i in schema.statics) { + model[i] = schema.statics[i]; + } +}; diff --git a/lib/services/model/discriminator.js b/lib/services/model/discriminator.js new file mode 100644 index 00000000000..a2479aaa1c4 --- /dev/null +++ b/lib/services/model/discriminator.js @@ -0,0 +1,144 @@ +'use strict'; + +var defineKey = require('../document/compile').defineKey; +var utils = require('../../utils'); + +var CUSTOMIZABLE_DISCRIMINATOR_OPTIONS = { + toJSON: true, + toObject: true, + _id: true, + id: true +}; + +/*! + * ignore + */ + +module.exports = function discriminator(model, name, schema) { + if (!(schema && schema.instanceOfSchema)) { + throw new Error('You must pass a valid discriminator Schema'); + } + + if (model.base && model.base.options.applyPluginsToDiscriminators) { + model.base._applyPlugins(schema); + } + + if (model.schema.discriminatorMapping && + !model.schema.discriminatorMapping.isRoot) { + throw new Error('Discriminator "' + name + + '" can only be a discriminator of the root model'); + } + + var key = model.schema.options.discriminatorKey; + + var baseSchemaAddition = {}; + baseSchemaAddition[key] = { + default: void 0, + select: true, + set: function(newName) { + if (newName === name) { + return name; + } + throw new Error('Can\'t set discriminator key "' + key + '", "' + + name + '" !== "' + newName + '"'); + }, + $skipDiscriminatorCheck: true + }; + baseSchemaAddition[key][model.schema.options.typeKey] = String; + model.schema.add(baseSchemaAddition); + defineKey(key, null, model.prototype, null, [key], model.schema.options); + + if (schema.path(key) && schema.path(key).options.$skipDiscriminatorCheck !== true) { + throw new Error('Discriminator "' + name + + '" cannot have field with name "' + key + '"'); + } + + function merge(schema, baseSchema) { + if (baseSchema.paths._id && + baseSchema.paths._id.options && + !baseSchema.paths._id.options.auto) { + var originalSchema = schema; + utils.merge(schema, originalSchema, { retainKeyOrder: true }); + delete schema.paths._id; + delete schema.tree._id; + } + utils.merge(schema, baseSchema, { + retainKeyOrder: true, + omit: { discriminators: true } + }); + + var obj = {}; + obj[key] = { + default: name, + select: true, + set: function(newName) { + if (newName === name) { + return name; + } + throw new Error('Can\'t set discriminator key "' + key + '"'); + }, + $skipDiscriminatorCheck: true + }; + obj[key][schema.options.typeKey] = String; + schema.add(obj); + schema.discriminatorMapping = {key: key, value: name, isRoot: false}; + + if (baseSchema.options.collection) { + schema.options.collection = baseSchema.options.collection; + } + + var toJSON = schema.options.toJSON; + var toObject = schema.options.toObject; + var _id = schema.options._id; + var id = schema.options.id; + + var keys = Object.keys(schema.options); + schema.options.discriminatorKey = baseSchema.options.discriminatorKey; + + for (var i = 0; i < keys.length; ++i) { + var _key = keys[i]; + if (!CUSTOMIZABLE_DISCRIMINATOR_OPTIONS[_key]) { + if (!utils.deepEqual(schema.options[_key], baseSchema.options[_key])) { + throw new Error('Can\'t customize discriminator option ' + _key + + ' (can only modify ' + + Object.keys(CUSTOMIZABLE_DISCRIMINATOR_OPTIONS).join(', ') + + ')'); + } + } + } + + schema.options = utils.clone(baseSchema.options); + if (toJSON) schema.options.toJSON = toJSON; + if (toObject) schema.options.toObject = toObject; + if (typeof _id !== 'undefined') { + schema.options._id = _id; + } + schema.options.id = id; + schema.s.hooks = model.schema.s.hooks.merge(schema.s.hooks); + + schema.plugins = Array.prototype.slice(baseSchema.plugins); + schema.callQueue = baseSchema.callQueue. + concat(schema.callQueue.slice(schema._defaultMiddleware.length)); + schema._requiredpaths = undefined; // reset just in case Schema#requiredPaths() was called on either schema + } + + // merges base schema into new discriminator schema and sets new type field. + merge(schema, model.schema); + + if (!model.discriminators) { + model.discriminators = {}; + } + + if (!model.schema.discriminatorMapping) { + model.schema.discriminatorMapping = {key: key, value: null, isRoot: true}; + model.schema.discriminators = {}; + } + + model.schema.discriminators[name] = schema; + + if (model.discriminators[name]) { + throw new Error('Discriminator with name "' + name + '" already exists'); + } + + return schema; +}; diff --git a/lib/services/populate/getSchemaTypes.js b/lib/services/populate/getSchemaTypes.js new file mode 100644 index 00000000000..e3246f36143 --- /dev/null +++ b/lib/services/populate/getSchemaTypes.js @@ -0,0 +1,115 @@ +'use strict'; + +/*! + * ignore + */ + +var Mixed = require('../../schema/mixed'); +var mpath = require('mpath'); + +/*! + * @param {Schema} schema + * @param {Object} doc POJO + * @param {string} path + */ + +module.exports = function getSchemaTypes(schema, doc, path) { + var pathschema = schema.path(path); + + if (pathschema) { + return pathschema; + } + + function search(parts, schema) { + var p = parts.length + 1; + var foundschema; + var trypath; + + while (p--) { + trypath = parts.slice(0, p).join('.'); + foundschema = schema.path(trypath); + if (foundschema) { + if (foundschema.caster) { + // array of Mixed? + if (foundschema.caster instanceof Mixed) { + return foundschema.caster; + } + + var schemas = null; + if (doc != null && foundschema.schema != null && foundschema.schema.discriminators != null) { + var discriminators = foundschema.schema.discriminators; + var keys = mpath.get(trypath + '.' + foundschema.schema.options.discriminatorKey, + doc); + schemas = Object.keys(discriminators). + reduce(function(cur, discriminator) { + if (keys.indexOf(discriminator) !== -1) { + cur.push(discriminators[discriminator]); + } + return cur; + }, []); + } + + // Now that we found the array, we need to check if there + // are remaining document paths to look up for casting. + // Also we need to handle array.$.path since schema.path + // doesn't work for that. + // If there is no foundschema.schema we are dealing with + // a path like array.$ + if (p !== parts.length && foundschema.schema) { + var ret; + if (parts[p] === '$') { + if (p + 1 === parts.length) { + // comments.$ + return foundschema; + } + // comments.$.comments.$.title + ret = search(parts.slice(p + 1), schema); + if (ret) { + ret.$isUnderneathDocArray = ret.$isUnderneathDocArray || + !foundschema.schema.$isSingleNested; + } + return ret; + } + + if (schemas != null && schemas.length > 0) { + ret = []; + for (var i = 0; i < schemas.length; ++i) { + var _ret = search(parts.slice(p), schemas[i]); + if (_ret != null) { + _ret.$isUnderneathDocArray = _ret.$isUnderneathDocArray || + !foundschema.schema.$isSingleNested; + if (_ret.$isUnderneathDocArray) { + ret.$isUnderneathDocArray = true; + } + ret.push(_ret); + } + } + return ret; + } else { + ret = search(parts.slice(p), foundschema.schema); + + if (ret) { + ret.$isUnderneathDocArray = ret.$isUnderneathDocArray || + !foundschema.schema.$isSingleNested; + } + + return ret; + } + } + } + + return foundschema; + } + } + } + + // look for arrays + var parts = path.split('.'); + for (var i = 0; i < parts.length; ++i) { + if (parts[i] === '$') { + // Re: gh-5628, because `schema.path()` doesn't take $ into account. + parts[i] = '0'; + } + } + return search(parts, schema); +}; diff --git a/lib/services/projection/isDefiningProjection.js b/lib/services/projection/isDefiningProjection.js new file mode 100644 index 00000000000..67dfb39fc68 --- /dev/null +++ b/lib/services/projection/isDefiningProjection.js @@ -0,0 +1,18 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function isDefiningProjection(val) { + if (val == null) { + // `undefined` or `null` become exclusive projections + return true; + } + if (typeof val === 'object') { + // Only cases where a value does **not** define whether the whole projection + // is inclusive or exclusive are `$meta` and `$slice`. + return !('$meta' in val) && !('$slice' in val); + } + return true; +}; diff --git a/lib/services/projection/isInclusive.js b/lib/services/projection/isInclusive.js new file mode 100644 index 00000000000..64c3ded349d --- /dev/null +++ b/lib/services/projection/isInclusive.js @@ -0,0 +1,30 @@ +'use strict'; + +var isDefiningProjection = require('./isDefiningProjection'); + +/*! + * ignore + */ + +module.exports = function isInclusive(projection) { + if (projection == null) { + return false; + } + + var props = Object.keys(projection); + var numProps = props.length; + if (numProps === 0) { + return false; + } + + for (var i = 0; i < numProps; ++i) { + var prop = props[i]; + // If field is truthy (1, true, etc.) and not an object, then this + // projection must be inclusive. If object, assume its $meta, $slice, etc. + if (isDefiningProjection(projection[prop]) && !!projection[prop]) { + return true; + } + } + + return false; +}; diff --git a/lib/services/projection/isPathSelectedInclusive.js b/lib/services/projection/isPathSelectedInclusive.js new file mode 100644 index 00000000000..7738e7c1d14 --- /dev/null +++ b/lib/services/projection/isPathSelectedInclusive.js @@ -0,0 +1,28 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function isPathSelectedInclusive(fields, path) { + var chunks = path.split('.'); + var cur = ''; + var j; + var keys; + var numKeys; + for (var i = 0; i < chunks.length; ++i) { + cur += cur.length ? '.' : '' + chunks[i]; + if (fields[cur]) { + keys = Object.keys(fields); + numKeys = keys.length; + for (j = 0; j < numKeys; ++j) { + if (keys[i].indexOf(cur + '.') === 0 && keys[i].indexOf(path) !== 0) { + continue; + } + } + return true; + } + } + + return false; +}; diff --git a/lib/services/query/castUpdate.js b/lib/services/query/castUpdate.js new file mode 100644 index 00000000000..c89de2cc902 --- /dev/null +++ b/lib/services/query/castUpdate.js @@ -0,0 +1,354 @@ +'use strict'; + +var StrictModeError = require('../../error/strict'); +var ValidationError = require('../../error/validation'); +var utils = require('../../utils'); + +/*! + * Casts an update op based on the given schema + * + * @param {Schema} schema + * @param {Object} obj + * @param {Object} options + * @param {Boolean} [options.overwrite] defaults to false + * @param {Boolean|String} [options.strict] defaults to true + * @param {Query} context passed to setters + * @return {Boolean} true iff the update is non-empty + */ + +module.exports = function castUpdate(schema, obj, options, context) { + if (!obj) { + return undefined; + } + + var ops = Object.keys(obj); + var i = ops.length; + var ret = {}; + var hasKeys; + var val; + var hasDollarKey = false; + var overwrite = options.overwrite; + + while (i--) { + var op = ops[i]; + // if overwrite is set, don't do any of the special $set stuff + if (op[0] !== '$' && !overwrite) { + // fix up $set sugar + if (!ret.$set) { + if (obj.$set) { + ret.$set = obj.$set; + } else { + ret.$set = {}; + } + } + ret.$set[op] = obj[op]; + ops.splice(i, 1); + if (!~ops.indexOf('$set')) ops.push('$set'); + } else if (op === '$set') { + if (!ret.$set) { + ret[op] = obj[op]; + } + } else { + ret[op] = obj[op]; + } + } + + // cast each value + i = ops.length; + + // if we get passed {} for the update, we still need to respect that when it + // is an overwrite scenario + if (overwrite) { + hasKeys = true; + } + + while (i--) { + op = ops[i]; + val = ret[op]; + hasDollarKey = hasDollarKey || op.charAt(0) === '$'; + if (val && + typeof val === 'object' && + (!overwrite || hasDollarKey)) { + hasKeys |= walkUpdatePath(schema, val, op, options.strict, context); + } else if (overwrite && ret && typeof ret === 'object') { + // if we are just using overwrite, cast the query and then we will + // *always* return the value, even if it is an empty object. We need to + // set hasKeys above because we need to account for the case where the + // user passes {} and wants to clobber the whole document + // Also, _walkUpdatePath expects an operation, so give it $set since that + // is basically what we're doing + walkUpdatePath(schema, ret, '$set', options.strict, context); + } else { + var msg = 'Invalid atomic update value for ' + op + '. ' + + 'Expected an object, received ' + typeof val; + throw new Error(msg); + } + } + + return hasKeys && ret; +}; + +/*! + * Walk each path of obj and cast its values + * according to its schema. + * + * @param {Schema} schema + * @param {Object} obj - part of a query + * @param {String} op - the atomic operator ($pull, $set, etc) + * @param {Boolean|String} strict + * @param {Query} context + * @param {String} pref - path prefix (internal only) + * @return {Bool} true if this path has keys to update + * @api private + */ + +function walkUpdatePath(schema, obj, op, strict, context, pref) { + var prefix = pref ? pref + '.' : ''; + var keys = Object.keys(obj); + var i = keys.length; + var hasKeys = false; + var schematype; + var key; + var val; + + var hasError = false; + var aggregatedError = new ValidationError(); + + var useNestedStrict = schema.options.useNestedStrict; + + while (i--) { + key = keys[i]; + val = obj[key]; + + if (val && val.constructor.name === 'Object') { + // watch for embedded doc schemas + schematype = schema._getSchema(prefix + key); + if (schematype && schematype.caster && op in castOps) { + // embedded doc schema + hasKeys = true; + + if ('$each' in val) { + try { + obj[key] = { + $each: castUpdateVal(schematype, val.$each, op, context) + }; + } catch (error) { + hasError = true; + _handleCastError(error, context, key, aggregatedError); + } + + if (val.$slice != null) { + obj[key].$slice = val.$slice | 0; + } + + if (val.$sort) { + obj[key].$sort = val.$sort; + } + + if (!!val.$position || val.$position === 0) { + obj[key].$position = val.$position; + } + } else { + try { + obj[key] = castUpdateVal(schematype, val, op, context); + } catch (error) { + hasError = true; + _handleCastError(error, context, key, aggregatedError); + } + } + } else if ((op === '$currentDate') || (op in castOps && schematype)) { + // $currentDate can take an object + try { + obj[key] = castUpdateVal(schematype, val, op, context); + } catch (error) { + hasError = true; + _handleCastError(error, context, key, aggregatedError); + } + + hasKeys = true; + } else { + var pathToCheck = (prefix + key); + var v = schema._getPathType(pathToCheck); + var _strict = strict; + if (useNestedStrict && + v && + v.schema && + 'strict' in v.schema.options) { + _strict = v.schema.options.strict; + } + + if (v.pathType === 'undefined') { + if (_strict === 'throw') { + throw new StrictModeError(pathToCheck); + } else if (_strict) { + delete obj[key]; + continue; + } + } + + // gh-2314 + // we should be able to set a schema-less field + // to an empty object literal + hasKeys |= walkUpdatePath(schema, val, op, strict, context, prefix + key) || + (utils.isObject(val) && Object.keys(val).length === 0); + } + } else { + var checkPath = (key === '$each' || key === '$or' || key === '$and' || key === '$in') ? + pref : prefix + key; + schematype = schema._getSchema(checkPath); + + var pathDetails = schema._getPathType(checkPath); + var isStrict = strict; + if (useNestedStrict && + pathDetails && + pathDetails.schema && + 'strict' in pathDetails.schema.options) { + isStrict = pathDetails.schema.options.strict; + } + + var skip = isStrict && + !schematype && + !/real|nested/.test(pathDetails.pathType); + + if (skip) { + if (isStrict === 'throw') { + throw new StrictModeError(prefix + key); + } else { + delete obj[key]; + } + } else { + // gh-1845 temporary fix: ignore $rename. See gh-3027 for tracking + // improving this. + if (op === '$rename') { + hasKeys = true; + continue; + } + + hasKeys = true; + try { + obj[key] = castUpdateVal(schematype, val, op, key, context); + } catch (error) { + hasError = true; + _handleCastError(error, context, key, aggregatedError); + } + } + } + } + + if (hasError) { + throw aggregatedError; + } + + return hasKeys; +} + +/*! + * ignore + */ + +function _handleCastError(error, query, key, aggregatedError) { + if (typeof query !== 'object' || !query.options.multipleCastError) { + throw error; + } + aggregatedError.addError(key, error); +} + +/*! + * These operators should be cast to numbers instead + * of their path schema type. + */ + +var numberOps = { + $pop: 1, + $unset: 1, + $inc: 1 +}; + +/*! + * These operators require casting docs + * to real Documents for Update operations. + */ + +var castOps = { + $push: 1, + $pushAll: 1, + $addToSet: 1, + $set: 1, + $setOnInsert: 1 +}; + +/*! + * ignore + */ + +var overwriteOps = { + $set: 1, + $setOnInsert: 1 +}; + +/*! + * Casts `val` according to `schema` and atomic `op`. + * + * @param {SchemaType} schema + * @param {Object} val + * @param {String} op - the atomic operator ($pull, $set, etc) + * @param {String} $conditional + * @param {Query} context + * @api private + */ + +function castUpdateVal(schema, val, op, $conditional, context) { + if (!schema) { + // non-existing schema path + return op in numberOps + ? Number(val) + : val; + } + + var cond = schema.caster && op in castOps && + (utils.isObject(val) || Array.isArray(val)); + if (cond) { + // Cast values for ops that add data to MongoDB. + // Ensures embedded documents get ObjectIds etc. + var tmp = schema.cast(val); + if (Array.isArray(val)) { + val = tmp; + } else if (Array.isArray(tmp)) { + val = tmp[0]; + } else { + val = tmp; + } + return val; + } + + if (op in numberOps) { + if (op === '$inc') { + return schema.castForQueryWrapper({ val: val, context: context }); + } + return Number(val); + } + if (op === '$currentDate') { + if (typeof val === 'object') { + return {$type: val.$type}; + } + return Boolean(val); + } + + if (/^\$/.test($conditional)) { + return schema.castForQueryWrapper({ + $conditional: $conditional, + val: val, + context: context + }); + } + + if (overwriteOps[op]) { + return schema.castForQueryWrapper({ + val: val, + context: context, + $skipQueryCastForUpdate: val != null && schema.$isMongooseArray + }); + } + + return schema.castForQueryWrapper({ val: val, context: context }); +} diff --git a/lib/services/query/hasDollarKeys.js b/lib/services/query/hasDollarKeys.js new file mode 100644 index 00000000000..92b4408a349 --- /dev/null +++ b/lib/services/query/hasDollarKeys.js @@ -0,0 +1,16 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function(obj) { + var keys = Object.keys(obj); + var len = keys.length; + for (var i = 0; i < len; ++i) { + if (keys[i].charAt(0) === '$') { + return true; + } + } + return false; +}; diff --git a/lib/services/query/selectPopulatedFields.js b/lib/services/query/selectPopulatedFields.js new file mode 100644 index 00000000000..88b3551cbe7 --- /dev/null +++ b/lib/services/query/selectPopulatedFields.js @@ -0,0 +1,45 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function selectPopulatedFields(query) { + var opts = query._mongooseOptions; + + if (opts.populate != null) { + var paths = Object.keys(opts.populate); + var i; + var userProvidedFields = query._userProvidedFields || {}; + if (query.selectedInclusively()) { + for (i = 0; i < paths.length; ++i) { + if (!isPathInFields(userProvidedFields, paths[i])) { + query.select(paths[i]); + } + } + } else if (query.selectedExclusively()) { + for (i = 0; i < paths.length; ++i) { + if (userProvidedFields[paths[i]] == null) { + delete query._fields[paths[i]]; + } + } + } + } +}; + +/*! + * ignore + */ + +function isPathInFields(userProvidedFields, path) { + var pieces = path.split('.'); + var len = pieces.length; + var cur = pieces[0]; + for (var i = 1; i < len; ++i) { + if (userProvidedFields[cur] != null) { + return true; + } + cur += '.' + pieces[i]; + } + return userProvidedFields[cur] != null; +} diff --git a/lib/services/setDefaultsOnInsert.js b/lib/services/setDefaultsOnInsert.js new file mode 100644 index 00000000000..265d4c8fd0f --- /dev/null +++ b/lib/services/setDefaultsOnInsert.js @@ -0,0 +1,118 @@ +'use strict'; + +var modifiedPaths = require('./common').modifiedPaths; + +/** + * Applies defaults to update and findOneAndUpdate operations. + * + * @param {Object} filter + * @param {Schema} schema + * @param {Object} castedDoc + * @param {Object} options + * @method setDefaultsOnInsert + * @api private + */ + +module.exports = function(filter, schema, castedDoc, options) { + var keys = Object.keys(castedDoc || {}); + var updatedKeys = {}; + var updatedValues = {}; + var numKeys = keys.length; + var hasDollarUpdate = false; + var modified = {}; + + if (options && options.upsert) { + for (var i = 0; i < numKeys; ++i) { + if (keys[i].charAt(0) === '$') { + modifiedPaths(castedDoc[keys[i]], '', modified); + hasDollarUpdate = true; + } + } + + if (!hasDollarUpdate) { + modifiedPaths(castedDoc, '', modified); + } + + var paths = Object.keys(filter); + var numPaths = paths.length; + for (i = 0; i < numPaths; ++i) { + var path = paths[i]; + var condition = filter[path]; + if (condition && typeof condition === 'object') { + var conditionKeys = Object.keys(condition); + var numConditionKeys = conditionKeys.length; + var hasDollarKey = false; + for (var j = 0; j < numConditionKeys; ++j) { + if (conditionKeys[j].charAt(0) === '$') { + hasDollarKey = true; + break; + } + } + if (hasDollarKey) { + continue; + } + } + updatedKeys[path] = true; + modified[path] = true; + } + + if (options && options.overwrite && !hasDollarUpdate) { + // Defaults will be set later, since we're overwriting we'll cast + // the whole update to a document + return castedDoc; + } + + if (options.setDefaultsOnInsert) { + schema.eachPath(function(path, schemaType) { + if (path === '_id') { + // Ignore _id for now because it causes bugs in 2.4 + return; + } + if (schemaType.$isSingleNested) { + // Only handle nested schemas 1-level deep to avoid infinite + // recursion re: https://github.com/mongodb-js/mongoose-autopopulate/issues/11 + schemaType.schema.eachPath(function(_path, _schemaType) { + if (path === '_id') { + // Ignore _id for now because it causes bugs in 2.4 + return; + } + + var def = _schemaType.getDefault(null, true); + if (!isModified(modified, path + '.' + _path) && + typeof def !== 'undefined') { + castedDoc = castedDoc || {}; + castedDoc.$setOnInsert = castedDoc.$setOnInsert || {}; + castedDoc.$setOnInsert[path + '.' + _path] = def; + updatedValues[path + '.' + _path] = def; + } + }); + } else { + var def = schemaType.getDefault(null, true); + if (!isModified(modified, path) && typeof def !== 'undefined') { + castedDoc = castedDoc || {}; + castedDoc.$setOnInsert = castedDoc.$setOnInsert || {}; + castedDoc.$setOnInsert[path] = def; + updatedValues[path] = def; + } + } + }); + } + } + + return castedDoc; +}; + +function isModified(modified, path) { + if (modified[path]) { + return true; + } + var sp = path.split('.'); + var cur = sp[0]; + for (var i = 0; i < sp.length; ++i) { + if (modified[cur]) { + return true; + } + cur += '.' + sp[i]; + } + return false; +} diff --git a/lib/services/updateValidators.js b/lib/services/updateValidators.js index 5b0b5d2cf1c..0d0be0857f2 100644 --- a/lib/services/updateValidators.js +++ b/lib/services/updateValidators.js @@ -2,9 +2,11 @@ * Module dependencies. */ -var async = require('async'); -var ValidationError = require('../error/validation.js'); -var ObjectId = require('../types/objectid'); +var Mixed = require('../schema/mixed'); +var ValidationError = require('../error/validation'); +var parallel = require('async/parallel'); +var flatten = require('./common').flatten; +var modifiedPaths = require('./common').modifiedPaths; /** * Applies validators and defaults to update and findOneAndUpdate operations, @@ -19,15 +21,34 @@ var ObjectId = require('../types/objectid'); */ module.exports = function(query, schema, castedDoc, options) { + var _keys; var keys = Object.keys(castedDoc || {}); var updatedKeys = {}; var updatedValues = {}; + var arrayAtomicUpdates = {}; var numKeys = keys.length; var hasDollarUpdate = false; var modified = {}; + var currentUpdate; + var key; for (var i = 0; i < numKeys; ++i) { if (keys[i].charAt(0) === '$') { + hasDollarUpdate = true; + if (keys[i] === '$push' || keys[i] === '$addToSet') { + _keys = Object.keys(castedDoc[keys[i]]); + for (var ii = 0; ii < _keys.length; ++ii) { + currentUpdate = castedDoc[keys[i]][_keys[ii]]; + if (currentUpdate && currentUpdate.$each) { + arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []). + concat(currentUpdate.$each); + } else { + arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []). + concat([currentUpdate]); + } + } + continue; + } modifiedPaths(castedDoc[keys[i]], '', modified); var flat = flatten(castedDoc[keys[i]]); var paths = Object.keys(flat); @@ -35,14 +56,15 @@ module.exports = function(query, schema, castedDoc, options) { for (var j = 0; j < numPaths; ++j) { var updatedPath = paths[j].replace('.$.', '.0.'); updatedPath = updatedPath.replace(/\.\$$/, '.0'); - if (keys[i] === '$set' || keys[i] === '$setOnInsert') { + key = keys[i]; + if (key === '$set' || key === '$setOnInsert' || + key === '$pull' || key === '$pullAll') { updatedValues[updatedPath] = flat[paths[j]]; - } else if (keys[i] === '$unset') { + } else if (key === '$unset') { updatedValues[updatedPath] = undefined; } updatedKeys[updatedPath] = true; } - hasDollarUpdate = true; } } @@ -52,75 +74,21 @@ module.exports = function(query, schema, castedDoc, options) { updatedKeys = Object.keys(updatedValues); } - if (options && options.upsert) { - paths = Object.keys(query._conditions); - numPaths = keys.length; - for (i = 0; i < numPaths; ++i) { - var path = paths[i]; - var condition = query._conditions[path]; - if (condition && typeof condition === 'object') { - var conditionKeys = Object.keys(condition); - var numConditionKeys = conditionKeys.length; - var hasDollarKey = false; - for (j = 0; j < numConditionKeys; ++j) { - if (conditionKeys[j].charAt(0) === '$') { - hasDollarKey = true; - break; - } - } - if (hasDollarKey) { - continue; - } - } - updatedKeys[path] = true; - modified[path] = true; - } - - if (options.setDefaultsOnInsert) { - schema.eachPath(function(path, schemaType) { - if (path === '_id') { - // Ignore _id for now because it causes bugs in 2.4 - return; - } - if (schemaType.$isSingleNested) { - // Only handle nested schemas 1-level deep to avoid infinite - // recursion re: https://github.com/mongodb-js/mongoose-autopopulate/issues/11 - schemaType.schema.eachPath(function(_path, _schemaType) { - if (path === '_id') { - // Ignore _id for now because it causes bugs in 2.4 - return; - } - - var def = _schemaType.getDefault(null, true); - if (!modified[path + '.' + _path] && typeof def !== 'undefined') { - castedDoc.$setOnInsert = castedDoc.$setOnInsert || {}; - castedDoc.$setOnInsert[path + '.' + _path] = def; - updatedValues[path + '.' + _path] = def; - } - }); - } else { - var def = schemaType.getDefault(null, true); - if (!modified[path] && typeof def !== 'undefined') { - castedDoc.$setOnInsert = castedDoc.$setOnInsert || {}; - castedDoc.$setOnInsert[path] = def; - updatedValues[path] = def; - } - } - }); - } - } - var updates = Object.keys(updatedValues); var numUpdates = updates.length; var validatorsToExecute = []; var validationErrors = []; - for (i = 0; i < numUpdates; ++i) { - (function(i) { - var schemaPath = schema._getSchema(updates[i]); - if (schemaPath) { - validatorsToExecute.push(function(callback) { - schemaPath.doValidate( - updatedValues[updates[i]], + function iter(i, v) { + var schemaPath = schema._getSchema(updates[i]); + if (schemaPath) { + // gh-4305: `_getSchema()` will report all sub-fields of a 'Mixed' path + // as 'Mixed', so avoid double validating them. + if (schemaPath instanceof Mixed && schemaPath.$fullPath !== updates[i]) { + return; + } + validatorsToExecute.push(function(callback) { + schemaPath.doValidate( + v, function(err) { if (err) { err.path = updates[i]; @@ -128,18 +96,62 @@ module.exports = function(query, schema, castedDoc, options) { } callback(null); }, - options && options.context === 'query' ? query : null); + options && options.context === 'query' ? query : null, + {updateValidator: true}); + }); + } + } + for (i = 0; i < numUpdates; ++i) { + iter(i, updatedValues[updates[i]]); + } + + var arrayUpdates = Object.keys(arrayAtomicUpdates); + var numArrayUpdates = arrayUpdates.length; + for (i = 0; i < numArrayUpdates; ++i) { + (function(i) { + var schemaPath = schema._getSchema(arrayUpdates[i]); + if (schemaPath && schemaPath.$isMongooseDocumentArray) { + validatorsToExecute.push(function(callback) { + schemaPath.doValidate( + arrayAtomicUpdates[arrayUpdates[i]], + function(err) { + if (err) { + err.path = arrayUpdates[i]; + validationErrors.push(err); + } + callback(null); + }, + options && options.context === 'query' ? query : null); }); + } else { + schemaPath = schema._getSchema(arrayUpdates[i] + '.0'); + for (var j = 0; j < arrayAtomicUpdates[arrayUpdates[i]].length; ++j) { + (function(j) { + validatorsToExecute.push(function(callback) { + schemaPath.doValidate( + arrayAtomicUpdates[arrayUpdates[i]][j], + function(err) { + if (err) { + err.path = arrayUpdates[i]; + validationErrors.push(err); + } + callback(null); + }, + options && options.context === 'query' ? query : null, + { updateValidator: true }); + }); + })(j); + } } })(i); } return function(callback) { - async.parallel(validatorsToExecute, function() { + parallel(validatorsToExecute, function() { if (validationErrors.length) { var err = new ValidationError(null); for (var i = 0; i < validationErrors.length; ++i) { - err.errors[validationErrors[i].path] = validationErrors[i]; + err.addError(validationErrors[i].path, validationErrors[i]); } return callback(err); } @@ -147,52 +159,3 @@ module.exports = function(query, schema, castedDoc, options) { }); }; }; - -function modifiedPaths(update, path, result) { - var keys = Object.keys(update); - var numKeys = keys.length; - result = result || {}; - path = path ? path + '.' : ''; - - for (var i = 0; i < numKeys; ++i) { - var key = keys[i]; - var val = update[key]; - - result[path + key] = true; - - if (shouldFlatten(val)) { - modifiedPaths(val, path + key, result); - } - } - - return result; -} - -function flatten(update, path) { - var keys = Object.keys(update); - var numKeys = keys.length; - var result = {}; - path = path ? path + '.' : ''; - - for (var i = 0; i < numKeys; ++i) { - var key = keys[i]; - var val = update[key]; - if (shouldFlatten(val)) { - var flat = flatten(val, path + key); - for (var k in flat) { - result[k] = flat[k]; - } - } else { - result[path + key] = val; - } - } - - return result; -} - -function shouldFlatten(val) { - return val && - typeof val === 'object' && - !(val instanceof Date) && - !(val instanceof ObjectId); -} diff --git a/lib/statemachine.js b/lib/statemachine.js index 98a37edd007..3bba519f6e4 100644 --- a/lib/statemachine.js +++ b/lib/statemachine.js @@ -102,10 +102,10 @@ StateMachine.prototype.clear = function clear(state) { */ StateMachine.prototype.some = function some() { - var self = this; + var _this = this; var what = arguments.length ? arguments : this.stateNames; return Array.prototype.some.call(what, function(state) { - return Object.keys(self.states[state]).length; + return Object.keys(_this.states[state]).length; }); }; @@ -126,10 +126,10 @@ StateMachine.prototype._iter = function _iter(iterMethod) { if (!states.length) states = this.stateNames; - var self = this; + var _this = this; var paths = states.reduce(function(paths, state) { - return paths.concat(Object.keys(self.states[state])); + return paths.concat(Object.keys(_this.states[state])); }, []); return paths[iterMethod](function(path, i, paths) { diff --git a/lib/types/array.js b/lib/types/array.js index 542d69bbbc7..06682f1bd3b 100644 --- a/lib/types/array.js +++ b/lib/types/array.js @@ -5,7 +5,9 @@ var EmbeddedDocument = require('./embedded'); var Document = require('../document'); var ObjectId = require('./objectid'); +var cleanModifiedSubpaths = require('../services/document/cleanModifiedSubpaths'); var utils = require('../utils'); + var isMongooseObject = utils.isMongooseObject; /** @@ -26,19 +28,17 @@ var isMongooseObject = utils.isMongooseObject; function MongooseArray(values, path, doc) { var arr = [].concat(values); - utils.decorate( arr, MongooseArray.mixin ); - arr.isMongooseArray = true; - - var _options = { enumerable: false, configurable: true, writable: true }; - var keys = Object.keys(MongooseArray.mixin). - concat(['isMongooseArray', 'validators', '_path']); - for (var i = 0; i < keys.length; ++i) { - Object.defineProperty(arr, keys[i], _options); + var keysMA = Object.keys(MongooseArray.mixin); + var numKeys = keysMA.length; + for (var i = 0; i < numKeys; ++i) { + arr[keysMA[i]] = MongooseArray.mixin[keysMA[i]]; } - arr._atomics = {}; - arr.validators = []; arr._path = path; + arr.isMongooseArray = true; + arr.validators = []; + arr._atomics = {}; + arr._schema = void 0; // Because doc comes from the context of another function, doc === global // can happen if there was a null somewhere up the chain (see #3020) @@ -53,6 +53,18 @@ function MongooseArray(values, path, doc) { } MongooseArray.mixin = { + /*! + * ignore + */ + toBSON: function() { + return this.toObject({ + transform: false, + virtuals: false, + _skipDepopulateTopLevel: true, + depopulate: true, + flattenDecimals: false + }); + }, /** * Stores a queue of atomic operations to perform @@ -84,44 +96,35 @@ MongooseArray.mixin = { */ _cast: function(value) { - var owner = this._owner; var populated = false; var Model; if (this._parent) { - // if a populated array, we must cast to the same model - // instance as specified in the original query. - if (!owner) { - owner = this._owner = this._parent.ownerDocument - ? this._parent.ownerDocument() - : this._parent; - } - - populated = owner.populated(this._path, true); + populated = this._parent.populated(this._path, true); } - if (populated && null != value) { + if (populated && value !== null && value !== undefined) { // cast to the populated Models schema - Model = populated.options.model; + Model = populated.options.model || populated.options.Model; // only objects are permitted so we can safely assume that // non-objects are to be interpreted as _id if (Buffer.isBuffer(value) || value instanceof ObjectId || !utils.isObject(value)) { - value = { _id: value }; + value = {_id: value}; } // gh-2399 // we should cast model only when it's not a discriminator var isDisc = value.schema && value.schema.discriminatorMapping && - value.schema.discriminatorMapping.key !== undefined; + value.schema.discriminatorMapping.key !== undefined; if (!isDisc) { value = new Model(value); } - return this._schema.caster.cast(value, this._parent, true); + return this._schema.caster.applySetters(value, this._parent, true); } - return this._schema.caster.cast(value, this._parent, false); + return this._schema.caster.applySetters(value, this._parent, false); }, /** @@ -144,7 +147,7 @@ MongooseArray.mixin = { dirtyPath = this._path; if (arguments.length) { - if (null != embeddedPath) { + if (embeddedPath != null) { // an embedded doc bubbled up the change dirtyPath = dirtyPath + '.' + this.indexOf(elem) + '.' + embeddedPath; } else { @@ -153,7 +156,7 @@ MongooseArray.mixin = { } } - parent.markModified(dirtyPath); + parent.markModified(dirtyPath, arguments.length > 0 ? elem : parent); } return this; @@ -170,20 +173,20 @@ MongooseArray.mixin = { */ _registerAtomic: function(op, val) { - if ('$set' == op) { + if (op === '$set') { // $set takes precedence over all other ops. // mark entire array modified. - this._atomics = { $set: val }; + this._atomics = {$set: val}; return this; } var atomics = this._atomics; // reset pop/shift after save - if ('$pop' == op && !('$pop' in atomics)) { - var self = this; + if (op === '$pop' && !('$pop' in atomics)) { + var _this = this; this._parent.once('save', function() { - self._popped = self._shifted = null; + _this._popped = _this._shifted = null; }); } @@ -193,7 +196,7 @@ MongooseArray.mixin = { Object.keys(atomics).length && !(op in atomics)) { // a different op was previously registered. // save the entire thing. - this._atomics = { $set: this }; + this._atomics = {$set: this}; return this; } @@ -207,10 +210,10 @@ MongooseArray.mixin = { if (val[0] instanceof EmbeddedDocument) { selector = pullOp['$or'] || (pullOp['$or'] = []); Array.prototype.push.apply(selector, val.map(function(v) { - return v.toObject({ virtuals: false }); + return v.toObject({transform: false, virtuals: false}); })); } else { - selector = pullOp['_id'] || (pullOp['_id'] = {'$in' : [] }); + selector = pullOp['_id'] || (pullOp['_id'] = {$in: []}); selector['$in'] = selector['$in'].concat(val); } } else { @@ -236,8 +239,8 @@ MongooseArray.mixin = { var keys = Object.keys(this._atomics); var i = keys.length; - if (0 === i) { - ret[0] = ['$set', this.toObject({ depopulate: 1, transform: false })]; + if (i === 0) { + ret[0] = ['$set', this.toObject({depopulate: 1, transform: false, _isNested: true, virtuals: false})]; return ret; } @@ -249,15 +252,15 @@ MongooseArray.mixin = { // need to convert their elements as if they were MongooseArrays // to handle populated arrays versus DocumentArrays properly. if (isMongooseObject(val)) { - val = val.toObject({ depopulate: 1, transform: false }); + val = val.toObject({depopulate: 1, transform: false, _isNested: true, virtuals: false}); } else if (Array.isArray(val)) { - val = this.toObject.call(val, { depopulate: 1, transform: false }); + val = this.toObject.call(val, {depopulate: 1, transform: false, _isNested: true}); } else if (val.valueOf) { val = val.valueOf(); } - if ('$addToSet' == op) { - val = { $each: val }; + if (op === '$addToSet') { + val = {$each: val}; } ret.push([op, val]); @@ -276,7 +279,7 @@ MongooseArray.mixin = { */ hasAtomics: function hasAtomics() { - if (!(this._atomics && 'Object' === this._atomics.constructor.name)) { + if (!(this._atomics && this._atomics.constructor.name === 'Object')) { return 0; } @@ -305,8 +308,10 @@ MongooseArray.mixin = { */ push: function() { + _checkManualPopulation(this, arguments); var values = [].map.call(arguments, this._mapCast, this); - values = this._schema.applySetters(values, this._parent); + values = this._schema.applySetters(values, this._parent, undefined, + undefined, { skipDocumentArrayCast: true }); var ret = [].push.apply(this, values); // $pushAll might be fibbed (could be $push). But it makes it easier to @@ -377,7 +382,9 @@ MongooseArray.mixin = { this._markModified(); // only allow popping once - if (this._popped) return; + if (this._popped) { + return; + } this._popped = true; return [].pop.call(this); @@ -441,7 +448,9 @@ MongooseArray.mixin = { this._markModified(); // only allow shifting once - if (this._shifted) return; + if (this._shifted) { + return; + } this._shifted = true; return [].shift.call(this); @@ -495,6 +504,8 @@ MongooseArray.mixin = { * doc.subdocs.push({ _id: 4815162342 }) * doc.subdocs.pull(4815162342); // works * + * The first pull call will result in a atomic operation on the database, if pull is called repeatedly without saving the document, a $set operation is used on the complete array instead, overwriting possible changes that happened on the database in the meantime. + * * @param {any} [args...] * @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull * @api public @@ -510,8 +521,11 @@ MongooseArray.mixin = { while (i--) { mem = cur[i]; - if (mem instanceof EmbeddedDocument) { - if (values.some(function(v) { return v.equals(mem); } )) { + if (mem instanceof Document) { + var some = values.some(function(v) { + return mem.equals(v); + }); + if (some) { [].splice.call(cur, i, 1); } } else if (~cur.indexOf.call(values, mem)) { @@ -528,6 +542,15 @@ MongooseArray.mixin = { } this._markModified(); + + // Might have modified child paths and then pulled, like + // `doc.children[1].name = 'test';` followed by + // `doc.children.remove(doc.children[0]);`. In this case we fall back + // to a `$set` on the whole array. See #3511 + if (cleanModifiedSubpaths(this._parent, this._path) > 0) { + this._registerAtomic('$set', this); + } + return this; }, @@ -544,14 +567,18 @@ MongooseArray.mixin = { */ splice: function splice() { - var ret, vals, i; + var ret; + var vals; + var i; + + _checkManualPopulation(this, Array.prototype.slice.call(arguments, 2)); if (arguments.length) { vals = []; for (i = 0; i < arguments.length; ++i) { vals[i] = i < 2 - ? arguments[i] - : this._cast(arguments[i], arguments[0] + (i - 2)); + ? arguments[i] + : this._cast(arguments[i], arguments[0] + (i - 2)); } ret = [].splice.apply(this, vals); this._registerAtomic('$set', this); @@ -574,6 +601,8 @@ MongooseArray.mixin = { */ unshift: function() { + _checkManualPopulation(this, arguments); + var values = [].map.call(arguments, this._cast, this); values = this._schema.applySetters(values, this._parent); [].unshift.apply(this, values); @@ -619,22 +648,31 @@ MongooseArray.mixin = { */ addToSet: function addToSet() { + _checkManualPopulation(this, arguments); + var values = [].map.call(arguments, this._mapCast, this); values = this._schema.applySetters(values, this._parent); var added = []; - var type = values[0] instanceof EmbeddedDocument ? 'doc' : - values[0] instanceof Date ? 'date' : - ''; + var type = ''; + if (values[0] instanceof EmbeddedDocument) { + type = 'doc'; + } else if (values[0] instanceof Date) { + type = 'date'; + } values.forEach(function(v) { var found; switch (type) { case 'doc': - found = this.some(function(doc) { return doc.equals(v); }); + found = this.some(function(doc) { + return doc.equals(v); + }); break; case 'date': var val = +v; - found = this.some(function(d) { return +d === val; }); + found = this.some(function(d) { + return +d === val; + }); break; default: found = ~this.indexOf(v); @@ -680,10 +718,6 @@ MongooseArray.mixin = { set: function set(i, val) { var value = this._cast(val, i); - value = this._schema.caster instanceof EmbeddedDocument ? - value : - this._schema.caster.applySetters(val, this._parent) - ; this[i] = value; this._markModified(i); return this; @@ -701,10 +735,11 @@ MongooseArray.mixin = { toObject: function(options) { if (options && options.depopulate) { + options._isNested = true; return this.map(function(doc) { return doc instanceof Document - ? doc.toObject(options) - : doc; + ? doc.toObject(options) + : doc; }); } @@ -734,10 +769,13 @@ MongooseArray.mixin = { */ indexOf: function indexOf(obj) { - if (obj instanceof ObjectId) obj = obj.toString(); + if (obj instanceof ObjectId) { + obj = obj.toString(); + } for (var i = 0, len = this.length; i < len; ++i) { - if (obj == this[i]) + if (obj == this[i]) { return i; + } } return -1; } @@ -755,6 +793,43 @@ MongooseArray.mixin = { MongooseArray.mixin.remove = MongooseArray.mixin.pull; +/*! + * ignore + */ + +function _isAllSubdocs(docs, ref) { + if (!ref) { + return false; + } + for (var i = 0; i < docs.length; ++i) { + var arg = docs[i]; + if (arg == null) { + return false; + } + var model = arg.constructor; + if (!(arg instanceof Document) || + (model.modelName !== ref && model.baseModelName !== ref)) { + return false; + } + } + + return true; +} + +/*! + * ignore + */ + +function _checkManualPopulation(arr, docs) { + var ref = arr._schema.caster.options && arr._schema.caster.options.ref; + if (arr.length === 0 && + docs.length > 0) { + if (_isAllSubdocs(docs, ref)) { + arr._parent.populated(arr._path, [], { model: docs[0].constructor }); + } + } +} + /*! * Module exports. */ diff --git a/lib/types/buffer.js b/lib/types/buffer.js index 0660d70a93d..c61459eca7b 100644 --- a/lib/types/buffer.js +++ b/lib/types/buffer.js @@ -22,7 +22,7 @@ function MongooseBuffer(value, encode, offset) { var length = arguments.length; var val; - if (0 === length || null === arguments[0] || undefined === arguments[0]) { + if (length === 0 || arguments[0] === null || arguments[0] === undefined) { val = 0; } else { val = value; @@ -41,17 +41,26 @@ function MongooseBuffer(value, encode, offset) { } var buf = new Buffer(val, encoding, offset); - utils.decorate( buf, MongooseBuffer.mixin ); + utils.decorate(buf, MongooseBuffer.mixin); buf.isMongooseBuffer = true; // make sure these internal props don't show up in Object.keys() Object.defineProperties(buf, { - validators: { value: [] }, - _path: { value: path }, - _parent: { value: doc } + validators: { + value: [], + enumerable: false + }, + _path: { + value: path, + enumerable: false + }, + _parent: { + value: doc, + enumerable: false + } }); - if (doc && "string" === typeof path) { + if (doc && typeof path === 'string') { Object.defineProperty(buf, '_schema', { value: doc.schema.path(path) }); @@ -65,7 +74,7 @@ function MongooseBuffer(value, encode, offset) { * Inherit from Buffer. */ -//MongooseBuffer.prototype = new Buffer(0); +// MongooseBuffer.prototype = new Buffer(0); MongooseBuffer.mixin = { @@ -131,7 +140,7 @@ MongooseBuffer.mixin = { * * `Buffer#copy` does not mark `target` as modified so you must copy from a `MongooseBuffer` for it to work as expected. This is a work around since `copy` modifies the target, not this. * - * @return {MongooseBuffer} + * @return {Number} The number of bytes copied. * @param {Buffer} target * @method copy * @receiver MongooseBuffer @@ -154,21 +163,23 @@ MongooseBuffer.mixin = { ( // node < 0.5 -'writeUInt8 writeUInt16 writeUInt32 writeInt8 writeInt16 writeInt32 ' + -'writeFloat writeDouble fill ' + -'utf8Write binaryWrite asciiWrite set ' + + 'writeUInt8 writeUInt16 writeUInt32 writeInt8 writeInt16 writeInt32 ' + + 'writeFloat writeDouble fill ' + + 'utf8Write binaryWrite asciiWrite set ' + // node >= 0.5 -'writeUInt16LE writeUInt16BE writeUInt32LE writeUInt32BE ' + -'writeInt16LE writeInt16BE writeInt32LE writeInt32BE ' + -'writeFloatLE writeFloatBE writeDoubleLE writeDoubleBE' + 'writeUInt16LE writeUInt16BE writeUInt32LE writeUInt32BE ' + + 'writeInt16LE writeInt16BE writeInt32LE writeInt32BE ' + + 'writeFloatLE writeFloatBE writeDoubleLE writeDoubleBE' ).split(' ').forEach(function(method) { - if (!Buffer.prototype[method]) return; - MongooseBuffer.mixin[method] = new Function( - 'var ret = Buffer.prototype.' + method + '.apply(this, arguments);' + - 'this._markModified();' + - 'return ret;' - ); + if (!Buffer.prototype[method]) { + return; + } + MongooseBuffer.mixin[method] = function() { + var ret = Buffer.prototype[method].apply(this, arguments); + this._markModified(); + return ret; + }; }); /** @@ -195,12 +206,25 @@ MongooseBuffer.mixin = { */ MongooseBuffer.mixin.toObject = function(options) { - var subtype = 'number' == typeof options - ? options - : (this._subtype || 0); + var subtype = typeof options === 'number' + ? options + : (this._subtype || 0); return new Binary(this, subtype); }; +/** + * Converts this buffer for storage in MongoDB, including subtype + * + * @return {Binary} + * @api public + * @method toBSON + * @receiver MongooseBuffer + */ + +MongooseBuffer.mixin.toBSON = function() { + return new Binary(this, this._subtype || 0); +}; + /** * Determines if this buffer is equals to `other` buffer * @@ -220,7 +244,9 @@ MongooseBuffer.mixin.equals = function(other) { } for (var i = 0; i < this.length; ++i) { - if (this[i] !== other[i]) return false; + if (this[i] !== other[i]) { + return false; + } } return true; @@ -249,11 +275,11 @@ MongooseBuffer.mixin.equals = function(other) { */ MongooseBuffer.mixin.subtype = function(subtype) { - if ('number' != typeof subtype) { + if (typeof subtype !== 'number') { throw new TypeError('Invalid subtype. Expected a number'); } - if (this._subtype != subtype) { + if (this._subtype !== subtype) { this._markModified(); } diff --git a/lib/types/decimal128.js b/lib/types/decimal128.js new file mode 100644 index 00000000000..ea49a3a96e3 --- /dev/null +++ b/lib/types/decimal128.js @@ -0,0 +1,11 @@ +/** + * ObjectId type constructor + * + * ####Example + * + * var id = new mongoose.Types.ObjectId; + * + * @constructor ObjectId + */ + +module.exports = require('../drivers').Decimal128; diff --git a/lib/types/documentarray.js b/lib/types/documentarray.js index d3055ef67fd..475396c27af 100644 --- a/lib/types/documentarray.js +++ b/lib/types/documentarray.js @@ -6,7 +6,6 @@ var MongooseArray = require('./array'), ObjectId = require('./objectid'), ObjectIdSchema = require('../schema/objectid'), utils = require('../utils'), - util = require('util'), Document = require('../document'); /** @@ -23,16 +22,36 @@ var MongooseArray = require('./array'), function MongooseDocumentArray(values, path, doc) { var arr = [].concat(values); + arr._path = path; + + var props = { + isMongooseArray: true, + isMongooseDocumentArray: true, + validators: [], + _atomics: {}, + _schema: void 0, + _handlers: void 0 + }; // Values always have to be passed to the constructor to initialize, since // otherwise MongooseArray#push will mark the array as modified to the parent. - utils.decorate( arr, MongooseDocumentArray.mixin ); - arr.isMongooseArray = true; - arr.isMongooseDocumentArray = true; + var keysMA = Object.keys(MongooseArray.mixin); + var numKeys = keysMA.length; + for (var j = 0; j < numKeys; ++j) { + arr[keysMA[j]] = MongooseArray.mixin[keysMA[j]]; + } - arr._atomics = {}; - arr.validators = []; - arr._path = path; + var keysMDA = Object.keys(MongooseDocumentArray.mixin); + numKeys = keysMDA.length; + for (var i = 0; i < numKeys; ++i) { + arr[keysMDA[i]] = MongooseDocumentArray.mixin[keysMDA[i]]; + } + + var keysP = Object.keys(props); + numKeys = keysP.length; + for (var k = 0; k < numKeys; ++k) { + arr[keysP[k]] = props[keysP[k]]; + } // Because doc comes from the context of another function, doc === global // can happen if there was a null somewhere up the chain (see #3020 && #3034) @@ -56,164 +75,208 @@ function MongooseDocumentArray(values, path, doc) { /*! * Inherits from MongooseArray */ -MongooseDocumentArray.mixin = Object.create( MongooseArray.mixin ); +// MongooseDocumentArray.mixin = Object.create( MongooseArray.mixin ); +MongooseDocumentArray.mixin = { + /*! + * ignore + */ + toBSON: function() { + return this.toObject({ + transform: false, + virtuals: false, + _skipDepopulateTopLevel: true, + depopulate: true, + flattenDecimals: false + }); + }, -/** - * Overrides MongooseArray#cast - * - * @method _cast - * @api private - * @receiver MongooseDocumentArray - */ + /** + * Overrides MongooseArray#cast + * + * @method _cast + * @api private + * @receiver MongooseDocumentArray + */ -MongooseDocumentArray.mixin._cast = function(value, index) { - if (value instanceof this._schema.casterConstructor) { - if (!(value.__parent && value.__parentArray)) { - // value may have been created using array.create() - value.__parent = this._parent; - value.__parentArray = this; + _cast: function(value, index) { + var Constructor = this._schema.casterConstructor; + if (value instanceof Constructor || + // Hack re: #5001, see #5005 + (value && value.constructor && value.constructor.baseCasterConstructor === Constructor)) { + if (!(value.__parent && value.__parentArray)) { + // value may have been created using array.create() + value.__parent = this._parent; + value.__parentArray = this; + } + value.__index = index; + return value; } - value.__index = index; - return value; - } - // handle cast('string') or cast(ObjectId) etc. - // only objects are permitted so we can safely assume that - // non-objects are to be interpreted as _id - if (Buffer.isBuffer(value) || - value instanceof ObjectId || !utils.isObject(value)) { - value = { _id: value }; - } - return new this._schema.casterConstructor(value, this, undefined, undefined, index); -}; + if (value === undefined || value === null) { + return null; + } -/** - * Searches array items for the first document with a matching _id. - * - * ####Example: - * - * var embeddedDoc = m.array.id(some_id); - * - * @return {EmbeddedDocument|null} the subdocument or null if not found. - * @param {ObjectId|String|Number|Buffer} id - * @TODO cast to the _id based on schema for proper comparison - * @method id - * @api public - * @receiver MongooseDocumentArray - */ + // handle cast('string') or cast(ObjectId) etc. + // only objects are permitted so we can safely assume that + // non-objects are to be interpreted as _id + if (Buffer.isBuffer(value) || + value instanceof ObjectId || !utils.isObject(value)) { + value = {_id: value}; + } -MongooseDocumentArray.mixin.id = function(id) { - var casted, - sid, - _id; + if (value && + Constructor.discriminators && + Constructor.schema.options.discriminatorKey && + typeof value[Constructor.schema.options.discriminatorKey] === 'string' && + Constructor.discriminators[value[Constructor.schema.options.discriminatorKey]]) { + Constructor = Constructor.discriminators[value[Constructor.schema.options.discriminatorKey]]; + } - try { - var casted_ = ObjectIdSchema.prototype.cast.call({}, id); - if (casted_) casted = String(casted_); - } catch (e) { - casted = null; - } + return new Constructor(value, this, undefined, undefined, index); + }, + + /** + * Searches array items for the first document with a matching _id. + * + * ####Example: + * + * var embeddedDoc = m.array.id(some_id); + * + * @return {EmbeddedDocument|null} the subdocument or null if not found. + * @param {ObjectId|String|Number|Buffer} id + * @TODO cast to the _id based on schema for proper comparison + * @method id + * @api public + * @receiver MongooseDocumentArray + */ - for (var i = 0, l = this.length; i < l; i++) { - _id = this[i].get('_id'); - - if (_id === null || typeof _id === 'undefined') { - continue; - } else if (_id instanceof Document) { - sid || (sid = String(id)); - if (sid == _id._id) return this[i]; - } else if (!(_id instanceof ObjectId)) { - if (utils.deepEqual(id, _id)) return this[i]; - } else if (casted == _id) { - return this[i]; + id: function(id) { + var casted, + sid, + _id; + + try { + var casted_ = ObjectIdSchema.prototype.cast.call({}, id); + if (casted_) { + casted = String(casted_); + } + } catch (e) { + casted = null; } - } - return null; -}; + for (var i = 0, l = this.length; i < l; i++) { + if (!this[i]) { + continue; + } + _id = this[i].get('_id'); -/** - * Returns a native js Array of plain js objects - * - * ####NOTE: - * - * _Each sub-document is converted to a plain object by calling its `#toObject` method._ - * - * @param {Object} [options] optional options to pass to each documents `toObject` method call during conversion - * @return {Array} - * @method toObject - * @api public - * @receiver MongooseDocumentArray - */ + if (_id === null || typeof _id === 'undefined') { + continue; + } else if (_id instanceof Document) { + sid || (sid = String(id)); + if (sid == _id._id) { + return this[i]; + } + } else if (!(id instanceof ObjectId) && !(_id instanceof ObjectId)) { + if (utils.deepEqual(id, _id)) { + return this[i]; + } + } else if (casted == _id) { + return this[i]; + } + } -MongooseDocumentArray.mixin.toObject = function(options) { - return this.map(function(doc) { - return doc && doc.toObject(options) || null; - }); -}; + return null; + }, -/** - * Helper for console.log - * - * @method inspect - * @api public - * @receiver MongooseDocumentArray - */ + /** + * Returns a native js Array of plain js objects + * + * ####NOTE: + * + * _Each sub-document is converted to a plain object by calling its `#toObject` method._ + * + * @param {Object} [options] optional options to pass to each documents `toObject` method call during conversion + * @return {Array} + * @method toObject + * @api public + * @receiver MongooseDocumentArray + */ -MongooseDocumentArray.mixin.inspect = function() { - return '[' + Array.prototype.map.call(this, function(doc) { - if (doc) { - return doc.inspect - ? doc.inspect() - : util.inspect(doc); - } - return 'null'; - }).join('\n') + ']'; -}; + toObject: function(options) { + return this.map(function(doc) { + return doc && doc.toObject(options) || null; + }); + }, -/** - * Creates a subdocument casted to this schema. - * - * This is the same subdocument constructor used for casting. - * - * @param {Object} obj the value to cast to this arrays SubDocument schema - * @method create - * @api public - * @receiver MongooseDocumentArray - */ + /** + * Helper for console.log + * + * @method inspect + * @api public + * @receiver MongooseDocumentArray + */ -MongooseDocumentArray.mixin.create = function(obj) { - return new this._schema.casterConstructor(obj); -}; + inspect: function() { + return Array.prototype.slice.call(this); + }, -/** - * Creates a fn that notifies all child docs of `event`. - * - * @param {String} event - * @return {Function} - * @method notify - * @api private - * @receiver MongooseDocumentArray - */ + /** + * Creates a subdocument casted to this schema. + * + * This is the same subdocument constructor used for casting. + * + * @param {Object} obj the value to cast to this arrays SubDocument schema + * @method create + * @api public + * @receiver MongooseDocumentArray + */ -MongooseDocumentArray.mixin.notify = function notify(event) { - var self = this; - return function notify(val) { - var i = self.length; - while (i--) { - if (!self[i]) continue; - switch (event) { - // only swap for save event for now, we may change this to all event types later - case 'save': - val = self[i]; - break; - default: - // NO-OP - break; - } - self[i].emit(event, val); + create: function(obj) { + var Constructor = this._schema.casterConstructor; + if (obj && + Constructor.discriminators && + Constructor.schema.options.discriminatorKey && + typeof obj[Constructor.schema.options.discriminatorKey] === 'string' && + Constructor.discriminators[obj[Constructor.schema.options.discriminatorKey]]) { + Constructor = Constructor.discriminators[obj[Constructor.schema.options.discriminatorKey]]; } - }; + + return new Constructor(obj); + }, + + /** + * Creates a fn that notifies all child docs of `event`. + * + * @param {String} event + * @return {Function} + * @method notify + * @api private + * @receiver MongooseDocumentArray + */ + + notify: function notify(event) { + var _this = this; + return function notify(val) { + var i = _this.length; + while (i--) { + if (!_this[i]) { + continue; + } + switch (event) { + // only swap for save event for now, we may change this to all event types later + case 'save': + val = _this[i]; + break; + default: + // NO-OP + break; + } + _this[i].emit(event, val); + } + }; + } + }; /*! diff --git a/lib/types/embedded.js b/lib/types/embedded.js index 4629bd85b3d..13e8ebda23f 100644 --- a/lib/types/embedded.js +++ b/lib/types/embedded.js @@ -5,7 +5,7 @@ */ var Document = require('../document_provider')(); -var inspect = require('util').inspect; +var EventEmitter = require('events').EventEmitter; var PromiseProvider = require('../promise_provider'); /** @@ -30,18 +30,36 @@ function EmbeddedDocument(obj, parentArr, skipId, fields, index) { Document.call(this, obj, fields, skipId); - var self = this; + var _this = this; this.on('isNew', function(val) { - self.isNew = val; + _this.isNew = val; + }); + + _this.on('save', function() { + _this.constructor.emit('save', _this); }); } /*! * Inherit from Document */ -EmbeddedDocument.prototype = Object.create( Document.prototype ); +EmbeddedDocument.prototype = Object.create(Document.prototype); EmbeddedDocument.prototype.constructor = EmbeddedDocument; +for (var i in EventEmitter.prototype) { + EmbeddedDocument[i] = EventEmitter.prototype[i]; +} + +EmbeddedDocument.prototype.toBSON = function() { + return this.toObject({ + transform: false, + virtuals: false, + _skipDepopulateTopLevel: true, + depopulate: true, + flattenDecimals: false + }); +}; + /** * Marks the embedded doc modified. * @@ -57,9 +75,11 @@ EmbeddedDocument.prototype.constructor = EmbeddedDocument; */ EmbeddedDocument.prototype.markModified = function(path) { - if (!this.__parentArray) return; - this.$__.activePaths.modify(path); + if (!this.__parentArray) { + return; + } + if (this.isNew) { // Mark the WHOLE parent array as modified // if this is a new document (i.e., we are initializing @@ -70,6 +90,16 @@ EmbeddedDocument.prototype.markModified = function(path) { } }; +/*! + * ignore + */ + +EmbeddedDocument.prototype.populate = function() { + throw new Error('Mongoose does not support calling populate() on nested ' + + 'docs. Instead of `doc.arr[0].populate("path")`, use ' + + '`doc.populate("arr.0.path")`'); +}; + /** * Used as a stub for [hooks.js](https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3) * @@ -90,56 +120,66 @@ EmbeddedDocument.prototype.save = function(fn) { }); }; +/*! + * Registers remove event listeners for triggering + * on subdocuments. + * + * @param {EmbeddedDocument} sub + * @api private + */ + +function registerRemoveListener(sub) { + var owner = sub.ownerDocument(); + + function emitRemove() { + owner.removeListener('save', emitRemove); + owner.removeListener('remove', emitRemove); + sub.emit('remove', sub); + sub.constructor.emit('remove', sub); + owner = sub = null; + } + + owner.on('save', emitRemove); + owner.on('remove', emitRemove); +} + /** * Removes the subdocument from its parent array. * + * @param {Object} [options] * @param {Function} [fn] * @api public */ -EmbeddedDocument.prototype.remove = function(fn) { - if (!this.__parentArray) return this; +EmbeddedDocument.prototype.remove = function(options, fn) { + if ( typeof options === 'function' && !fn ) { + fn = options; + options = undefined; + } + if (!this.__parentArray || (options && options.noop)) { + fn && fn(null); + return this; + } var _id; if (!this.willRemove) { _id = this._doc._id; if (!_id) { throw new Error('For your own good, Mongoose does not know ' + - 'how to remove an EmbeddedDocument that has no _id'); + 'how to remove an EmbeddedDocument that has no _id'); } - this.__parentArray.pull({ _id: _id }); + this.__parentArray.pull({_id: _id}); this.willRemove = true; registerRemoveListener(this); } - if (fn) + if (fn) { fn(null); + } return this; }; -/*! - * Registers remove event listeners for triggering - * on subdocuments. - * - * @param {EmbeddedDocument} sub - * @api private - */ - -function registerRemoveListener(sub) { - var owner = sub.ownerDocument(); - - owner.on('save', emitRemove); - owner.on('remove', emitRemove); - - function emitRemove() { - owner.removeListener('save', emitRemove); - owner.removeListener('remove', emitRemove); - sub.emit('remove', sub); - owner = sub = emitRemove = null; - } -} - /** * Override #update method of parent documents. * @api private @@ -156,7 +196,12 @@ EmbeddedDocument.prototype.update = function() { */ EmbeddedDocument.prototype.inspect = function() { - return inspect(this.toObject()); + return this.toObject({ + transform: false, + retainKeyOrder: true, + virtuals: false, + flattenDecimals: false + }); }; /** @@ -170,8 +215,11 @@ EmbeddedDocument.prototype.inspect = function() { EmbeddedDocument.prototype.invalidate = function(path, err, val, first) { if (!this.__parent) { - var msg = 'Unable to invalidate a subdocument that has not been added to an array.'; - throw new Error(msg); + Document.prototype.invalidate.call(this, path, err, val); + if (err.$isValidatorError) { + return true; + } + throw err; } var index = this.__index; @@ -221,10 +269,9 @@ EmbeddedDocument.prototype.$markValid = function(path) { EmbeddedDocument.prototype.$isValid = function(path) { var index = this.__index; - if (typeof index !== 'undefined') { - + if (typeof index !== 'undefined' && this.__parent) { return !this.__parent.$__.validationError || - !this.__parent.$__.validationError.errors[path]; + !this.__parent.$__.validationError.errors[this.$__fullPath(path)]; } return true; @@ -242,13 +289,16 @@ EmbeddedDocument.prototype.ownerDocument = function() { } var parent = this.__parent; - if (!parent) return this; + if (!parent) { + return this; + } - while (parent.__parent) { - parent = parent.__parent; + while (parent.__parent || parent.$parent) { + parent = parent.__parent || parent.$parent; } - return this.$__.ownerDocument = parent; + this.$__.ownerDocument = parent; + return this.$__.ownerDocument; }; /** @@ -263,13 +313,19 @@ EmbeddedDocument.prototype.ownerDocument = function() { EmbeddedDocument.prototype.$__fullPath = function(path) { if (!this.$__.fullPath) { - var parent = this; - if (!parent.__parent) return path; + var parent = this; // eslint-disable-line consistent-this + if (!parent.__parent) { + return path; + } var paths = []; - while (parent.__parent) { - paths.unshift(parent.__parentArray._path); - parent = parent.__parent; + while (parent.__parent || parent.$parent) { + if (parent.__parent) { + paths.unshift(parent.__parentArray._path); + } else { + paths.unshift(parent.$basePath); + } + parent = parent.__parent || parent.$parent; } this.$__.fullPath = paths.join('.'); @@ -281,8 +337,8 @@ EmbeddedDocument.prototype.$__fullPath = function(path) { } return path - ? this.$__.fullPath + '.' + path - : this.$__.fullPath; + ? this.$__.fullPath + '.' + path + : this.$__.fullPath; }; /** diff --git a/lib/types/index.js b/lib/types/index.js index 0d01923df6c..3fdf2bd6758 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -10,6 +10,7 @@ exports.Document = // @deprecate exports.Embedded = require('./embedded'); exports.DocumentArray = require('./documentarray'); +exports.Decimal128 = require('./decimal128'); exports.ObjectId = require('./objectid'); exports.Subdocument = require('./subdocument'); diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index 1a345b9e044..3d622ec1635 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -10,13 +10,23 @@ module.exports = Subdocument; * @api private */ -function Subdocument() { - Document.apply(this, arguments); +function Subdocument(value, fields, parent, skipId, options) { this.$isSingleNested = true; + Document.call(this, value, fields, skipId, options); } Subdocument.prototype = Object.create(Document.prototype); +Subdocument.prototype.toBSON = function() { + return this.toObject({ + transform: false, + virtuals: false, + _skipDepopulateTopLevel: true, + depopulate: true, + flattenDecimals: false + }); +}; + /** * Used as a stub for [hooks.js](https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3) * @@ -38,25 +48,131 @@ Subdocument.prototype.save = function(fn) { }; Subdocument.prototype.$isValid = function(path) { - if (this.$parent) { + if (this.$parent && this.$basePath) { return this.$parent.$isValid([this.$basePath, path].join('.')); } + return Document.prototype.$isValid.call(this, path); }; Subdocument.prototype.markModified = function(path) { - if (this.$parent) { - this.$parent.markModified([this.$basePath, path].join('.')); + Document.prototype.markModified.call(this, path); + if (this.$parent && this.$basePath) { + if (this.$parent.isDirectModified(this.$basePath)) { + return; + } + this.$parent.markModified([this.$basePath, path].join('.'), this); } }; Subdocument.prototype.$markValid = function(path) { - if (this.$parent) { + Document.prototype.$markValid.call(this, path); + if (this.$parent && this.$basePath) { this.$parent.$markValid([this.$basePath, path].join('.')); } }; Subdocument.prototype.invalidate = function(path, err, val) { - if (this.$parent) { + // Hack: array subdocuments' validationError is equal to the owner doc's, + // so validating an array subdoc gives the top-level doc back. Temporary + // workaround for #5208 so we don't have circular errors. + if (err !== this.ownerDocument().$__.validationError) { + Document.prototype.invalidate.call(this, path, err, val); + } + + if (this.$parent && this.$basePath) { this.$parent.invalidate([this.$basePath, path].join('.'), err, val); + } else if (err.kind === 'cast' || err.name === 'CastError') { + throw err; + } +}; + +/** + * Returns the top level document of this sub-document. + * + * @return {Document} + */ + +Subdocument.prototype.ownerDocument = function() { + if (this.$__.ownerDocument) { + return this.$__.ownerDocument; } + + var parent = this.$parent; + if (!parent) { + return this; + } + + while (parent.$parent || parent.__parent) { + parent = parent.$parent || parent.__parent; + } + this.$__.ownerDocument = parent; + return this.$__.ownerDocument; +}; + +/** + * Returns this sub-documents parent document. + * + * @api public + */ + +Subdocument.prototype.parent = function() { + return this.$parent; }; + +/** + * Null-out this subdoc + * + * @param {Object} [options] + * @param {Function} [callback] optional callback for compatibility with Document.prototype.remove + */ + +Subdocument.prototype.remove = function(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + registerRemoveListener(this); + + // If removing entire doc, no need to remove subdoc + if (!options || !options.noop) { + this.$parent.set(this.$basePath, null); + } + + if (typeof callback === 'function') { + callback(null); + } +}; + +/*! + * ignore + */ + +Subdocument.prototype.populate = function() { + throw new Error('Mongoose does not support calling populate() on nested ' + + 'docs. Instead of `doc.nested.populate("path")`, use ' + + '`doc.populate("nested.path")`'); +}; + +/*! + * Registers remove event listeners for triggering + * on subdocuments. + * + * @param {EmbeddedDocument} sub + * @api private + */ + +function registerRemoveListener(sub) { + var owner = sub.ownerDocument(); + + function emitRemove() { + owner.removeListener('save', emitRemove); + owner.removeListener('remove', emitRemove); + sub.emit('remove', sub); + sub.constructor.emit('remove', sub); + owner = sub = null; + } + + owner.on('save', emitRemove); + owner.on('remove', emitRemove); +} diff --git a/lib/utils.js b/lib/utils.js index 3c279e96f5c..c687ae55cab 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,6 +2,7 @@ * Module dependencies. */ +var Decimal = require('./types/decimal128'); var ObjectId = require('./types/objectid'); var cloneRegExp = require('regexp-clone'); var sliced = require('sliced'); @@ -21,9 +22,15 @@ var Document; exports.toCollectionName = function(name, options) { options = options || {}; - if ('system.profile' === name) return name; - if ('system.indexes' === name) return name; - if (options.pluralization === false) return name; + if (name === 'system.profile') { + return name; + } + if (name === 'system.indexes') { + return name; + } + if (options.pluralization === false) { + return name; + } return pluralize(name.toLowerCase()); }; @@ -113,7 +120,9 @@ function pluralize(str) { found = rules.filter(function(rule) { return str.match(rule[0]); }); - if (found[0]) return str.replace(found[0][0], found[0][1]); + if (found[0]) { + return str.replace(found[0][0], found[0][1]); + } } return str; } @@ -130,29 +139,37 @@ function pluralize(str) { */ exports.deepEqual = function deepEqual(a, b) { - if (a === b) return true; + if (a === b) { + return true; + } - if (a instanceof Date && b instanceof Date) + if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); + } - if (a instanceof ObjectId && b instanceof ObjectId) { + if ((a instanceof ObjectId && b instanceof ObjectId) || + (a instanceof Decimal && b instanceof Decimal)) { return a.toString() === b.toString(); } if (a instanceof RegExp && b instanceof RegExp) { - return a.source == b.source && - a.ignoreCase == b.ignoreCase && - a.multiline == b.multiline && - a.global == b.global; + return a.source === b.source && + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.global === b.global; } - if (typeof a !== 'object' && typeof b !== 'object') + if (typeof a !== 'object' && typeof b !== 'object') { return a == b; + } - if (a === null || b === null || a === undefined || b === undefined) + if (a === null || b === null || a === undefined || b === undefined) { return false; + } - if (a.prototype !== b.prototype) return false; + if (a.prototype !== b.prototype) { + return false; + } // Handle MongooseNumbers if (a instanceof Number && b instanceof Number) { @@ -163,37 +180,46 @@ exports.deepEqual = function deepEqual(a, b) { return exports.buffer.areEqual(a, b); } - if (isMongooseObject(a)) a = a.toObject(); - if (isMongooseObject(b)) b = b.toObject(); + if (isMongooseObject(a)) { + a = a.toObject(); + } + if (isMongooseObject(b)) { + b = b.toObject(); + } try { var ka = Object.keys(a), kb = Object.keys(b), key, i; - } catch (e) {//happens when one is a string literal and the other isn't + } catch (e) { + // happens when one is a string literal and the other isn't return false; } // having the same number of owned properties (keys incorporates // hasOwnProperty) - if (ka.length != kb.length) + if (ka.length !== kb.length) { return false; + } - //the same set of keys (although not necessarily the same order), + // the same set of keys (although not necessarily the same order), ka.sort(); kb.sort(); - //~~~cheap key test + // ~~~cheap key test for (i = ka.length - 1; i >= 0; i--) { - if (ka[i] != kb[i]) + if (ka[i] !== kb[i]) { return false; + } } - //equivalent values for every corresponding key, and - //~~~possibly expensive deep test + // equivalent values for every corresponding key, and + // ~~~possibly expensive deep test for (i = ka.length - 1; i >= 0; i--) { key = ka[i]; - if (!deepEqual(a[key], b[key])) return false; + if (!deepEqual(a[key], b[key])) { + return false; + } } return true; @@ -213,18 +239,19 @@ exports.deepEqual = function deepEqual(a, b) { */ exports.clone = function clone(obj, options) { - if (obj === undefined || obj === null) + if (obj === undefined || obj === null) { return obj; + } - if (Array.isArray(obj)) + if (Array.isArray(obj)) { return cloneArray(obj, options); + } if (isMongooseObject(obj)) { - if (options && options.json && 'function' === typeof obj.toJSON) { + if (options && options.json && typeof obj.toJSON === 'function') { return obj.toJSON(options); - } else { - return obj.toObject(options); } + return obj.toObject(options); } if (obj.constructor) { @@ -241,19 +268,47 @@ exports.clone = function clone(obj, options) { } } - if (obj instanceof ObjectId) + if (obj instanceof ObjectId) { return new ObjectId(obj.id); + } + if (obj instanceof Decimal) { + if (options && options.flattenDecimals) { + return obj.toJSON(); + } + return Decimal.fromString(obj.toString()); + } if (!obj.constructor && exports.isObject(obj)) { // object created with Object.create(null) return cloneObject(obj, options); } - if (obj.valueOf) + if (obj.valueOf) { return obj.valueOf(); + } }; var clone = exports.clone; +/*! + * TODO: replace with Object.assign() in 5.0 + */ + +exports.assign = function(target) { + for (var i = 1; i < arguments.length; ++i) { + var nextSource = arguments[i]; + + if (nextSource != null) { + for (var nextKey in nextSource) { + if (nextSource.hasOwnProperty(nextKey)) { + target[nextKey] = nextSource[nextKey]; + } + } + } + } + + return target; +}; + /*! * ignore */ @@ -272,7 +327,7 @@ function cloneObject(obj, options) { for (k in obj) { val = clone(obj[k], options); - if (!minimize || ('undefined' !== typeof val)) { + if (!minimize || (typeof val !== 'undefined')) { hasKeys || (hasKeys = true); ret[k] = val; } @@ -287,22 +342,25 @@ function cloneObject(obj, options) { k = keys[i]; val = clone(obj[k], options); - if (!minimize || ('undefined' !== typeof val)) { - if (!hasKeys) hasKeys = true; + if (!minimize || (typeof val !== 'undefined')) { + if (!hasKeys) { + hasKeys = true; + } ret[k] = val; } } } return minimize - ? hasKeys && ret - : ret; + ? hasKeys && ret + : ret; } function cloneArray(arr, options) { var ret = []; - for (var i = 0, l = arr.length; i < l; i++) + for (var i = 0, l = arr.length; i < l; i++) { ret.push(clone(arr[i], options)); + } return ret; } @@ -350,27 +408,44 @@ exports.random = function() { * @api private */ -exports.merge = function merge(to, from) { - var keys = Object.keys(from), - i = keys.length, - key; - - while (i--) { - key = keys[i]; - if ('undefined' === typeof to[key]) { - to[key] = from[key]; - } else if (exports.isObject(from[key])) { - merge(to[key], from[key]); +exports.merge = function merge(to, from, options) { + options = options || {}; + var keys = Object.keys(from); + var i = 0; + var len = keys.length; + var key; + + if (options.retainKeyOrder) { + while (i < len) { + key = keys[i++]; + if (options.omit && options.omit[key]) { + continue; + } + if (to[key] == null) { + to[key] = from[key]; + } else if (exports.isObject(from[key])) { + merge(to[key], from[key], options); + } else if (options.overwrite) { + to[key] = from[key]; + } + } + } else { + while (len--) { + key = keys[len]; + if (options.omit && options.omit[key]) { + continue; + } + if (to[key] == null) { + to[key] = from[key]; + } else if (exports.isObject(from[key])) { + merge(to[key], from[key], options); + } else if (options.overwrite) { + to[key] = from[key]; + } } } }; -/*! - * toString helper - */ - -var toString = Object.prototype.toString; - /*! * Applies toObject recursively. * @@ -380,6 +455,7 @@ var toString = Object.prototype.toString; */ exports.toObject = function toObject(obj) { + Document || (Document = require('./document')); var ret; if (exports.isNullOrUndefined(obj)) { @@ -426,7 +502,7 @@ exports.isObject = function(arg) { if (Buffer.isBuffer(arg)) { return true; } - return '[object Object]' == toString.call(arg); + return Object.prototype.toString.call(arg) === '[object Object]'; }; /*! @@ -448,7 +524,9 @@ exports.args = sliced; */ exports.tick = function tick(callback) { - if ('function' !== typeof callback) return; + if (typeof callback !== 'function') { + return; + } return function() { try { callback.apply(this, arguments); @@ -477,8 +555,8 @@ exports.isMongooseObject = function(v) { MongooseBuffer || (MongooseBuffer = require('./types').Buffer); return v instanceof Document || - (v && v.isMongooseArray) || - (v && v.isMongooseBuffer); + (v && v.isMongooseArray) || + (v && v.isMongooseBuffer); }; var isMongooseObject = exports.isMongooseObject; @@ -490,11 +568,15 @@ var isMongooseObject = exports.isMongooseObject; */ exports.expires = function expires(object) { - if (!(object && 'Object' == object.constructor.name)) return; - if (!('expires' in object)) return; + if (!(object && object.constructor.name === 'Object')) { + return; + } + if (!('expires' in object)) { + return; + } var when; - if ('string' != typeof object.expires) { + if (typeof object.expires !== 'string') { when = object.expires; } else { when = Math.round(ms(object.expires) / 1000); @@ -535,7 +617,7 @@ exports.populate = function populate(path, select, model, match, options, subPop // an array, string, or object literal). // might have passed an object specifying all arguments - if (1 === arguments.length) { + if (arguments.length === 1) { if (path instanceof PopulateOptions) { return [path]; } @@ -554,13 +636,13 @@ exports.populate = function populate(path, select, model, match, options, subPop subPopulate = path.populate; path = path.path; } - } else if ('string' !== typeof model && 'function' !== typeof model) { + } else if (typeof model !== 'string' && typeof model !== 'function') { options = match; match = model; model = undefined; } - if ('string' != typeof path) { + if (typeof path !== 'string') { throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`'); } @@ -570,6 +652,7 @@ exports.populate = function populate(path, select, model, match, options, subPop var ret = []; var paths = path.split(' '); + options = exports.clone(options, { retainKeyOrder: true }); for (var i = 0; i < paths.length; ++i) { ret.push(new PopulateOptions(paths[i], select, match, options, model, subPopulate)); } @@ -646,7 +729,7 @@ exports.object.hasOwnProperty = function(obj, prop) { */ exports.isNullOrUndefined = function(val) { - return null == val; + return val === null || val === undefined; }; /*! @@ -729,11 +812,19 @@ exports.array.unique = function(arr) { exports.buffer = {}; exports.buffer.areEqual = function(a, b) { - if (!Buffer.isBuffer(a)) return false; - if (!Buffer.isBuffer(b)) return false; - if (a.length !== b.length) return false; + if (!Buffer.isBuffer(a)) { + return false; + } + if (!Buffer.isBuffer(b)) { + return false; + } + if (a.length !== b.length) { + return false; + } for (var i = 0, len = a.length; i < len; ++i) { - if (a[i] !== b[i]) return false; + if (a[i] !== b[i]) { + return false; + } } return true; }; @@ -755,28 +846,42 @@ exports.decorate = function(destination, source) { * merges to with a copy of from * * @param {Object} to - * @param {Object} from + * @param {Object} fromObj * @api private */ -exports.mergeClone = function(to, from) { - var keys = Object.keys(from), - i = keys.length, - key; +exports.mergeClone = function(to, fromObj) { + var keys = Object.keys(fromObj); + var len = keys.length; + var i = 0; + var key; - while (i--) { - key = keys[i]; - if ('undefined' === typeof to[key]) { + while (i < len) { + key = keys[i++]; + if (typeof to[key] === 'undefined') { // make sure to retain key order here because of a bug handling the $each // operator in mongodb 2.4.4 - to[key] = exports.clone(from[key], { retainKeyOrder : 1}); + to[key] = exports.clone(fromObj[key], { + retainKeyOrder: 1, + flattenDecimals: false + }); } else { - if (exports.isObject(from[key])) { - exports.mergeClone(to[key], from[key]); + if (exports.isObject(fromObj[key])) { + var obj = fromObj[key]; + if (isMongooseObject(fromObj[key]) && !fromObj[key].isMongooseBuffer) { + obj = obj.toObject({ transform: false, virtuals: false }); + } + if (fromObj[key].isMongooseBuffer) { + obj = new Buffer(obj); + } + exports.mergeClone(to[key], obj); } else { // make sure to retain key order here because of a bug handling the // $each operator in mongodb 2.4.4 - to[key] = exports.clone(from[key], { retainKeyOrder : 1}); + to[key] = exports.clone(fromObj[key], { + retainKeyOrder: 1, + flattenDecimals: false + }); } } } @@ -795,3 +900,9 @@ exports.each = function(arr, fn) { fn(arr[i]); } }; + +/*! + * ignore + */ + +exports.noop = function() {}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..26bd7546d2f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3355 @@ +{ + "name": "mongoose", + "version": "4.13.20", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-es7-plugin": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz", + "integrity": "sha1-8u4fMiipDurRJF+asZIusucdM2s=", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "acquit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/acquit/-/acquit-0.4.1.tgz", + "integrity": "sha1-hGur7RTm5JpkqgiFuYpbqaJHd70=", + "dev": true, + "requires": { + "commander": "2.5.0", + "esprima": "https://github.com/ariya/esprima/archive/85fc2f4b6ad109a86d80d9821f52b5b38d0105c0.tar.gz", + "marked": "0.3.5", + "underscore": "1.5.2" + }, + "dependencies": { + "marked": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.5.tgz", + "integrity": "sha1-QROhWsXXvKFYpargciRYe5+hW5Q=", + "dev": true + } + } + }, + "acquit-ignore": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/acquit-ignore/-/acquit-ignore-0.0.3.tgz", + "integrity": "sha1-U20adFyqaGf/oILwlJmvzjIn3vo=", + "dev": true + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "optional": true + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "requires": { + "lodash": "^4.14.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + } + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + } + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + }, + "dependencies": { + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "benchmark": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.2.tgz", + "integrity": "sha1-BnbYLlYNgtLzF/gs8IWEg5Vae/4=", + "dev": true, + "requires": { + "lodash": "^4.16.4", + "platform": "^1.3.1" + } + }, + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "bson": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz", + "integrity": "sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "call-signature": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/call-signature/-/call-signature-0.0.2.tgz", + "integrity": "sha1-qEq8glpV70yysCi9dOIFpluaSZY=", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "coffeescript": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.12.7.tgz", + "integrity": "sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "commander": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.5.0.tgz", + "integrity": "sha1-13e2pNhH1CPl1HXahkKUrB/1qp0=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "diff-match-patch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz", + "integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg==", + "dev": true + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "dox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/dox/-/dox-0.3.1.tgz", + "integrity": "sha1-38RsSojRvLFTp4Q3H5OtN2ukmXs=", + "dev": true, + "requires": { + "commander": "0.6.1", + "github-flavored-markdown": ">= 0.0.1" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + } + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "empower": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/empower/-/empower-1.3.1.tgz", + "integrity": "sha512-uB6/ViBaawOO/uujFADTK3SqdYlxYNn+N4usK9MRKZ4Hbn/1QSy8k2PezxCA2/+JGbF8vd/eOfghZ90oOSDZCA==", + "dev": true, + "requires": { + "core-js": "^2.0.0", + "empower-core": "^1.2.0" + } + }, + "empower-core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/empower-core/-/empower-core-1.2.0.tgz", + "integrity": "sha512-g6+K6Geyc1o6FdXs9HwrXleCFan7d66G5xSCfSF7x1mJDCes6t0om9lFQG3zOrzh3Bkb/45N0cZ5Gqsf7YrzGQ==", + "dev": true, + "requires": { + "call-signature": "0.0.2", + "core-js": "^2.0.0" + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-promise": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", + "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + }, + "dependencies": { + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + } + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + } + } + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.4.0.tgz", + "integrity": "sha1-v7OO/Lf5mBiApyS8LmHSFAku46k=", + "dev": true, + "requires": { + "chalk": "^1.0.0", + "concat-stream": "^1.4.6", + "debug": "^2.1.1", + "doctrine": "^1.2.0", + "es6-map": "^0.1.3", + "escope": "^3.6.0", + "espree": "^3.1.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^1.1.1", + "glob": "^6.0.4", + "globals": "^8.18.0", + "ignore": "^2.2.19", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "optionator": "^0.8.1", + "path-is-absolute": "^1.0.0", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "resolve": "^1.1.6", + "shelljs": "^0.5.3", + "strip-json-comments": "~1.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.0.0" + } + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "esprima": { + "version": "https://github.com/ariya/esprima/archive/85fc2f4b6ad109a86d80d9821f52b5b38d0105c0.tar.gz", + "integrity": "sha512-GyWe6YkK295gEHgCUfRdjb9BWkxWrf3zAB6AvUGhSmW4HLCOw0Zq6cY3eSKCQQk6kaimdNQFssPUY5t8cz2SdQ==", + "dev": true + }, + "espurify": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/espurify/-/espurify-1.8.1.tgz", + "integrity": "sha512-ZDko6eY/o+D/gHCWyHTU85mKDgYcS4FJj7S+YD6WIInm7GQ6AnOjmcL4+buFV/JOztVLELi/7MmuGU5NHta0Mg==", + "dev": true, + "requires": { + "core-js": "^2.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "dev": true, + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==", + "dev": true + } + } + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-entry-cache": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", + "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "fileset": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.2.1.tgz", + "integrity": "sha1-WI74lzxmI7KnbfRlEFaWuWqsgGc=", + "dev": true, + "requires": { + "glob": "5.x", + "minimatch": "2.x" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "^1.0.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "github-flavored-markdown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/github-flavored-markdown/-/github-flavored-markdown-1.0.1.tgz", + "integrity": "sha1-kzYbh6McJXkNnIGht5ghSnN+qzg=", + "dev": true + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-8.18.0.tgz", + "integrity": "sha1-k9SmK9ysOM+vr8R9awNHaMsP/LQ=", + "dev": true + }, + "globule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.0.tgz", + "integrity": "sha512-YlD4kdMqRCQHrhVdonet4TdRtv1/sZKepvoxNT4Nrhrp5HI8XFfc8kFlGlBn2myBo80aGp8Eft259mbcUJhgSg==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "handlebars": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "uglify-js": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.4.tgz", + "integrity": "sha512-tinYWE8X1QfCHxS1lBS8yiDekyhSXOO6R66yNOCdUJeojxxw+PX2BHAz/BWyW7PQ7pkiWVxJfIEbiDxyLWvUGg==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.20.3", + "source-map": "~0.6.1" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "highlight.js": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-7.0.1.tgz", + "integrity": "sha1-QuRhuYoNPE+QzQtmg2TGXAi8bdA=", + "dev": true + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "hooks-fixed": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.2.tgz", + "integrity": "sha512-YurCM4gQSetcrhwEtpQHhQ4M7Zo7poNGqY4kQGeBS6eZtOcT3tnNs01ThFa0jYBByAiYt1MjMjP/YApG0EnAvQ==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "2.2.19", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-2.2.19.tgz", + "integrity": "sha1-TIRaYfflC0pBD2FWqqOLatleDI8=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.0.tgz", + "integrity": "sha512-XTHBZSIIxNsIsZXg7XB5l8z/OBFosl1Wao4tXLpeC7eKU4Vm/kdop2azkPqULwnfGQjmeDIyey9g7afMMtdWAA==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.4.tgz", + "integrity": "sha1-6M9xjf7bcTyDNKuf+t418QQtKlY=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "fileset": "0.2.x", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "jasmine-growl-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jasmine-growl-reporter/-/jasmine-growl-reporter-2.0.0.tgz", + "integrity": "sha512-RYwVfPaGgxQQSHDOt6jQ99/KAkFQ/Fiwg/AzBS+uO9A4UhGhxb7hwXaUUSU/Zs0MxBoFNqmIRC+7P4/+5O3lXg==", + "dev": true, + "requires": { + "growl": "^1.10.5" + }, + "dependencies": { + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + } + } + }, + "jasmine-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jasmine-node/-/jasmine-node-3.0.0.tgz", + "integrity": "sha512-vUa5Q7bQYwHHqi6FlJYndiKqZp+d+c3MKe0QUMwwrC4JRmoRV3zkg0buxB/uQ6qLh0NO34TNstpAnvaZ6xGlAA==", + "dev": true, + "requires": { + "coffeescript": "~1.12.7", + "gaze": "~1.1.2", + "jasmine-growl-reporter": "~2.0.0", + "jasmine-reporters": "~1.0.0", + "mkdirp": "~0.3.5", + "requirejs": "~2.3.6", + "underscore": "~1.9.1", + "walkdir": "~0.0.12" + }, + "dependencies": { + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", + "dev": true + }, + "underscore": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz", + "integrity": "sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==", + "dev": true + } + } + }, + "jasmine-reporters": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-1.0.2.tgz", + "integrity": "sha1-q2E+1Zd9x0h+hbPBL2qOqNsq3jE=", + "dev": true, + "requires": { + "mkdirp": "~0.3.5" + }, + "dependencies": { + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", + "dev": true + } + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "kareem": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz", + "integrity": "sha1-4+QQHZ3P3imXadr0tNtk2JXRdEg=" + }, + "kerberos": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.17.tgz", + "integrity": "sha1-KaZ8CxJ7+lK907U7e4yGWamghPg=", + "dev": true, + "requires": { + "nan": "~2.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" + } + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "markdown": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/markdown/-/markdown-0.5.0.tgz", + "integrity": "sha1-KCBbVlqK51kt4gdGPWY33BgnIrI=", + "dev": true, + "requires": { + "nopt": "~2.1.1" + }, + "dependencies": { + "nopt": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz", + "integrity": "sha1-bMzZd7gBMqB3MdbozljCyDA8+a8=", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, + "marked": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz", + "integrity": "sha512-nW5u0dxpXxHfkHzzrveY45gCbi+R4PaO4WRZYqZNl+vB0hVGeqlFn0aOg1c8AKL63TrNFn9Bm2UP4AdiZ9TPLw==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "dev": true, + "requires": { + "mime-db": "1.43.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.2.0.tgz", + "integrity": "sha1-fcT0XlCIB1FxpoiWgU5q6et6heM=", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.5", + "glob": "7.0.5", + "growl": "1.9.2", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "glob": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", + "integrity": "sha1-tCAqaQmbu00pKnwblbZoK2fr3JU=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "mongodb": { + "version": "2.2.34", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.34.tgz", + "integrity": "sha1-o09Zu+thdUrsQy3nLD/iFSakTBo=", + "requires": { + "es6-promise": "3.2.1", + "mongodb-core": "2.1.18", + "readable-stream": "2.2.7" + } + }, + "mongodb-core": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.18.tgz", + "integrity": "sha1-TEYTm986HwMt7ZHbSfOO7AFlkFA=", + "requires": { + "bson": "~1.0.4", + "require_optional": "~1.0.0" + } + }, + "mongodb-topology-manager": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/mongodb-topology-manager/-/mongodb-topology-manager-1.0.11.tgz", + "integrity": "sha1-GXDHRbhe36SAEQRaj+bQP2W1ofA=", + "dev": true, + "requires": { + "babel-core": "^6.10.4", + "babel-polyfill": "^6.9.1", + "bluebird": "^3.4.1", + "co": "^4.6.0", + "es6-promise": "^3.2.1", + "kerberos": "0.0.17", + "mkdirp": "^0.5.1", + "mongodb-core": "^1.2.24", + "rimraf": "^2.4.3" + }, + "dependencies": { + "bson": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz", + "integrity": "sha1-5louPHUH/63kEJvHV1p25Q+NqRU=", + "dev": true + }, + "mongodb-core": { + "version": "1.3.21", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.3.21.tgz", + "integrity": "sha1-/hKee+4rOybBQJ3gKrYNA/YpHMo=", + "dev": true, + "requires": { + "bson": "~0.4.23", + "require_optional": "~1.0.0" + } + } + } + }, + "mpath": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.5.1.tgz", + "integrity": "sha512-H8OVQ+QEz82sch4wbODFOz+3YQ61FYz/z3eJ5pIdbMEaUzDqA268Wd+Vt4Paw9TJfvDgVKaayC0gBzMIw2jhsg==" + }, + "mpromise": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", + "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" + }, + "mquery": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.3.tgz", + "integrity": "sha512-NC8L14kn+qxJbbJ1gbcEMDxF0sC3sv+1cbRReXXwVvowcwY1y9KoVZFq0ebwARibsadu8lx8nWGvm3V0Pf0ZWQ==", + "requires": { + "bluebird": "3.5.0", + "debug": "2.6.9", + "regexp-clone": "0.0.1", + "sliced": "0.0.5" + }, + "dependencies": { + "sliced": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", + "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "muri": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/muri/-/muri-1.3.0.tgz", + "integrity": "sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg==" + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "nan": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.0.9.tgz", + "integrity": "sha1-0Cp3D0Z3iELM65ThfKsx/8cjSgU=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "node-static": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.7.tgz", + "integrity": "sha1-kwgdg02b2dg3N3mN89o81TjokIo=", + "dev": true, + "requires": { + "colors": ">=0.6.0", + "mime": ">=1.2.9", + "optimist": ">=0.3.4" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "nsp": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/nsp/-/nsp-2.8.1.tgz", + "integrity": "sha512-jvjDg2Gsw4coD/iZ5eQddsDlkvnwMCNnpG05BproSnuG+Gr1bSQMwWMcQeYje+qdDl3XznmhblMPLpZLecTORQ==", + "dev": true, + "requires": { + "chalk": "^1.1.1", + "cli-table": "^0.3.1", + "cvss": "^1.0.0", + "https-proxy-agent": "^1.0.0", + "joi": "^6.9.1", + "nodesecurity-npm-utils": "^5.0.0", + "path-is-absolute": "^1.0.0", + "rc": "^1.1.2", + "semver": "^5.0.3", + "subcommand": "^2.0.3", + "wreck": "^6.3.0" + }, + "dependencies": { + "agent-base": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz", + "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=", + "dev": true, + "requires": { + "extend": "~3.0.0", + "semver": "~5.0.1" + }, + "dependencies": { + "semver": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", + "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", + "dev": true + } + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "dev": true, + "requires": { + "colors": "1.0.3" + } + }, + "cliclopts": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cliclopts/-/cliclopts-1.1.1.tgz", + "integrity": "sha1-aUMcfLWvcjd0sNORG0w3USQxkQ8=", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + }, + "cvss": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cvss/-/cvss-1.0.2.tgz", + "integrity": "sha1-32fpK/EqeW9J6Sh5nI2zunS5/NY=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "https-proxy-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz", + "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=", + "dev": true, + "requires": { + "agent-base": "2", + "debug": "2", + "extend": "3" + } + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true + }, + "isemail": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", + "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=", + "dev": true + }, + "joi": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", + "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", + "dev": true, + "requires": { + "hoek": "2.x.x", + "isemail": "1.x.x", + "moment": "2.x.x", + "topo": "1.x.x" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "moment": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", + "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nodesecurity-npm-utils": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nodesecurity-npm-utils/-/nodesecurity-npm-utils-5.0.0.tgz", + "integrity": "sha1-Baow3jDKjIRcQEjpT9eOXgi1Xtk=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "rc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "dev": true, + "requires": { + "deep-extend": "~0.4.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "subcommand": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/subcommand/-/subcommand-2.1.0.tgz", + "integrity": "sha1-XkzspaN3njNlsVEeBfhmh3MC92A=", + "dev": true, + "requires": { + "cliclopts": "^1.1.0", + "debug": "^2.1.3", + "minimist": "^1.2.0", + "xtend": "^4.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "topo": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", + "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "wreck": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/wreck/-/wreck-6.3.0.tgz", + "integrity": "sha1-oTaXafB7u2LWo3gzanhx/Hc8dAs=", + "dev": true, + "requires": { + "boom": "2.x.x", + "hoek": "2.x.x" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + } + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "platform": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", + "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", + "dev": true + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "power-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/power-assert/-/power-assert-1.4.1.tgz", + "integrity": "sha1-SC7gmKmHfoz6ciQshJm5PyBwnE4=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "empower": "^1.1.0", + "power-assert-formatter": "^1.3.1", + "universal-deep-strict-equal": "^1.2.1", + "xtend": "^4.0.0" + } + }, + "power-assert-context-formatter": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/power-assert-context-formatter/-/power-assert-context-formatter-1.2.0.tgz", + "integrity": "sha512-HLNEW8Bin+BFCpk/zbyKwkEu9W8/zThIStxGo7weYcFkKgMuGCHUJhvJeBGXDZf0Qm2xis4pbnnciGZiX0EpSg==", + "dev": true, + "requires": { + "core-js": "^2.0.0", + "power-assert-context-traversal": "^1.2.0" + } + }, + "power-assert-context-reducer-ast": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/power-assert-context-reducer-ast/-/power-assert-context-reducer-ast-1.2.0.tgz", + "integrity": "sha512-EgOxmZ/Lb7tw4EwSKX7ZnfC0P/qRZFEG28dx/690qvhmOJ6hgThYFm5TUWANDLK5NiNKlPBi5WekVGd2+5wPrw==", + "dev": true, + "requires": { + "acorn": "^5.0.0", + "acorn-es7-plugin": "^1.0.12", + "core-js": "^2.0.0", + "espurify": "^1.6.0", + "estraverse": "^4.2.0" + } + }, + "power-assert-context-traversal": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/power-assert-context-traversal/-/power-assert-context-traversal-1.2.0.tgz", + "integrity": "sha512-NFoHU6g2umNajiP2l4qb0BRWD773Aw9uWdWYH9EQsVwIZnog5bd2YYLFCVvaxWpwNzWeEfZIon2xtyc63026pQ==", + "dev": true, + "requires": { + "core-js": "^2.0.0", + "estraverse": "^4.1.0" + } + }, + "power-assert-formatter": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/power-assert-formatter/-/power-assert-formatter-1.4.1.tgz", + "integrity": "sha1-XcEl7VCj37HdomwZNH879Y7CiEo=", + "dev": true, + "requires": { + "core-js": "^2.0.0", + "power-assert-context-formatter": "^1.0.7", + "power-assert-context-reducer-ast": "^1.0.7", + "power-assert-renderer-assertion": "^1.0.7", + "power-assert-renderer-comparison": "^1.0.7", + "power-assert-renderer-diagram": "^1.0.7", + "power-assert-renderer-file": "^1.0.7" + } + }, + "power-assert-renderer-assertion": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.2.0.tgz", + "integrity": "sha512-3F7Q1ZLmV2ZCQv7aV7NJLNK9G7QsostrhOU7U0RhEQS/0vhEqrRg2jEJl1jtUL4ZyL2dXUlaaqrmPv5r9kRvIg==", + "dev": true, + "requires": { + "power-assert-renderer-base": "^1.1.1", + "power-assert-util-string-width": "^1.2.0" + } + }, + "power-assert-renderer-base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/power-assert-renderer-base/-/power-assert-renderer-base-1.1.1.tgz", + "integrity": "sha1-lqZQxv0F7hvB9mtUrWFELIs/Y+s=", + "dev": true + }, + "power-assert-renderer-comparison": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/power-assert-renderer-comparison/-/power-assert-renderer-comparison-1.2.0.tgz", + "integrity": "sha512-7c3RKPDBKK4E3JqdPtYRE9cM8AyX4LC4yfTvvTYyx8zSqmT5kJnXwzR0yWQLOavACllZfwrAGQzFiXPc5sWa+g==", + "dev": true, + "requires": { + "core-js": "^2.0.0", + "diff-match-patch": "^1.0.0", + "power-assert-renderer-base": "^1.1.1", + "stringifier": "^1.3.0", + "type-name": "^2.0.1" + } + }, + "power-assert-renderer-diagram": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.2.0.tgz", + "integrity": "sha512-JZ6PC+DJPQqfU6dwSmpcoD7gNnb/5U77bU5KgNwPPa+i1Pxiz6UuDeM3EUBlhZ1HvH9tMjI60anqVyi5l2oNdg==", + "dev": true, + "requires": { + "core-js": "^2.0.0", + "power-assert-renderer-base": "^1.1.1", + "power-assert-util-string-width": "^1.2.0", + "stringifier": "^1.3.0" + } + }, + "power-assert-renderer-file": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/power-assert-renderer-file/-/power-assert-renderer-file-1.2.0.tgz", + "integrity": "sha512-/oaVrRbeOtGoyyd7e4IdLP/jIIUFJdqJtsYzP9/88R39CMnfF/S/rUc8ZQalENfUfQ/wQHu+XZYRMaCEZmEesg==", + "dev": true, + "requires": { + "power-assert-renderer-base": "^1.1.1" + } + }, + "power-assert-util-string-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/power-assert-util-string-width/-/power-assert-util-string-width-1.2.0.tgz", + "integrity": "sha512-lX90G0igAW0iyORTILZ/QjZWsa1MZ6VVY3L0K86e2eKun3S4LKPH4xZIl8fdeMYLfOjkaszbNSzf1uugLeAm2A==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", + "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", + "requires": { + "buffer-shims": "~1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regexp-clone": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", + "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + } + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "requirejs": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", + "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", + "dev": true + }, + "resolve": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.14.2.tgz", + "integrity": "sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "^1.3.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "shelljs": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.5.3.tgz", + "integrity": "sha1-xUmCuZbHbvDB5rWfvcWCX1txMRM=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "stringifier": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/stringifier/-/stringifier-1.4.0.tgz", + "integrity": "sha512-cNsMOqqrcbLcHTXEVmkw9y0fwDwkdgtZwlfyolzpQDoAE1xdNGhQhxBUfiDvvZIKl1hnUEgMv66nHwtMz3OjPw==", + "dev": true, + "requires": { + "core-js": "^2.0.0", + "traverse": "^0.6.6", + "type-name": "^2.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", + "slice-ansi": "0.0.4", + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "tbd": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/tbd/-/tbd-0.6.4.tgz", + "integrity": "sha1-btWic3ZPhu0mjJYeG9TVyk0N5tQ=", + "dev": true, + "requires": { + "express": ">=2.5.0", + "jade": ">=0.18.0", + "jasmine-node": ">=1.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "type-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/type-name/-/type-name-2.0.2.tgz", + "integrity": "sha1-7+fUEj2KxSr/9/QMfk3sUmYAj7Q=", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.0.tgz", + "integrity": "sha1-8CHji6LKdAhg9b1caVwqgXNF8Ow=", + "dev": true, + "requires": { + "async": "~0.2.6", + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true + }, + "underscore": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", + "integrity": "sha1-EzXF5PXm0zu7SwBrqMhqAPVW3gg=", + "dev": true + }, + "universal-deep-strict-equal": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/universal-deep-strict-equal/-/universal-deep-strict-equal-1.2.2.tgz", + "integrity": "sha1-DaSsL3PP95JMgfpN4BjKViyisKc=", + "dev": true, + "requires": { + "array-filter": "^1.0.0", + "indexof": "0.0.1", + "object-keys": "^1.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true + }, + "uuid-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.0.0.tgz", + "integrity": "sha1-9GV3F2JLDkuIrzb5jYlYmlu+5Wk=", + "dev": true + }, + "validator": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-5.4.0.tgz", + "integrity": "sha1-RCDdyf3CCU2QS4UwPhtKE4+E5x4=", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "walkdir": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.12.tgz", + "integrity": "sha512-HFhaD4mMWPzFSqhpyDG48KDdrjfn409YQuVW7ckZYhW4sE87mYtWifdB/+73RA7+p4s4K18n5Jfx1kHthE1gBw==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/package.json b/package.json index ad3238ab418..006b5373612 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.2.4", + "version": "4.13.20", "author": "Guillermo Rauch ", "keywords": [ "mongodb", @@ -19,42 +19,49 @@ ], "license": "MIT", "dependencies": { - "async": "0.9.0", - "bson": "~0.4.18", - "hooks-fixed": "1.1.0", - "kareem": "1.0.1", - "mongodb": "2.0.46", - "mpath": "0.1.1", - "mpromise": "0.5.4", - "mquery": "1.6.3", - "ms": "0.7.1", - "muri": "1.0.0", + "async": "2.6.0", + "bson": "~1.0.4", + "hooks-fixed": "2.0.2", + "kareem": "1.5.0", + "lodash.get": "4.4.2", + "mongodb": "2.2.34", + "mpath": "0.5.1", + "mpromise": "0.5.5", + "mquery": "2.3.3", + "ms": "2.0.0", + "muri": "1.3.0", "regexp-clone": "0.0.1", - "sliced": "0.0.5" + "sliced": "1.0.1" }, "devDependencies": { - "acquit": "0.3.0", - "acquit-ignore": "0.0.1", - "benchmark": "1.0.0", - "bluebird": "2.9.34", - "co": "3.1.0", + "acquit": "0.4.1", + "acquit-ignore": "0.0.3", + "benchmark": "2.1.2", + "bluebird": "3.5.0", + "co": "4.6.0", "dox": "0.3.1", - "eslint": "1.7.3", + "eslint": "2.4.0", "highlight.js": "7.0.1", - "istanbul": "^0.3.13", + "istanbul": "0.4.4", "jade": "0.26.3", - "markdown": "0.3.1", - "marked": "0.3.2", - "mocha": "1.20.0", - "node-static": "0.5.9", - "open": "0.0.3", - "q": "1.4.1", + "lodash": "4.17.10", + "markdown": "0.5.0", + "marked": "0.3.9", + "mocha": "3.2.0", + "mongodb-topology-manager": "1.0.11", + "node-static": "0.7.7", + "nsp": "~2.8.1", + "power-assert": "1.4.1", + "q": "1.5.1", "tbd": "0.6.4", - "uglify-js": "2.4.24", - "underscore": "1.5.2" + "uglify-js": "2.7.0", + "uuid": "2.0.3", + "uuid-parse": "1.0.0", + "validator": "5.4.0" }, "browserDependencies": { "browserify": "4.1.10", + "chai": "3.5.0", "karma": "0.12.16", "karma-chai": "0.1.0", "karma-mocha": "0.1.4", @@ -65,9 +72,14 @@ "lib": "./lib/mongoose" }, "scripts": { + "fix-lint": "eslint . --fix", "install-browser": "npm install `node format_deps.js`", - "test": "mocha --async-only test/*.test.js test/**/*.test.js", - "test-cov": "istanbul cover _mocha --async-only test/*.test.js" + "lint": "eslint . --quiet", + "nsp": "nsp check", + "release": "git push origin master --tags && npm publish", + "release-legacy": "git push origin 4.x --tags && npm publish --tag legacy", + "test": "mocha test/*.test.js test/**/*.test.js", + "test-cov": "istanbul cover --report text --report html _mocha test/*.test.js" }, "main": "./index.js", "engines": { diff --git a/release-items.md b/release-items.md index 3dc270e908e..12ab9a5daca 100644 --- a/release-items.md +++ b/release-items.md @@ -1,29 +1,29 @@ ## mongoose release procedure 1. tests must pass -2. update package.json version -3. update History.md using `git changelog` or similar. list the related ticket(s) # as well as a link to the github user who fixed it if applicable. -4. git commit -m 'release x.x.x' +2. update `package.json` and `package-lock.json` version +3. update History.md using `git changelog` or similar. Add # as well as a link to the github user who fixed it if applicable. +4. git commit -a -m 'release x.x.x' 5. git tag x.x.x -6. git push origin BRANCH --tags && npm publish +6. `npm run release`, or `npm run release-legacy` for 4.x 7. update mongoosejs.com (see "updating the website" below) -8. announce to google groups - include the relevant change log and links to issues -9. tweet google group announcement from [@mongoosejs](https://twitter.com/mongoosejs) +8. tweet changelog link from [@mongoosejs](https://twitter.com/mongoosejs) +9. Announce on mongoosejsteam slack channel 10. change package.json version to next patch version suffixed with '-pre' and commit "now working on x.x.x" 11. if this is a legacy release, `git merge` changes into master. ## updating the website -For 4.x +For 5.x 0. Change to the master branch 1. execute `make docs` (when this process completes you'll be on the gh-pages branch) -2. `git commit -a -m 'website; regen <4.x.x>'` +2. `git commit -a -m 'chore: website 5.x.x'` 3. `git push origin gh-pages` -For 3.8.x: +For 4.x -0. Change to the 3.8.x branch +0. Change to the 4.x branch 1. execute `make docs_legacy` (when this process completes you'll be on the gh-pages branch) -2. `git commit -a -m 'website; regen '` +2. `git commit -a -m 'chore: website 4.x.x'` 3. `git push origin gh-pages` diff --git a/static.js b/static.js index 113d06ffff7..d84e59f7a3a 100644 --- a/static.js +++ b/static.js @@ -1,10 +1,9 @@ var static = require('node-static'); -var server = new static.Server('.', { cache: 0 }); -var open = require('open'); +var server = new static.Server('.', {cache: 0}); require('http').createServer(function(req, res) { - if ('/favicon.ico' == req.url) { + if (req.url === '/favicon.ico') { req.destroy(); res.statusCode = 204; return res.end(); @@ -23,4 +22,3 @@ require('http').createServer(function(req, res) { }).listen(8088); console.error('now listening on http://localhost:8088'); -open('http://localhost:8088'); diff --git a/test/aggregate.test.js b/test/aggregate.test.js index 11be81327ef..0e36da861ec 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -2,11 +2,11 @@ * Module dependencies */ -var start = require('./common') - , Aggregate = require('../lib/aggregate') - , mongoose = start.mongoose - , Schema = mongoose.Schema - , assert = require('assert'); +var start = require('./common'); +var Aggregate = require('../lib/aggregate'); +var mongoose = start.mongoose; +var Schema = mongoose.Schema; +var assert = require('power-assert'); /** * Test data @@ -16,21 +16,22 @@ var EmployeeSchema = new Schema({ name: String, sal: Number, dept: String, - customers: [String] + customers: [String], + reportsTo: String }); mongoose.model('Employee', EmployeeSchema); function setupData(callback) { - var saved = 0 - , emps = [ - { name: "Alice", sal: 18000, dept: "sales", customers: [ 'Eve', 'Fred' ] } - , { name: "Bob", sal: 15000, dept: "sales", customers: [ 'Gary', 'Herbert', 'Isaac' ] } - , { name: "Carol", sal: 14000, dept: "r&d" } - , { name: "Dave", sal: 14500, dept: "r&d" } - ] - , db = start() - , Employee = db.model('Employee'); + var saved = 0; + var emps = [ + { name: 'Alice', sal: 18000, dept: 'sales', customers: ['Eve', 'Fred'] }, + { name: 'Bob', sal: 15000, dept: 'sales', customers: ['Gary', 'Herbert', 'Isaac'], reportsTo: 'Alice' }, + { name: 'Carol', sal: 14000, dept: 'r&d', reportsTo: 'Bob' }, + { name: 'Dave', sal: 14500, dept: 'r&d', reportsTo: 'Carol' } + ]; + var db = start(); + var Employee = db.model('Employee'); emps.forEach(function(data) { var emp = new Employee(data); @@ -43,9 +44,24 @@ function setupData(callback) { }); } -function clearData(db, callback) { - db.model('Employee').remove(function() { - db.close(callback); +/** + * Helper function to test operators that only work in MongoDB 3.4 and above (such as some aggregation pipeline operators) + * + * @param {Object} ctx, `this`, so that mocha tests can be skipped + * @param {Function} done + * @return {Void} + */ +function onlyTestMongo34(ctx, done) { + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + ctx.skip(); + } + done(); }); } @@ -67,22 +83,38 @@ describe('aggregate: ', function() { done(); }); + it('supports array as single argument', function(done) { + var aggregate = new Aggregate(); + + assert.equal(aggregate.append([{ $a: 1 }, { $b: 2 }, { $c: 3 }]), aggregate); + assert.deepEqual(aggregate._pipeline, [{ $a: 1 }, { $b: 2 }, { $c: 3 }]); + + aggregate.append([{ $d: 4 }, { $c: 5 }]); + assert.deepEqual(aggregate._pipeline, [{ $a: 1 }, { $b: 2 }, { $c: 3 }, { $d: 4 }, { $c: 5 }]); + + done(); + }); + it('throws if non-operator parameter is passed', function(done) { - var aggregate = new Aggregate() - , regexp = /Arguments must be aggregate pipeline operators/; + var aggregate = new Aggregate(); + var regexp = /Arguments must be aggregate pipeline operators/; assert.throws(function() { - aggregate.append({ $a: 1 }, "string"); + aggregate.append({ $a: 1 }, 'string'); }, regexp); assert.throws(function() { - aggregate.append({ $a: 1 }, ["array"]); + aggregate.append({ $a: 1 }, ['array']); }, regexp); assert.throws(function() { aggregate.append({ $a: 1 }, { a: 1 }); }, regexp); + assert.throws(function() { + aggregate.append([{ $a: 1 }, { a: 1 }]); + }, regexp); + done(); }); @@ -96,6 +128,16 @@ describe('aggregate: ', function() { done(); }); + it('does not throw when empty array is passed as single argument', function(done) { + var aggregate = new Aggregate(); + + assert.doesNotThrow(function() { + aggregate.append([]); + }); + + done(); + }); + it('called from constructor', function(done) { var aggregate = new Aggregate({ $a: 1 }, { $b: 2 }, { $c: 3 }); assert.deepEqual(aggregate._pipeline, [{ $a: 1 }, { $b: 2 }, { $c: 3 }]); @@ -119,10 +161,10 @@ describe('aggregate: ', function() { it('(string)', function(done) { var aggregate = new Aggregate(); - aggregate.project(" a b -c "); + aggregate.project(' a b -c '); assert.deepEqual(aggregate._pipeline, [{ $project: { a: 1, b: 1, c: 0 } }]); - aggregate.project("b"); + aggregate.project('b'); assert.deepEqual(aggregate._pipeline, [{ $project: { a: 1, b: 1, c: 0 } }, { $project: { b: 1 } }]); done(); @@ -131,7 +173,7 @@ describe('aggregate: ', function() { it('("a","b","c")', function(done) { assert.throws(function() { var aggregate = new Aggregate(); - aggregate.project("a", "b", "c"); + aggregate.project('a', 'b', 'c'); }, /Invalid project/); done(); @@ -140,7 +182,7 @@ describe('aggregate: ', function() { it('["a","b","c"]', function(done) { assert.throws(function() { var aggregate = new Aggregate(); - aggregate.project(["a", "b", "c"]); + aggregate.project(['a', 'b', 'c']); }, /Invalid project/); done(); @@ -193,15 +235,15 @@ describe('aggregate: ', function() { it('("field")', function(done) { var aggregate = new Aggregate(); - assert.equal(aggregate.unwind("field"), aggregate); - assert.deepEqual(aggregate._pipeline, [{ $unwind: "$field" }]); + assert.equal(aggregate.unwind('field'), aggregate); + assert.deepEqual(aggregate._pipeline, [{ $unwind: '$field' }]); - aggregate.unwind("a", "b", "c"); + aggregate.unwind('a', 'b', 'c'); assert.deepEqual(aggregate._pipeline, [ - { $unwind: "$field" } - , { $unwind: "$a" } - , { $unwind: "$b" } - , { $unwind: "$c" } + { $unwind: '$field' }, + { $unwind: '$a' }, + { $unwind: '$b' }, + { $unwind: '$c' } ]); done(); @@ -238,10 +280,10 @@ describe('aggregate: ', function() { it('(string)', function(done) { var aggregate = new Aggregate(); - aggregate.sort(" a b -c "); + aggregate.sort(' a b -c '); assert.deepEqual(aggregate._pipeline, [{ $sort: { a: 1, b: 1, c: -1 } }]); - aggregate.sort("b"); + aggregate.sort('b'); assert.deepEqual(aggregate._pipeline, [{ $sort: { a: 1, b: 1, c: -1 } }, { $sort: { b: 1 } }]); done(); @@ -250,7 +292,7 @@ describe('aggregate: ', function() { it('("a","b","c")', function(done) { assert.throws(function() { var aggregate = new Aggregate(); - aggregate.sort("a", "b", "c"); + aggregate.sort('a', 'b', 'c'); }, /Invalid sort/); done(); @@ -259,7 +301,7 @@ describe('aggregate: ', function() { it('["a","b","c"]', function(done) { assert.throws(function() { var aggregate = new Aggregate(); - aggregate.sort(["a", "b", "c"]); + aggregate.sort(['a', 'b', 'c']); }, /Invalid sort/); done(); @@ -295,9 +337,7 @@ describe('aggregate: ', function() { assert.equal(aggregate.near({ a: 1 }), aggregate); // Run exec so we apply discriminator pipeline - assert.throws(function() { - aggregate.exec(); - }, /Cannot read property 'aggregate' of undefined|Cannot call method 'aggregate' of undefined/); + Aggregate._prepareDiscriminatorPipeline(aggregate); assert.deepEqual(aggregate._pipeline, [{ $geoNear: { a: 1, query: { __t: 'subschema' } } }]); @@ -305,9 +345,7 @@ describe('aggregate: ', function() { aggregate._model = stub; aggregate.near({ b: 2, query: { x: 1 } }); - assert.throws(function() { - aggregate.exec(); - }, /Cannot read property 'aggregate' of undefined|Cannot call method 'aggregate' of undefined/); + Aggregate._prepareDiscriminatorPipeline(aggregate); assert.deepEqual(aggregate._pipeline, [{ $geoNear: { b: 2, query: { x: 1, __t: 'subschema' } } }]); @@ -315,10 +353,40 @@ describe('aggregate: ', function() { }); }); + describe('lookup', function() { + it('works', function(done) { + var aggregate = new Aggregate(); + var obj = { + from: 'users', + localField: 'userId', + foreignField: '_id', + as: 'users' + }; + + aggregate.lookup(obj); + + assert.equal(aggregate._pipeline.length, 1); + assert.deepEqual(aggregate._pipeline[0].$lookup, obj); + done(); + }); + }); + + describe('sample', function() { + it('works', function(done) { + var aggregate = new Aggregate(); + + aggregate.sample(3); + + assert.equal(aggregate._pipeline.length, 1); + assert.deepEqual(aggregate._pipeline[0].$sample, { size: 3 }); + done(); + }); + }); + describe('bind', function() { it('works', function(done) { - var aggregate = new Aggregate() - , model = { foo: 42 }; + var aggregate = new Aggregate(); + var model = { foo: 42 }; assert.equal(aggregate.model(model), aggregate); assert.equal(aggregate._model, model); @@ -327,205 +395,449 @@ describe('aggregate: ', function() { }); }); + describe('Mongo 3.4 operators', function() { + before(function(done) { + onlyTestMongo34(this, done); + }); + + describe('graphLookup', function() { + it('works', function(done) { + var aggregate = new Aggregate(); + aggregate.graphLookup({ + startWith: '$test', + from: 'sourceCollection', + connectFromField: 'testFromField', + connectToField: '_id' + }); + + assert.equal(aggregate._pipeline.length, 1); + assert.deepEqual(aggregate._pipeline[0].$graphLookup, { + startWith: '$test', + from: 'sourceCollection', + connectFromField: 'testFromField', + connectToField: '_id' + }); + done(); + }); + + it('automatically prepends $ to the startWith field', function(done) { + var aggregate = new Aggregate(); + aggregate.graphLookup({ + startWith: 'test' + }); + + assert.deepEqual(aggregate._pipeline[0].$graphLookup, { + startWith: '$test' + }); + done(); + }); + + it('Throws if no options are passed to graphLookup', function(done) { + var aggregate = new Aggregate(); + try { + aggregate.graphLookup('invalid options'); + done(new Error('Should have errored')); + } catch (error) { + assert.ok(error instanceof TypeError); + done(); + } + }); + }); + + describe('addFields', function() { + it('(object)', function(done) { + var aggregate = new Aggregate(); + + assert.equal(aggregate.addFields({ a: 1, b: 1, c: 0 }), aggregate); + assert.deepEqual(aggregate._pipeline, [{ $addFields: { a: 1, b: 1, c: 0 } }]); + + aggregate.addFields({ d: {$add: ['$a','$b']} }); + assert.deepEqual(aggregate._pipeline, [{ $addFields: { a: 1, b: 1, c: 0 } }, { $addFields: { d: {$add: ['$a','$b']} } }]); + done(); + }); + }); + + describe('facet', function() { + it('works', function(done) { + var aggregate = new Aggregate(); + + aggregate.facet({ + heights: [ + // This will group documents by their `height` property + { $group: { _id: '$height', count: { $sum: 1 } } }, + // This will sort by descending height + { $sort: { count: -1, _id: -1 } } + ], + players: [ + // This will group documents by their `firstName` property + { + $group: { _id: '$firstName', count: { $sum: 1 } } + }, + // This will sort documents by their firstName descending + { $sort: { count: -1, _id: -1 } } + ] + }); + + assert.equal(aggregate._pipeline.length, 1); + assert.deepEqual(aggregate._pipeline[0].$facet, { + heights: [ + // This will group documents by their `height` property + { $group: { _id: '$height', count: { $sum: 1 } } }, + // This will sort by descending height + { $sort: { count: -1, _id: -1 } } + ], + players: [ + // This will group documents by their `firstName` property + { + $group: { + _id: '$firstName', count: { $sum: 1 } + } + }, + + // This will sort documents by their firstName descending + { + $sort: { count: -1, _id: -1 } + } + ] + }); + done(); + }); + }); + }); + describe('exec', function() { + var db; + + before(function(done) { + setupData(function(_db) { + db = _db; + done(); + }); + }); + + after(function(done) { + db.close(done); + }); + it('project', function(done) { var aggregate = new Aggregate(); - setupData(function(db) { - aggregate - .model(db.model('Employee')) - .project({ sal: 1, sal_k: { $divide: [ "$sal", 1000 ] } }) - .exec(function(err, docs) { - assert.ifError(err); - docs.forEach(function(doc) { - assert.equal(doc.sal / 1000, doc.sal_k); - }); - - clearData(db, function() { done(); }); + aggregate. + model(db.model('Employee')). + project({ sal: 1, sal_k: { $divide: ['$sal', 1000] } }). + exec(function(err, docs) { + assert.ifError(err); + docs.forEach(function(doc) { + assert.equal(doc.sal / 1000, doc.sal_k); }); - }); + + done(); + }); }); it('group', function(done) { var aggregate = new Aggregate(); - setupData(function(db) { - aggregate - .model(db.model('Employee')) - .group({ _id: "$dept" }) - .exec(function(err, docs) { - var depts; + aggregate. + model(db.model('Employee')). + group({ _id: '$dept' }). + exec(function(err, docs) { + var depts; + assert.ifError(err); + assert.equal(docs.length, 2); - assert.ifError(err); - assert.equal(docs.length, 2); - - depts = docs.map(function(doc) { return doc._id; }); - assert.notEqual(depts.indexOf("sales"), -1); - assert.notEqual(depts.indexOf("r&d"), -1); - - clearData(db, function() { done(); }); + depts = docs.map(function(doc) { + return doc._id; }); - }); + assert.notEqual(depts.indexOf('sales'), -1); + assert.notEqual(depts.indexOf('r&d'), -1); + done(); + }); }); it('skip', function(done) { var aggregate = new Aggregate(); - setupData(function(db) { - aggregate - .model(db.model('Employee')) - .skip(1) - .exec(function(err, docs) { - assert.ifError(err); - assert.equal(docs.length, 3); + aggregate. + model(db.model('Employee')). + skip(1). + exec(function(err, docs) { + assert.ifError(err); + assert.equal(docs.length, 3); - clearData(db, function() { done(); }); - }); - }); + done(); + }); }); it('limit', function(done) { var aggregate = new Aggregate(); - setupData(function(db) { - aggregate - .model(db.model('Employee')) - .limit(3) - .exec(function(err, docs) { - assert.ifError(err); - assert.equal(docs.length, 3); + aggregate. + model(db.model('Employee')). + limit(3). + exec(function(err, docs) { + assert.ifError(err); + assert.equal(docs.length, 3); - clearData(db, function() { done(); }); - }); - }); + done(); + }); }); it('unwind', function(done) { var aggregate = new Aggregate(); - setupData(function(db) { - aggregate - .model(db.model('Employee')) - .unwind('customers') - .exec(function(err, docs) { - assert.ifError(err); - assert.equal(docs.length, 5); + aggregate. + model(db.model('Employee')). + unwind('customers'). + exec(function(err, docs) { + assert.ifError(err); + assert.equal(docs.length, 5); - clearData(db, function() { done(); }); - }); - }); + done(); + }); + }); + + it('unwind with obj', function(done) { + var aggregate = new Aggregate(); + + var agg = aggregate. + model(db.model('Employee')). + unwind({ path: '$customers', preserveNullAndEmptyArrays: true }); + + assert.equal(agg._pipeline.length, 1); + assert.strictEqual(agg._pipeline[0].$unwind.preserveNullAndEmptyArrays, + true); + done(); + }); + + it('unwind throws with bad arg', function(done) { + var aggregate = new Aggregate(); + + var threw = false; + try { + aggregate. + model(db.model('Employee')). + unwind(36); + } catch (err) { + assert.ok(err.message.indexOf('to unwind()') !== -1); + threw = true; + } + assert.ok(threw); + done(); }); it('match', function(done) { var aggregate = new Aggregate(); - setupData(function(db) { - aggregate - .model(db.model('Employee')) - .match({ sal: { $gt: 15000 } }) - .exec(function(err, docs) { - assert.ifError(err); - assert.equal(docs.length, 1); + aggregate. + model(db.model('Employee')). + match({ sal: { $gt: 15000 } }). + exec(function(err, docs) { + assert.ifError(err); + assert.equal(docs.length, 1); - clearData(db, function() { done(); }); - }); - }); + done(); + }); }); it('sort', function(done) { var aggregate = new Aggregate(); - setupData(function(db) { - aggregate - .model(db.model('Employee')) - .sort("sal") - .exec(function(err, docs) { - assert.ifError(err); - assert.equal(docs[0].sal, 14000); + aggregate. + model(db.model('Employee')). + sort('sal'). + exec(function(err, docs) { + assert.ifError(err); + assert.equal(docs[0].sal, 14000); + + done(); + }); + }); - clearData(db, function() { done(); }); + it('graphLookup', function(done) { + var _this = this; + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + _this.skip(); + } + test(); + }); + + function test() { + var aggregate = new Aggregate(); + + aggregate. + model(db.model('Employee')). + graphLookup({ + from: 'employees', + startWith: '$reportsTo', + connectFromField: 'reportsTo', + connectToField: 'name', + as: 'employeeHierarchy' + }). + sort({name: 1}). + exec(function(err, docs) { + if (err) { + return done(err); + } + var lowest = docs[3]; + assert.equal(lowest.name, 'Dave'); + assert.equal(lowest.employeeHierarchy.length, 3); + + // First result in array is max depth result + var names = lowest.employeeHierarchy.map(function(doc) { + return doc.name; + }).sort(); + assert.equal(names[0], 'Alice'); + assert.equal(names[1], 'Bob'); + assert.equal(names[2], 'Carol'); + done(); }); + } + }); + + it('facet', function(done) { + var _this = this; + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + _this.skip(); + } + test(); }); + + function test() { + var aggregate = new Aggregate(); + + aggregate. + model(db.model('Employee')). + facet({ + departments: [ + { + $group: { _id: '$dept', count: { $sum: 1 } } + } + ], + employeesPerCustomer: [ + { $unwind: '$customers' }, + { $sortByCount: '$customers' }, + { $sort: { _id: 1 } } + ] + }). + exec(function(error, docs) { + if (error) { + return done(error); + } + assert.deepEqual(docs[0].departments, [ + { _id: 'r&d', count: 2 }, + { _id: 'sales', count: 2 } + ]); + + assert.deepEqual(docs[0].employeesPerCustomer, [ + { _id: 'Eve', count: 1 }, + { _id: 'Fred', count: 1 }, + { _id: 'Gary', count: 1 }, + { _id: 'Herbert', count: 1 }, + { _id: 'Isaac', count: 1 } + ]); + done(); + }); + } }); it('complex pipeline', function(done) { var aggregate = new Aggregate(); - setupData(function(db) { - aggregate - .model(db.model('Employee')) - .match({ sal: { $lt: 16000 } }) - .unwind('customers') - .project({ emp: "$name", cust: "$customers" }) - .sort('-cust') - .skip(2) - .exec(function(err, docs) { - assert.ifError(err); - assert.equal(docs.length, 1); - assert.equal(docs[0].cust, 'Gary'); - assert.equal(docs[0].emp, 'Bob'); + aggregate. + model(db.model('Employee')). + match({ sal: { $lt: 16000 } }). + unwind('customers'). + project({ emp: '$name', cust: '$customers' }). + sort('-cust'). + skip(2). + exec(function(err, docs) { + assert.ifError(err); + assert.equal(docs.length, 1); + assert.equal(docs[0].cust, 'Gary'); + assert.equal(docs[0].emp, 'Bob'); + + done(); + }); + }); - clearData(db, function() { done(); }); - }); - }); + it('pipeline() (gh-5825)', function(done) { + var aggregate = new Aggregate(); + + var pipeline = aggregate. + model(db.model('Employee')). + match({ sal: { $lt: 16000 } }). + pipeline(); + + assert.deepEqual(pipeline, [{ $match: { sal: { $lt: 16000 } } }]); + done(); }); it('explain()', function(done) { var aggregate = new Aggregate(); start.mongodVersion(function(err, version) { if (err) { - return done(err); + done(err); + return; } - var mongo26 = 2 < version[0] || (2 == version[0] && 6 <= version[1]); + var mongo26 = version[0] > 2 || (version[0] === 2 && version[1] >= 6); if (!mongo26) { - return done(); + done(); + return; } - setupData(function(db) { - aggregate. - model(db.model('Employee')). - match({ sal: { $lt: 16000 } }). - explain(function(err, output) { - assert.ifError(err); - assert.ok(output); - // make sure we got explain output - assert.ok(output.stages); - - clearData(db, function() { done(); }); - }); - }); + aggregate. + model(db.model('Employee')). + match({ sal: { $lt: 16000 } }). + explain(function(err1, output) { + assert.ifError(err1); + assert.ok(output); + // make sure we got explain output + assert.ok(output.stages); + + done(); + }); }); }); describe('error when empty pipeline', function() { it('without a callback', function(done) { var agg = new Aggregate; - setupData(function(db) { - agg.model(db.model('Employee')); - var promise = agg.exec(); - assert.ok(promise instanceof mongoose.Promise); - promise.onResolve(function(err) { - assert.ok(err); - assert.equal(err.message, "Aggregate has empty pipeline"); - done(); - }); + + agg.model(db.model('Employee')); + var promise = agg.exec(); + assert.ok(promise instanceof mongoose.Promise); + promise.onResolve(function(err) { + assert.ok(err); + assert.equal(err.message, 'Aggregate has empty pipeline'); + done(); }); }); it('with a callback', function(done) { - var aggregate = new Aggregate() - , callback; - - setupData(function(db) { - aggregate.model(db.model('Employee')); - callback = function(err) { - assert.ok(err); - assert.equal(err.message, "Aggregate has empty pipeline"); - done(); - }; + var aggregate = new Aggregate(); + var callback; - aggregate.exec(callback); - }); + aggregate.model(db.model('Employee')); + callback = function(err) { + assert.ok(err); + assert.equal(err.message, 'Aggregate has empty pipeline'); + done(); + }; + + aggregate.exec(callback); }); }); @@ -543,34 +855,178 @@ describe('aggregate: ', function() { }); it('handles aggregation options', function(done) { - setupData(function(db) { - start.mongodVersion(function(err, version) { - if (err) throw err; - var mongo26_or_greater = 2 < version[0] || (2 == version[0] && 6 <= version[1]); - - var m = db.model('Employee'); - var match = { $match: { sal: { $gt: 15000 }}}; - var pref = 'primaryPreferred'; - var aggregate = m.aggregate(match).read(pref); - if (mongo26_or_greater) { - aggregate.allowDiskUse(true); - } + start.mongodVersion(function(err, version) { + if (err) { + throw err; + } + var mongo26_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 6); + + var m = db.model('Employee'); + var match = { $match: { sal: { $gt: 15000 } } }; + var pref = 'primaryPreferred'; + var aggregate = m.aggregate(match).read(pref); + if (mongo26_or_greater) { + aggregate.allowDiskUse(true); + aggregate.option({maxTimeMS: 1000}); + } - assert.equal(aggregate.options.readPreference.mode, pref); - if (mongo26_or_greater) { - assert.equal(aggregate.options.allowDiskUse, true); - } + assert.equal(aggregate.options.readPreference.mode, pref); + if (mongo26_or_greater) { + assert.equal(aggregate.options.allowDiskUse, true); + assert.equal(aggregate.options.maxTimeMS, 1000); + } + + aggregate. + exec(function(err, docs) { + assert.ifError(err); + assert.equal(1, docs.length); + assert.equal(docs[0].sal, 18000); + done(); + }); + }); + }); + + describe('middleware (gh-5251)', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + it('pre', function(done) { + var s = new Schema({ name: String }); + + var called = 0; + s.pre('aggregate', function(next) { + ++called; + next(); + }); + + var M = db.model('gh5251', s); + + M.aggregate([{ $match: { name: 'test' } }], function(error, res) { + assert.ifError(error); + assert.deepEqual(res, []); + assert.equal(called, 1); + done(); + }); + }); + + it('post', function(done) { + var s = new Schema({ name: String }); + + var calledWith = []; + s.post('aggregate', function(res, next) { + calledWith.push(res); + next(); + }); + + var M = db.model('gh5251_post', s); + + M.aggregate([{ $match: { name: 'test' } }], function(error, res) { + assert.ifError(error); + assert.deepEqual(res, []); + assert.equal(calledWith.length, 1); + assert.deepEqual(calledWith[0], []); + done(); + }); + }); + + it('error handler with agg error', function(done) { + var s = new Schema({ name: String }); + + var calledWith = []; + s.post('aggregate', function(error, res, next) { + calledWith.push(error); + next(); + }); + + var M = db.model('gh5251_error_agg', s); + + M.aggregate([{ $fakeStage: { name: 'test' } }], function(error, res) { + assert.ok(error); + assert.ok(error.message.indexOf('Unrecognized pipeline stage') !== -1, + error.message); + assert.equal(res, null); + assert.equal(calledWith.length, 1); + assert.equal(calledWith[0], error); + done(); + }); + }); + + it('error handler with pre error', function(done) { + var s = new Schema({ name: String }); - aggregate - .exec(function(err, docs) { - assert.ifError(err); - assert.equal(1, docs.length); - assert.equal(docs[0].sal, 18000); - clearData(db, done); - }); + var calledWith = []; + s.pre('aggregate', function(next) { + next(new Error('woops')); }); + s.post('aggregate', function(error, res, next) { + calledWith.push(error); + next(); + }); + + var M = db.model('gh5251_error', s); + + M.aggregate([{ $match: { name: 'test' } }], function(error, res) { + assert.ok(error); + assert.equal(error.message, 'woops'); + assert.equal(res, null); + assert.equal(calledWith.length, 1); + assert.equal(calledWith[0], error); + done(); + }); + }); + + it('with agg cursor', function(done) { + var s = new Schema({ name: String }); + + var calledPre = 0; + var calledPost = 0; + s.pre('aggregate', function(next) { + ++calledPre; + next(); + }); + s.post('aggregate', function(res, next) { + ++calledPost; + next(); + }); + + var M = db.model('gh5251_cursor', s); + + var numDocs = 0; + M. + aggregate([{ $match: { name: 'test' } }]). + cursor({ useMongooseAggCursor: true }). + exec(). + eachAsync(function() { + ++numDocs; + }). + then(function() { + assert.equal(numDocs, 0); + assert.equal(calledPre, 1); + assert.equal(calledPost, 0); + done(); + }); }); }); + + it('readPref from schema (gh-5522)', function(done) { + var schema = new Schema({ name: String }, { read: 'secondary' }); + var M = db.model('gh5522', schema); + var a = M.aggregate(); + assert.equal(a.options.readPreference.mode, 'secondary'); + + a.read('secondaryPreferred'); + + assert.equal(a.options.readPreference.mode, 'secondaryPreferred'); + + done(); + }); }); it('cursor (gh-3160)', function(done) { @@ -578,13 +1034,163 @@ describe('aggregate: ', function() { var MyModel = db.model('gh3160', { name: String }); + MyModel.create({ name: 'test' }, function(error) { + assert.ifError(error); + MyModel. + aggregate([{ $match: { name: 'test' } }, { $project: { name: '$name' } }]). + allowDiskUse(true). + cursor({ batchSize: 2500, async: true }). + exec(function(error, cursor) { + assert.ifError(error); + assert.ok(cursor); + cursor.toArray(function(error) { + assert.ifError(error); + db.close(done); + }); + }); + }); + }); + + it('cursor() without options (gh-3855)', function(done) { + var db = start(); + + var MyModel = db.model('gh3855', { name: String }); + + db.on('open', function() { + var cursor = MyModel. + aggregate([{ $match: { name: 'test' } }]). + cursor(). + exec(); + assert.ok(cursor instanceof require('stream').Readable); + done(); + }); + }); + + it('cursor() with useMongooseAggCursor (gh-5145)', function(done) { + var db = start(); + + var MyModel = db.model('gh5145', { name: String }); + + var cursor = MyModel. + aggregate([{ $match: { name: 'test' } }]). + cursor({ useMongooseAggCursor: true }). + exec(); + assert.ok(cursor instanceof require('stream').Readable); + + done(); + }); + + it('cursor() with useMongooseAggCursor works (gh-5145) (gh-5394)', function(done) { + var db = start(); + + var MyModel = db.model('gh5394', { name: String }); + + MyModel.create({ name: 'test' }, function(error) { + assert.ifError(error); + + var docs = []; + MyModel. + aggregate([{ $match: { name: 'test' } }]). + cursor({ useMongooseAggCursor: true }). + exec(). + eachAsync(function(doc) { + docs.push(doc); + }). + then(function() { + assert.equal(docs.length, 1); + assert.equal(docs[0].name, 'test'); + done(); + }); + }); + }); + + it('cursor() eachAsync (gh-4300)', function(done) { + var db = start(); + + var MyModel = db.model('gh4300', { name: String }); + + var cur = 0; + var expectedNames = ['Axl', 'Slash']; + MyModel.create([{ name: 'Axl' }, { name: 'Slash' }]). + then(function() { + return MyModel.aggregate([{ $sort: { name: 1 } }]). + cursor(). + exec(). + eachAsync(function(doc) { + var _cur = cur; + assert.equal(doc.name, expectedNames[cur]); + return { + then: function(onResolve) { + setTimeout(function() { + assert.equal(_cur, cur++); + onResolve(); + }, 50); + } + }; + }). + then(function() { + done(); + }); + }). + catch(done); + }); + + it('ability to add noCursorTimeout option (gh-4241)', function(done) { + var db = start(); + + var MyModel = db.model('gh4241', { + name: String + }); + MyModel. aggregate([{ $match: { name: 'test' } }]). + addCursorFlag('noCursorTimeout', true). cursor({ async: true }). exec(function(error, cursor) { assert.ifError(error); - assert.ok(cursor); - db.close(done); + assert.ok(cursor.s.cmd.noCursorTimeout); + done(); }); }); + + it('query by document (gh-4866)', function(done) { + var db = start(); + + var MyModel = db.model('gh4866', { + name: String + }); + + MyModel.create({ name: 'test' }). + then(function(doc) { return MyModel.aggregate([{ $match: doc }]); }). + then(function() { + done(); + }). + catch(done); + }); + + it('sort by text score (gh-5258)', function(done) { + var db = start(); + + var mySchema = new Schema({ test: String }); + mySchema.index({ test: 'text' }); + var M = db.model('gh5258', mySchema); + + M.on('index', function(error) { + assert.ifError(error); + M.create([{ test: 'test test' }, { test: 'a test' }], function(error) { + assert.ifError(error); + var aggregate = M.aggregate(); + aggregate.match({ $text: { $search: 'test' } }); + aggregate.sort({ score: { $meta: 'textScore' } }); + + aggregate.exec(function(error, res) { + assert.ifError(error); + assert.equal(res.length, 2); + assert.equal(res[0].test, 'test test'); + assert.equal(res[1].test, 'a test'); + done(); + }); + }); + }); + }); }); diff --git a/test/browser.test.js b/test/browser.test.js new file mode 100644 index 00000000000..e499e92b25a --- /dev/null +++ b/test/browser.test.js @@ -0,0 +1,28 @@ +/** + * Module dependencies. + */ + +var Document = require('../lib/browserDocument'); +var Schema = require('../lib/schema'); +var exec = require('child_process').exec; + +/** + * Test. + */ +describe('browser', function() { + it('require() works with no other require calls (gh-5842)', function(done) { + exec('node --eval "require(\'./lib/browserDocument\')"', done); + }); + + it('document works (gh-4987)', function(done) { + var schema = new Schema({ + name: {type: String, required: true}, + quest: {type: String, match: /Holy Grail/i, required: true}, + favoriteColor: {type: String, enum: ['Red', 'Blue'], required: true} + }); + + new Document({}, schema); + + done(); + }); +}); diff --git a/test/browser/.eslintrc b/test/browser/.eslintrc.yml similarity index 100% rename from test/browser/.eslintrc rename to test/browser/.eslintrc.yml diff --git a/test/browser/document.test_.js b/test/browser/document.test_.js index 5f2a57d1977..04055ad647f 100644 --- a/test/browser/document.test_.js +++ b/test/browser/document.test_.js @@ -1,28 +1,28 @@ -var Schema = mongoose.Schema - , ObjectId = mongoose.Schema.Types.ObjectId; +var Schema = mongoose.Schema, + ObjectId = mongoose.Schema.Types.ObjectId; -var em = new mongoose.Schema({ title: String, body: String }); +var em = new mongoose.Schema({title: String, body: String}); em.virtual('works').get(function() { return 'em virtual works'; }); var schema = new Schema({ - test : String, - oids : [ ObjectId ], - numbers : [ Number ], - nested : { - age : Number, - cool : ObjectId, - deep : { x: String }, - path : String, - setr : String + test: String, + oids: [ObjectId], + numbers: [Number], + nested: { + age: Number, + cool: ObjectId, + deep: {x: String}, + path: String, + setr: String }, - nested2 : { + nested2: { nested: String, - yup : { - nested : Boolean, - yup : String, - age : Number + yup: { + nested: Boolean, + yup: String, + age: Number } }, em: [em], @@ -45,12 +45,12 @@ schema.path('nested.setr').set(function(v) { describe('browser:document', function() { it('work', function(done) { var obj = { - test : 'test', - oids : [], - nested : { - age : 5, - cool : mongoose.Types.ObjectId.createFromHexString('4c6c2d6240ced95d0e00003c'), - path : 'my path' + test: 'test', + oids: [], + nested: { + age: 5, + cool: mongoose.Types.ObjectId.createFromHexString('4c6c2d6240ced95d0e00003c'), + path: 'my path' } }; @@ -68,22 +68,22 @@ describe('browser:document', function() { assert.equal(doc.getValue('nested.setr'), 'set it setter'); var doc2 = new mongoose.Document( - { - test : 'toop', - oids : [], - nested : { - age : 2, - cool : mongoose.Types.ObjectId.createFromHexString('4cf70857337498f95900001c'), - deep : { x: 'yay' } - } - }, - schema); + { + test: 'toop', + oids: [], + nested: { + age: 2, + cool: mongoose.Types.ObjectId.createFromHexString('4cf70857337498f95900001c'), + deep: {x: 'yay'} + } + }, + schema); assert.equal('toop', doc2.get('test')); assert.ok(doc2.get('oids') instanceof Array); assert.equal(doc2.get('nested.age'), 2); - // GH-366 + // GH-366 assert.equal(doc2.get('nested.bonk'), undefined); assert.equal(doc2.get('nested.nested'), undefined); assert.equal(doc2.get('nested.test'), undefined); @@ -101,13 +101,13 @@ describe('browser:document', function() { doc2.set('nested2.yup', { age: 150, - yup: "Yesiree", + yup: 'Yesiree', nested: true }); assert.equal(doc2.get('nested2.nested'), undefined); assert.equal(doc2.get('nested2.yup.nested'), true); - assert.equal(doc2.get('nested2.yup.yup'), "Yesiree"); + assert.equal(doc2.get('nested2.yup.yup'), 'Yesiree'); assert.equal(doc2.get('nested2.yup.age'), 150); doc2.set('nested2.nested', 'y'); assert.equal(doc2.get('nested2.nested'), 'y'); @@ -125,11 +125,14 @@ describe('browser:document', function() { describe('browser:validate', function() { it('works', function(done) { var called = false; - var validate = [ function() { called = true; return true; }, 'BAM']; + var validate = [function() { + called = true; + return true; + }, 'BAM']; schema = new Schema({ - prop: { type: String, required: true, validate: validate }, - nick: { type: String, required: true } + prop: {type: String, required: true, validate: validate}, + nick: {type: String, required: true} }); var doc = new mongoose.Document({}, schema); @@ -151,39 +154,39 @@ describe('browser:validate', function() { describe('#equals', function() { describe('should work', function() { - var S = new Schema({ _id: String }); - var N = new Schema({ _id: Number }); - var O = new Schema({ _id: Schema.ObjectId }); - var B = new Schema({ _id: mongoose.Schema.Types.Buffer }); - var M = new Schema({ name: String }, { _id: false }); + var S = new Schema({_id: String}); + var N = new Schema({_id: Number}); + var O = new Schema({_id: Schema.ObjectId}); + var B = new Schema({_id: mongoose.Schema.Types.Buffer}); + var M = new Schema({name: String}, {_id: false}); it('with string _ids', function(done) { - var s1 = new mongoose.Document({ _id: 'one' }, S); - var s2 = new mongoose.Document({ _id: 'one' }, S); + var s1 = new mongoose.Document({_id: 'one'}, S); + var s2 = new mongoose.Document({_id: 'one'}, S); assert.ok(s1.equals(s2)); done(); }); it('with number _ids', function(done) { - var n1 = new mongoose.Document({ _id: 0 }, N); - var n2 = new mongoose.Document({ _id: 0 }, N); + var n1 = new mongoose.Document({_id: 0}, N); + var n2 = new mongoose.Document({_id: 0}, N); assert.ok(n1.equals(n2)); done(); }); it('with ObjectId _ids', function(done) { var id = new mongoose.Types.ObjectId; - var o1 = new mongoose.Document({ _id: id }, O); - var o2 = new mongoose.Document({ _id: id }, O); + var o1 = new mongoose.Document({_id: id}, O); + var o2 = new mongoose.Document({_id: id}, O); assert.ok(o1.equals(o2)); id = String(new mongoose.Types.ObjectId); - o1 = new mongoose.Document({ _id: id }, O); - o2 = new mongoose.Document({ _id: id }, O); + o1 = new mongoose.Document({_id: id}, O); + o2 = new mongoose.Document({_id: id}, O); assert.ok(o1.equals(o2)); done(); }); it('with Buffer _ids', function(done) { - var n1 = new mongoose.Document({ _id: 0 }, B); - var n2 = new mongoose.Document({ _id: 0 }, B); + var n1 = new mongoose.Document({_id: 0}, B); + var n2 = new mongoose.Document({_id: 0}, B); assert.ok(n1.equals(n2)); done(); }); diff --git a/test/browser/example.test_.js b/test/browser/example.test_.js index f12b2611615..35e4404319e 100644 --- a/test/browser/example.test_.js +++ b/test/browser/example.test_.js @@ -9,13 +9,18 @@ * utilizes mongoose's Amazon [CloudFront](http://aws.amazon.com/cloudfront/) * CDN. * + * If you are using webpack to bundle your client-side code, you need to add the following plugin to webpack's `plugins` array + * so that webpack does not attempt to resolve mongoose's server-side dependencies: + * + * new webpack.DefinePlugin({ + * 'typeof window': '\"object\"' + * }) * ``` * * ``` */ describe('Mongoose in the browser', function() { - /** * When you include the `mongoose.js` file in a script tag, mongoose will * attach a `mongoose` object to the global `window`. This object includes @@ -28,10 +33,10 @@ describe('Mongoose in the browser', function() { */ describe('Declaring schemas in the browser', function() { it('allows you to use mongoose types', function() { - var foodSchema = new mongoose.Schema({ name: String }); + var foodSchema = new mongoose.Schema({name: String}); var breakfastSchema = new mongoose.Schema({ foods: [foodSchema], - date: { type: Date, default: Date.now } + date: {type: Date, default: Date.now} }); assert.ok(foodSchema.path('name') instanceof mongoose.Schema.Types.String); @@ -49,9 +54,9 @@ describe('Mongoose in the browser', function() { describe('Validating documents in the browser', function() { it('allows you to create a schema and use it to validate documents', function(done) { var schema = new mongoose.Schema({ - name: { type: String, required: true }, - quest: { type: String, match: /Holy Grail/i, required: true }, - favoriteColor: { type: String, enum: ['Red', 'Blue'], required: true } + name: {type: String, required: true}, + quest: {type: String, match: /Holy Grail/i, required: true}, + favoriteColor: {type: String, enum: ['Red', 'Blue'], required: true} }); /* `mongoose.Document` is different in the browser than in NodeJS. diff --git a/test/browser/schema.test_.js b/test/browser/schema.test_.js index 8247543adf8..573e19c904a 100644 --- a/test/browser/schema.test_.js +++ b/test/browser/schema.test_.js @@ -1,58 +1,58 @@ var underlyingBuffer = Buffer; -var Schema = mongoose.Schema - , Document = mongoose.Document - , VirtualType = mongoose.VirtualType - , SchemaTypes = Schema.Types - , ObjectId = SchemaTypes.ObjectId - , Mixed = SchemaTypes.Mixed - , Buffer = SchemaTypes.Buffer - , DocumentObjectId = mongoose.Types.ObjectId; +var Schema = mongoose.Schema, + Document = mongoose.Document, + VirtualType = mongoose.VirtualType, + SchemaTypes = Schema.Types, + ObjectId = SchemaTypes.ObjectId, + Mixed = SchemaTypes.Mixed, + Buffer = SchemaTypes.Buffer, + DocumentObjectId = mongoose.Types.ObjectId; /** * Test Document constructor. */ -function TestDocument( obj ) { +function TestDocument(obj) { mongoose.Document.call(this, obj || {}, new Schema({ test: String - }) ); + })); } /** * Inherits from Document. */ -TestDocument.prototype = Object.create( Document.prototype ); +TestDocument.prototype = Object.create(Document.prototype); TestDocument.prototype.constructor = TestDocument; describe('schema', function() { it('can be created without the "new" keyword', function(done) { - var schema = mongoose.Schema({ name: String }); + var schema = mongoose.Schema({name: String}); assert.ok(schema instanceof mongoose.Schema); done(); }); it('supports different schematypes', function(done) { var Checkin = new mongoose.Schema({ - date : Date - , location : { - lat: Number - , lng: Number + date: Date, + location: { + lat: Number, + lng: Number } }); var Ferret = new mongoose.Schema({ - name : String - , owner : mongoose.Schema.Types.ObjectId - , fur : String - , color : { type: String } - , age : Number - , checkins : [Checkin] - , friends : [mongoose.Schema.Types.ObjectId] - , likes : Array - , alive : Boolean - , extra : mongoose.Schema.Types.Mixed + name: String, + owner: mongoose.Schema.Types.ObjectId, + fur: String, + color: {type: String}, + age: Number, + checkins: [Checkin], + friends: [mongoose.Schema.Types.ObjectId], + likes: Array, + alive: Boolean, + extra: mongoose.Schema.Types.Mixed }); assert.ok(Ferret.path('name') instanceof mongoose.Schema.Types.String); @@ -72,10 +72,10 @@ describe('schema', function() { // check strings var Checkin1 = new mongoose.Schema({ - date : 'date' - , location : { - lat: 'number' - , lng: 'Number' + date: 'date', + location: { + lat: 'number', + lng: 'Number' } }); @@ -84,20 +84,20 @@ describe('schema', function() { assert.ok(Checkin1.path('location.lng') instanceof mongoose.Schema.Types.Number); var Ferret1 = new mongoose.Schema({ - name : "string" - , owner : "oid" - , fur : { type: "string" } - , color : { type: "String" } - , checkins : [Checkin] - , friends : Array - , likes : "array" - , alive : "Bool" - , alive1 : "bool" - , alive2 : "boolean" - , extra : "mixed" - , obj : "object" - , buf : "buffer" - , Buf : "Buffer" + name: 'string', + owner: 'oid', + fur: {type: 'string'}, + color: {type: 'String'}, + checkins: [Checkin], + friends: Array, + likes: 'array', + alive: 'Bool', + alive1: 'bool', + alive2: 'boolean', + extra: 'mixed', + obj: 'object', + buf: 'buffer', + Buf: 'Buffer' }); assert.ok(Ferret1.path('name') instanceof mongoose.Schema.Types.String); @@ -105,7 +105,7 @@ describe('schema', function() { assert.ok(Ferret1.path('fur') instanceof mongoose.Schema.Types.String); assert.ok(Ferret1.path('color') instanceof mongoose.Schema.Types.String); assert.ok(Ferret1.path('checkins') instanceof mongoose.Schema.Types.DocumentArray); - assert.ok( Ferret1.path('friends') instanceof mongoose.Schema.Types.Array); + assert.ok(Ferret1.path('friends') instanceof mongoose.Schema.Types.Array); assert.ok(Ferret1.path('likes') instanceof mongoose.Schema.Types.Array); assert.ok(Ferret1.path('alive') instanceof mongoose.Schema.Types.Boolean); assert.ok(Ferret1.path('alive1') instanceof mongoose.Schema.Types.Boolean); @@ -119,19 +119,19 @@ describe('schema', function() { it('supports dot notation for path accessors', function(done) { var Racoon = new Schema({ - name : { type: String, enum: ['Edwald', 'Tobi'] } - , age : Number + name: {type: String, enum: ['Edwald', 'Tobi']}, + age: Number }); // check for global variable leak assert.equal('undefined', typeof errorMessage); var Person = new Schema({ - name : String - , raccoons : [Racoon] - , location : { - city : String - , state : String + name: String, + raccoons: [Racoon], + location: { + city: String, + state: String } }); @@ -158,21 +158,27 @@ describe('schema', function() { it('default definition', function(done) { var Test = new Schema({ - simple : { type: String, default: 'a' } - , array : { type: Array, default: [1,2,3,4,5] } - , arrayX : { type: Array, default: 9 } - , arrayFn : { type: Array, default: function() { return [8]; } } - , callback : { type: Number, default: function() { + simple: {type: String, default: 'a'}, + array: {type: Array, default: [1, 2, 3, 4, 5]}, + arrayX: {type: Array, default: 9}, + arrayFn: { + type: Array, default: function() { + return [8]; + } + }, + callback: { + type: Number, default: function() { assert.equal('b', this.a); return '3'; - }} + } + } }); assert.equal(Test.path('simple').defaultValue, 'a'); assert.equal(typeof Test.path('callback').defaultValue, 'function'); assert.equal(Test.path('simple').getDefault(), 'a'); - assert.equal((+Test.path('callback').getDefault({ a: 'b' })), 3); + assert.equal((+Test.path('callback').getDefault({a: 'b'})), 3); assert.equal(typeof Test.path('array').defaultValue, 'function'); assert.equal(Test.path('array').getDefault(new TestDocument)[3], 4); assert.equal(Test.path('arrayX').getDefault(new TestDocument)[0], 9); @@ -183,8 +189,8 @@ describe('schema', function() { it('Mixed defaults can be empty arrays', function(done) { var Test = new Schema({ - mixed1 : { type: Mixed, default: [] } - , mixed2 : { type: Mixed, default: Array } + mixed1: {type: Mixed, default: []}, + mixed2: {type: Mixed, default: Array} }); assert.ok(Test.path('mixed1').getDefault() instanceof Array); @@ -215,7 +221,9 @@ describe('schema', function() { nickname: String }); - function Test() {} + function Test() { + } + Test.prototype.toString = function() { return 'woot'; }; @@ -229,7 +237,7 @@ describe('schema', function() { assert.equal('woot', Tobi.path('nickname').cast(new Test)); done(); }); - /*it('casts undefined to "undefined"', function(done){ + /* it('casts undefined to "undefined"', function(done){ var schema = new Schema({ arr: [String] }); var M = db.model('castingStringArrayWithUndefined', schema); M.find({ arr: { $in: [undefined] }}, function (err) { @@ -237,12 +245,12 @@ describe('schema', function() { assert.equal(err && err.message, 'Cast to string failed for value "undefined" at path "arr"'); done(); }); - });*/ + }); */ }); it('date', function(done) { var Loki = new Schema({ - birth_date: { type: Date } + birth_date: {type: Date} }); assert.ok(Loki.path('birth_date').cast(1294525628301) instanceof Date); @@ -253,20 +261,17 @@ describe('schema', function() { it('objectid', function(done) { var Loki = new Schema({ - owner: { type: ObjectId } + owner: {type: ObjectId} }); - var doc = new TestDocument - , id = doc._id.toString(); + var doc = new TestDocument, + id = doc._id.toString(); - assert.ok(Loki.path('owner').cast('4c54f3453e688c000000001a') - instanceof DocumentObjectId); + assert.ok(Loki.path('owner').cast('4c54f3453e688c000000001a') instanceof DocumentObjectId); - assert.ok(Loki.path('owner').cast(new DocumentObjectId()) - instanceof DocumentObjectId); + assert.ok(Loki.path('owner').cast(new DocumentObjectId()) instanceof DocumentObjectId); - assert.ok(Loki.path('owner').cast(doc) - instanceof DocumentObjectId); + assert.ok(Loki.path('owner').cast(doc) instanceof DocumentObjectId); assert.equal(id, Loki.path('owner').cast(doc).toString()); done(); @@ -274,13 +279,13 @@ describe('schema', function() { it('array', function(done) { var Loki = new Schema({ - oids : [ObjectId] - , dates : [Date] - , numbers : [Number] - , strings : [String] - , buffers : [Buffer] - , nocast : [] - , mixed : [Mixed] + oids: [ObjectId], + dates: [Date], + numbers: [Number], + strings: [String], + buffers: [Buffer], + nocast: [], + mixed: [Mixed] }); var oids = Loki.path('oids').cast(['4c54f3453e688c000000001a', new DocumentObjectId]); @@ -301,12 +306,12 @@ describe('schema', function() { var strings = Loki.path('strings').cast(['test', 123]); assert.equal(typeof strings[0], 'string'); - assert.equal('test',strings[0]); + assert.equal('test', strings[0]); assert.equal(typeof strings[1], 'string'); assert.equal('123', strings[1]); - var buffers = Loki.path('buffers').cast(['\0\0\0', new underlyingBuffer("abc")]); + var buffers = Loki.path('buffers').cast(['\0\0\0', new underlyingBuffer('abc')]); assert.ok(underlyingBuffer.isBuffer(buffers[0])); assert.ok(underlyingBuffer.isBuffer(buffers[1])); @@ -332,7 +337,7 @@ describe('schema', function() { it('boolean', function(done) { var Animal = new Schema({ - isFerret: { type: Boolean, required: true } + isFerret: {type: Boolean, required: true} }); assert.strictEqual(Animal.path('isFerret').cast(null), null); @@ -352,10 +357,13 @@ describe('schema', function() { it('methods declaration', function(done) { var a = new Schema; - a.method('test', function() {}); + a.method('test', function() { + }); a.method({ - a: function() {} - , b: function() {} + a: function() { + }, + b: function() { + } }); assert.equal(3, Object.keys(a.methods).length); done(); @@ -363,11 +371,15 @@ describe('schema', function() { it('static declaration', function(done) { var a = new Schema; - a.static('test', function() {}); + a.static('test', function() { + }); a.static({ - a: function() {} - , b: function() {} - , c: function() {} + a: function() { + }, + b: function() { + }, + c: function() { + } }); assert.equal(Object.keys(a.statics).length, 4); @@ -381,7 +393,7 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, set: lowercase } + name: {type: String, set: lowercase} }); assert.equal('woot', Tobi.path('name').applySetters('WOOT')); @@ -399,21 +411,21 @@ describe('schema', function() { it('order', function(done) { function extract(v) { return (v && v._id) - ? v._id - : v; + ? v._id + : v; } var Tobi = new Schema({ - name: { type: Schema.ObjectId, set: extract } + name: {type: Schema.ObjectId, set: extract} }); - var id = new DocumentObjectId - , sid = id.toString() - , _id = { _id: id }; + var id = new DocumentObjectId, + sid = id.toString(), + _id = {_id: id}; - assert.equal(Tobi.path('name').applySetters(sid, { a: 'b' }).toString(),sid); - assert.equal(Tobi.path('name').applySetters(_id, { a: 'b' }).toString(),sid); - assert.equal(Tobi.path('name').applySetters(id, { a: 'b' }).toString(),sid); + assert.equal(Tobi.path('name').applySetters(sid, {a: 'b'}).toString(), sid); + assert.equal(Tobi.path('name').applySetters(_id, {a: 'b'}).toString(), sid); + assert.equal(Tobi.path('name').applySetters(id, {a: 'b'}).toString(), sid); done(); }); @@ -425,10 +437,10 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, set: lowercase } + name: {type: String, set: lowercase} }); - assert.equal('what', Tobi.path('name').applySetters('WHAT', { a: 'b' })); + assert.equal('what', Tobi.path('name').applySetters('WHAT', {a: 'b'})); done(); }); @@ -444,7 +456,7 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, set: last } + name: {type: String, set: last} }); Tobi.path('name').set(first); @@ -468,7 +480,7 @@ describe('schema', function() { describe('string', function() { it('lowercase', function(done) { var Tobi = new Schema({ - name: { type: String, lowercase: true } + name: {type: String, lowercase: true} }); assert.equal('what', Tobi.path('name').applySetters('WHAT')); @@ -477,7 +489,7 @@ describe('schema', function() { }); it('uppercase', function(done) { var Tobi = new Schema({ - name: { type: String, uppercase: true } + name: {type: String, uppercase: true} }); assert.equal('WHAT', Tobi.path('name').applySetters('what')); @@ -486,7 +498,7 @@ describe('schema', function() { }); it('trim', function(done) { var Tobi = new Schema({ - name: { type: String, uppercase: true, trim: true } + name: {type: String, uppercase: true, trim: true} }); assert.equal('WHAT', Tobi.path('name').applySetters(' what ')); @@ -505,7 +517,7 @@ describe('schema', function() { }); it('assignment of non-functions throw', function(done) { - var schema = new Schema({ fun: String }); + var schema = new Schema({fun: String}); var g; try { @@ -515,7 +527,7 @@ describe('schema', function() { } assert.ok(g); - assert.equal(g.message,'A setter must be a function.'); + assert.equal(g.message, 'A setter must be a function.'); done(); }); }); @@ -527,7 +539,7 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, get: woot } + name: {type: String, get: woot} }); assert.equal(1, Tobi.path('name').getters.length); @@ -537,15 +549,15 @@ describe('schema', function() { it('order', function(done) { function format(v) { return v - ? '$' + v - : v; + ? '$' + v + : v; } var Tobi = new Schema({ - name: { type: Number, get: format } + name: {type: Number, get: format} }); - assert.equal('$30', Tobi.path('name').applyGetters(30, { a: 'b' })); + assert.equal('$30', Tobi.path('name').applyGetters(30, {a: 'b'})); done(); }); it('scope', function(done) { @@ -556,10 +568,10 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, get: woot } + name: {type: String, get: woot} }); - assert.equal('yep', Tobi.path('name').applyGetters('YEP', { a: 'b' })); + assert.equal('yep', Tobi.path('name').applyGetters('YEP', {a: 'b'})); done(); }); it('casting', function(done) { @@ -574,7 +586,7 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, get: last } + name: {type: String, get: last} }); Tobi.path('name').get(first); @@ -590,7 +602,7 @@ describe('schema', function() { done(); }); it('assignment of non-functions throw', function(done) { - var schema = new Schema({ fun: String }); + var schema = new Schema({fun: String}); var g; try { @@ -600,7 +612,7 @@ describe('schema', function() { } assert.ok(g); - assert.equal(g.message,'A getter must be a function.'); + assert.equal(g.message, 'A getter must be a function.'); done(); }); it('auto _id', function(done) { @@ -611,23 +623,23 @@ describe('schema', function() { schema = new Schema({ name: String - }, { _id: true }); + }, {_id: true}); assert.ok(schema.path('_id') instanceof Schema.ObjectId); schema = new Schema({ name: String - }, { _id: false }); + }, {_id: false}); assert.equal(undefined, schema.path('_id')); // old options schema = new Schema({ name: String - }, { noId: false }); + }, {noId: false}); assert.ok(schema.path('_id') instanceof Schema.ObjectId); schema = new Schema({ name: String - }, { noId: true }); + }, {noId: true}); assert.equal(undefined, schema.path('_id')); done(); }); @@ -640,23 +652,23 @@ describe('schema', function() { schema = new Schema({ name: String - }, { id: true }); + }, {id: true}); assert.ok(schema.virtualpath('id') instanceof mongoose.VirtualType); schema = new Schema({ name: String - }, { id: false }); + }, {id: false}); assert.equal(undefined, schema.virtualpath('id')); // old options schema = new Schema({ name: String - }, { noVirtualId: false }); + }, {noVirtualId: false}); assert.ok(schema.virtualpath('id') instanceof mongoose.VirtualType); schema = new Schema({ name: String - }, { noVirtualId: true }); + }, {noVirtualId: true}); assert.equal(undefined, schema.virtualpath('id')); done(); }); @@ -666,13 +678,16 @@ describe('schema', function() { it('registration', function(done) { var Tobi = new Schema(); - Tobi.pre('save', function() {}); + Tobi.pre('save', function() { + }); assert.equal(2, Tobi.callQueue.length); - Tobi.post('save', function() {}); + Tobi.post('save', function() { + }); assert.equal(3, Tobi.callQueue.length); - Tobi.pre('save', function() {}); + Tobi.pre('save', function() { + }); assert.equal(4, Tobi.callQueue.length); done(); }); @@ -680,8 +695,8 @@ describe('schema', function() { describe('plugins', function() { it('work', function(done) { - var Tobi = new Schema - , called = false; + var Tobi = new Schema, + called = false; Tobi.plugin(function(schema) { assert.equal(schema, Tobi); @@ -710,14 +725,14 @@ describe('schema', function() { }); it('setting', function(done) { - var Tobi = new Schema({}, { collection: 'users' }); + var Tobi = new Schema({}, {collection: 'users'}); Tobi.set('a', 'b'); Tobi.set('safe', false); assert.equal('users', Tobi.options.collection); assert.equal('b', Tobi.options.a); - assert.deepEqual(Tobi.options.safe, { w: 0 }); + assert.deepEqual(Tobi.options.safe, {w: 0}); assert.equal(null, Tobi.options.read); done(); @@ -727,20 +742,20 @@ describe('schema', function() { describe('virtuals', function() { it('works', function(done) { var Contact = new Schema({ - firstName: String - , lastName: String + firstName: String, + lastName: String }); Contact - .virtual('fullName') - .get(function() { - return this.get('firstName') + ' ' + this.get('lastName'); - }) - .set(function(fullName) { - var split = fullName.split(' '); - this.set('firstName', split[0]); - this.set('lastName', split[1]); - }); + .virtual('fullName') + .get(function() { + return this.get('firstName') + ' ' + this.get('lastName'); + }) + .set(function(fullName) { + var split = fullName.split(' '); + this.set('firstName', split[0]); + this.set('lastName', split[1]); + }); assert.ok(Contact.virtualpath('fullName') instanceof VirtualType); done(); @@ -749,12 +764,12 @@ describe('schema', function() { describe('id', function() { it('default creation of id can be overridden (gh-298)', function(done) { assert.doesNotThrow(function() { - new Schema({ id: String }); + new Schema({id: String}); }); done(); }); it('disabling', function(done) { - var schema = new Schema({ name: String }, { noVirtualId: true }); + var schema = new Schema({name: String}, {noVirtualId: true}); assert.strictEqual(undefined, schema.virtuals.id); done(); }); @@ -770,7 +785,7 @@ describe('schema', function() { return v.toLowerCase(); }); - assert.equal('yep', Tobi.virtualpath('name').applyGetters('YEP', { a: 'b' })); + assert.equal('yep', Tobi.virtualpath('name').applyGetters('YEP', {a: 'b'})); done(); }); }); @@ -785,7 +800,7 @@ describe('schema', function() { return v.toLowerCase(); }); - assert.equal('yep', Tobi.virtualpath('name').applySetters('YEP', { a: 'b' })); + assert.equal('yep', Tobi.virtualpath('name').applySetters('YEP', {a: 'b'})); done(); }); }); @@ -797,13 +812,13 @@ describe('schema', function() { it('debugging msgs', function(done) { var err; try { - new Schema({ name: { first: null } }); + new Schema({name: {first: null}}); } catch (e) { err = e; } - assert.equal(err.message,'Invalid value for schema path `name.first`'); + assert.equal(err.message, 'Invalid value for schema path `name.first`'); try { - new Schema({ age: undefined }); + new Schema({age: undefined}); } catch (e) { err = e; } @@ -815,11 +830,11 @@ describe('schema', function() { it('array of object literal missing a type is interpreted as DocumentArray', function(done) { var s = new Schema({ arr: [ - { something: { type: String } } + {something: {type: String}} ] }); assert.ok(s.path('arr') instanceof SchemaTypes.DocumentArray); - var m = new mongoose.Document({ arr: [ { something: 'wicked this way comes' }] }, s); + var m = new mongoose.Document({arr: [{something: 'wicked this way comes'}]}, s); assert.equal('wicked this way comes', m.arr[0].something); assert.ok(m.arr[0]._id); done(); @@ -828,22 +843,22 @@ describe('schema', function() { it('array of object literal with type.type is interpreted as DocumentArray', function(done) { var s = new Schema({ arr: [ - { type: { type: String } } + {type: {type: String}} ] }); assert.ok(s.path('arr') instanceof SchemaTypes.DocumentArray); - var m = new mongoose.Document({ arr: [ { type: 'works' }] }, s); + var m = new mongoose.Document({arr: [{type: 'works'}]}, s); assert.equal('works', m.arr[0].type); assert.ok(m.arr[0]._id); done(); }); it('of nested schemas should throw (gh-700)', function(done) { - var a = new Schema({ title: String }) - , err; + var a = new Schema({title: String}), + err; try { - new Schema({ blah: Boolean, a: a }); + new Schema({blah: Boolean, a: a}); } catch (err_) { err = err_; } @@ -855,27 +870,27 @@ describe('schema', function() { it('does not alter original argument (gh-1364)', function(done) { var schema = { - ids: [{ type: Schema.ObjectId, ref: 'something' }] - , a: { type: Array } - , b: Array - , c: [Date] - , d: { type: 'Boolean' } - , e: [{ a: String, b: [{ /*type: { type: Buffer },*/ x: Number }] }] + ids: [{type: Schema.ObjectId, ref: 'something'}], + a: {type: Array}, + b: Array, + c: [Date], + d: {type: 'Boolean'}, + e: [{a: String, b: [{x: Number}]}] }; new Schema(schema); assert.equal(6, Object.keys(schema).length); - assert.deepEqual([{ type: Schema.ObjectId, ref: 'something' }], schema.ids); - assert.deepEqual({ type: Array }, schema.a); + assert.deepEqual([{type: Schema.ObjectId, ref: 'something'}], schema.ids); + assert.deepEqual({type: Array}, schema.a); assert.deepEqual(Array, schema.b); assert.deepEqual([Date], schema.c); - assert.deepEqual({ type: 'Boolean' }, schema.d); - assert.deepEqual([{ a: String, b: [{ /*type: { type: Buffer },*/ x: Number }] }], schema.e); + assert.deepEqual({type: 'Boolean'}, schema.d); + assert.deepEqual([{a: String, b: [{x: Number}]}], schema.e); done(); }); - /*it('properly gets value of plain objects when dealing with refs (gh-1606)', function (done) { + /* it('properly gets value of plain objects when dealing with refs (gh-1606)', function (done) { var el = new Schema({ title : String }); var so = new Schema({ title : String, @@ -899,6 +914,6 @@ describe('schema', function() { }); }); }); - });*/ + }); */ }); }); diff --git a/test/browser/schema.validation.test_.js b/test/browser/schema.validation.test_.js index cbf529bfc1b..09de999c8ee 100644 --- a/test/browser/schema.validation.test_.js +++ b/test/browser/schema.validation.test_.js @@ -2,37 +2,37 @@ * Module dependencies. */ -var Schema = mongoose.Schema - , ValidatorError = mongoose.Error.ValidatorError - , SchemaTypes = Schema.Types - , ObjectId = SchemaTypes.ObjectId - , Mixed = SchemaTypes.Mixed - , DocumentObjectId = mongoose.Types.ObjectId; +var Schema = mongoose.Schema, + ValidatorError = mongoose.Error.ValidatorError, + SchemaTypes = Schema.Types, + ObjectId = SchemaTypes.ObjectId, + Mixed = SchemaTypes.Mixed, + DocumentObjectId = mongoose.Types.ObjectId; describe('schema', function() { describe('validation', function() { it('invalid arguments are rejected (1044)', function(done) { assert.throws(function() { new Schema({ - simple: { type: String, validate: 'nope' } + simple: {type: String, validate: 'nope'} }); }, /Invalid validator/); assert.throws(function() { new Schema({ - simple: { type: String, validate: ['nope'] } + simple: {type: String, validate: ['nope']} }); }, /Invalid validator/); assert.throws(function() { new Schema({ - simple: { type: String, validate: { nope: 1, msg: 'nope' } } + simple: {type: String, validate: {nope: 1, msg: 'nope'}} }); }, /Invalid validator/); assert.throws(function() { new Schema({ - simple: { type: String, validate: [{ nope: 1, msg: 'nope' }, 'nope'] } + simple: {type: String, validate: [{nope: 1, msg: 'nope'}, 'nope']} }); }, /Invalid validator/); @@ -41,12 +41,12 @@ describe('schema', function() { it('string enum', function(done) { var Test = new Schema({ - complex: { type: String, enum: ['a', 'b', undefined, 'c', null] }, - state: { type: String } + complex: {type: String, enum: ['a', 'b', undefined, 'c', null]}, + state: {type: String} }); assert.ok(Test.path('complex') instanceof SchemaTypes.String); - assert.deepEqual(Test.path('complex').enumValues,['a', 'b', 'c', null]); + assert.deepEqual(Test.path('complex').enumValues, ['a', 'b', 'c', null]); assert.equal(Test.path('complex').validators.length, 1); Test.path('complex').enum('d', 'e'); @@ -96,7 +96,7 @@ describe('schema', function() { it('string regexp', function(done) { var Test = new Schema({ - simple: { type: String, match: /[a-z]/ } + simple: {type: String, match: /[a-z]/} }); assert.equal(1, Test.path('simple').validators.length); @@ -135,7 +135,7 @@ describe('schema', function() { it('number min and max', function(done) { var Tobi = new Schema({ - friends: { type: Number, max: 15, min: 5 } + friends: {type: Number, max: 15, min: 5} }); assert.equal(Tobi.path('friends').validators.length, 2); @@ -247,7 +247,7 @@ describe('schema', function() { it('number required', function(done) { var Edwald = new Schema({ - friends: { type: Number, required: true } + friends: {type: Number, required: true} }); Edwald.path('friends').doValidate(null, function(err) { @@ -267,7 +267,7 @@ describe('schema', function() { it('date required', function(done) { var Loki = new Schema({ - birth_date: { type: Date, required: true } + birth_date: {type: Date, required: true} }); Loki.path('birth_date').doValidate(null, function(err) { @@ -287,7 +287,7 @@ describe('schema', function() { it('objectid required', function(done) { var Loki = new Schema({ - owner: { type: ObjectId, required: true } + owner: {type: ObjectId, required: true} }); Loki.path('owner').doValidate(new DocumentObjectId(), function(err) { @@ -306,7 +306,7 @@ describe('schema', function() { it('array required', function(done) { var Loki = new Schema({ - likes: { type: Array, required: true } + likes: {type: Array, required: true} }); Loki.path('likes').doValidate(null, function(err) { @@ -325,7 +325,7 @@ describe('schema', function() { it('boolean required', function(done) { var Animal = new Schema({ - isFerret: { type: Boolean, required: true } + isFerret: {type: Boolean, required: true} }); Animal.path('isFerret').doValidate(null, function(err) { @@ -348,7 +348,7 @@ describe('schema', function() { it('mixed required', function(done) { var Animal = new Schema({ - characteristics: { type: Mixed, required: true } + characteristics: {type: Mixed, required: true} }); Animal.path('characteristics').doValidate(null, function(err) { @@ -380,12 +380,14 @@ describe('schema', function() { setTimeout(function() { executed++; fn(value === true); - if (2 === executed) done(); + if (executed === 2) { + done(); + } }, 5); } var Animal = new Schema({ - ferret: { type: Boolean, validate: validator } + ferret: {type: Boolean, validate: validator} }); Animal.path('ferret').doValidate(true, function(err) { @@ -404,23 +406,22 @@ describe('schema', function() { setTimeout(function() { executed++; fn(value === true); - if (2 === executed) done(); + if (executed === 2) { + done(); + } }, 5); } var Animal = new Schema({ ferret: { type: Boolean, - validate: [ - { - 'validator': validator, - 'msg': 'validator1' - }, - { - 'validator': validator, - 'msg': 'validator2' - } - ] + validate: [{ + validator: validator, + msg: 'validator1' + }, { + validator: validator, + msg: 'validator2' + }] } }); @@ -431,6 +432,7 @@ describe('schema', function() { it('scope', function(done) { var called = false; + function validator(value, fn) { assert.equal('b', this.a); @@ -441,14 +443,14 @@ describe('schema', function() { } var Animal = new Schema({ - ferret: { type: Boolean, validate: validator } + ferret: {type: Boolean, validate: validator} }); Animal.path('ferret').doValidate(true, function(err) { assert.ifError(err); assert.equal(true, called); done(); - }, { a: 'b' }); + }, {a: 'b'}); }); }); @@ -456,16 +458,16 @@ describe('schema', function() { describe('are customizable', function() { it('within schema definitions', function(done) { var schema = new Schema({ - name: { type: String, enum: ['one', 'two'] } - , myenum: { type: String, enum: { values: ['x'], message: 'enum validator failed for path: {PATH} with {VALUE}' }} - , requiredString1: { type: String, required: true } - , requiredString2: { type: String, required: 'oops, {PATH} is missing. {TYPE}' } - , matchString0: { type: String, match: /bryancranston/ } - , matchString1: { type: String, match: [/bryancranston/, 'invalid string for {PATH} with value: {VALUE}'] } - , numMin0: { type: Number, min: 10 } - , numMin1: { type: Number, min: [10, 'hey, {PATH} is too small']} - , numMax0: { type: Number, max: 20 } - , numMax1: { type: Number, max: [20, 'hey, {PATH} ({VALUE}) is greater than {MAX}'] } + name: {type: String, enum: ['one', 'two']}, + myenum: {type: String, enum: {values: ['x'], message: 'enum validator failed for path: {PATH} with {VALUE}'}}, + requiredString1: {type: String, required: true}, + requiredString2: {type: String, required: 'oops, {PATH} is missing. {TYPE}'}, + matchString0: {type: String, match: /bryancranston/}, + matchString1: {type: String, match: [/bryancranston/, 'invalid string for {PATH} with value: {VALUE}']}, + numMin0: {type: Number, min: 10}, + numMin1: {type: Number, min: [10, 'hey, {PATH} is too small']}, + numMax0: {type: Number, max: 20}, + numMax1: {type: Number, max: [20, 'hey, {PATH} ({VALUE}) is greater than {MAX}']} }); var a = new mongoose.Document({}, schema); @@ -504,11 +506,12 @@ describe('schema', function() { function validate() { return false; } + var validator = [validate, '{PATH} failed validation ({VALUE})']; - var schema = new Schema({ x: { type: [], validate: validator }}); + var schema = new Schema({x: {type: [], validate: validator}}); - var doc = new mongoose.Document({ x: [3,4,5,6] }, schema); + var doc = new mongoose.Document({x: [3, 4, 5, 6]}, schema); doc.validate(function(err) { assert.equal('x failed validation (3,4,5,6)', String(err.errors.x)); @@ -525,11 +528,12 @@ describe('schema', function() { function validate() { return false; } + var validator = [validate, '{PATH} failed validation ({VALUE})', 'customType']; - var schema = new Schema({ x: { type: [], validate: validator }}); + var schema = new Schema({x: {type: [], validate: validator}}); - var doc = new mongoose.Document({ x: [3,4,5,6] }, schema); + var doc = new mongoose.Document({x: [3, 4, 5, 6]}, schema); doc.validate(function(err) { assert.equal('x failed validation (3,4,5,6)', String(err.errors.x)); @@ -542,12 +546,13 @@ describe('schema', function() { function validate() { return false; } + var validator = [ - { validator: validate, msg: '{PATH} failed validation ({VALUE})', type: 'customType'} + {validator: validate, msg: '{PATH} failed validation ({VALUE})', type: 'customType'} ]; - var schema = new Schema({ x: { type: [], validate: validator }}); + var schema = new Schema({x: {type: [], validate: validator}}); - var doc = new mongoose.Document({ x: [3,4,5,6] }, schema); + var doc = new mongoose.Document({x: [3, 4, 5, 6]}, schema); doc.validate(function(err) { assert.equal('x failed validation (3,4,5,6)', String(err.errors.x)); @@ -559,7 +564,7 @@ describe('schema', function() { }); describe('sync', function() { - it('works', function( done ) { + it('works', function(done) { var executed = 0; function validator(value) { @@ -568,13 +573,15 @@ describe('schema', function() { } var Animal = new Schema({ - ferret: { type: Boolean, validate: validator } + ferret: {type: Boolean, validate: validator} }); assert.ifError(Animal.path('ferret').doValidateSync(true)); assert.ok(Animal.path('ferret').doValidateSync(false) instanceof Error); - if (2 === executed) done(); + if (executed === 2) { + done(); + } }); it('multiple', function(done) { @@ -588,26 +595,26 @@ describe('schema', function() { var Animal = new Schema({ ferret: { type: Boolean, - validate: [ - { - 'validator': validator, - 'msg': 'validator1' - }, - { - 'validator': validator, - 'msg': 'validator2' - } - ] + validate: [{ + validator: validator, + msg: 'validator1' + }, { + validator: validator, + msg: 'validator2' + }] } }); assert.ifError(Animal.path('ferret').doValidateSync(true)); - if (2 === executed) done(); + if (executed === 2) { + done(); + } }); it('scope', function(done) { var called = false; + function validator() { assert.equal('b', this.a); @@ -616,10 +623,10 @@ describe('schema', function() { } var Animal = new Schema({ - ferret: { type: Boolean, validate: validator } + ferret: {type: Boolean, validate: validator} }); - var err = Animal.path('ferret').doValidateSync(true, { a: 'b' }); + var err = Animal.path('ferret').doValidateSync(true, {a: 'b'}); assert.ifError(err); assert.equal(true, called); @@ -629,20 +636,21 @@ describe('schema', function() { it('ingore async', function(done) { function syncValidator(val) { - return val == 'sync'; + return val === 'sync'; } var called = false; + function asyncValidator(val, respond) { called = true; setTimeout(function() { - respond( val == 'async' ); + respond(val === 'async'); }, 0); } var Animal = new Schema({ - simple: { type: Boolean, validate: [ syncValidator, asyncValidator ] }, - simpleAsync: { type: Boolean, validate: asyncValidator } + simple: {type: Boolean, validate: [syncValidator, asyncValidator]}, + simpleAsync: {type: Boolean, validate: asyncValidator} }); assert.ifError(Animal.path('simple').doValidateSync('sync')); @@ -655,24 +663,25 @@ describe('schema', function() { it('subdoc', function(done) { function syncValidator(val) { - return val == 'sync'; + return val === 'sync'; } var called = false; + function asyncValidator(val, respond) { called = true; setTimeout(function() { - respond( val == 'async' ); + respond(val === 'async'); }, 0); } var Sub = new mongoose.Schema({ - title: { type: String, required: true }, - sync: { type: String, validate: [ syncValidator, asyncValidator ] }, - async: { type: String, validate: asyncValidator } + title: {type: String, required: true}, + sync: {type: String, validate: [syncValidator, asyncValidator]}, + async: {type: String, validate: asyncValidator} }); var SubsSchema = new mongoose.Schema({ - subs: [ Sub ] + subs: [Sub] }); var doc = new mongoose.Document({ @@ -680,7 +689,7 @@ describe('schema', function() { }, SubsSchema); var err = doc.validateSync(); - assert.ok( err instanceof mongoose.Error.ValidationError ); + assert.ok(err instanceof mongoose.Error.ValidationError); assert.equal('title', err.errors['subs.0.title'].path); @@ -691,7 +700,7 @@ describe('schema', function() { }, SubsSchema); var err1 = doc1.validateSync(); - assert.ok( err1 instanceof mongoose.Error.ValidationError ); + assert.ok(err1 instanceof mongoose.Error.ValidationError); assert.equal('title', err1.errors['subs.1.title'].path); @@ -702,7 +711,7 @@ describe('schema', function() { title: 'title2' }] }, SubsSchema); - assert.ifError( doc2.validateSync() ); + assert.ifError(doc2.validateSync()); var doc3 = new mongoose.Document({ @@ -715,7 +724,7 @@ describe('schema', function() { }, SubsSchema); var err3 = doc3.validateSync(); - assert.ok( err3 instanceof mongoose.Error.ValidationError ); + assert.ok(err3 instanceof mongoose.Error.ValidationError); assert.equal('Validator failed for path `sync` with value `fail`', err3.errors['subs.0.sync'].message); @@ -732,9 +741,9 @@ describe('schema', function() { }, SubsSchema); var err4 = doc4.validateSync(); - assert.ok( err4 instanceof mongoose.Error.ValidationError ); - assert.equal( err4.errors['subs.0.sync'], undefined ); - assert.ok( err4.errors['subs.1.sync'] ); + assert.ok(err4 instanceof mongoose.Error.ValidationError); + assert.equal(err4.errors['subs.0.sync'], undefined); + assert.ok(err4.errors['subs.1.sync']); assert.equal(false, called); @@ -744,8 +753,8 @@ describe('schema', function() { it('string enum', function(done) { var Test = new Schema({ - complex: { type: String, enum: ['a', 'b', undefined, 'c', null] }, - state: { type: String } + complex: {type: String, enum: ['a', 'b', undefined, 'c', null]}, + state: {type: String} }); // with SchemaTypes validate method @@ -769,7 +778,7 @@ describe('schema', function() { it('string regexp', function(done) { var Test = new Schema({ - simple: { type: String, match: /[a-z]/ } + simple: {type: String, match: /[a-z]/} }); assert.ifError(Test.path('simple').doValidateSync('az')); @@ -792,7 +801,7 @@ describe('schema', function() { it('number min and max', function(done) { var Tobi = new Schema({ - friends: { type: Number, max: 15, min: 5 } + friends: {type: Number, max: 15, min: 5} }); assert.ifError(Tobi.path('friends').doValidateSync(10)); @@ -855,7 +864,7 @@ describe('schema', function() { it('number required', function(done) { var Edwald = new Schema({ - friends: { type: Number, required: true } + friends: {type: Number, required: true} }); assert.ok(Edwald.path('friends').doValidateSync(null) instanceof ValidatorError); @@ -867,7 +876,7 @@ describe('schema', function() { it('date required', function(done) { var Loki = new Schema({ - birth_date: { type: Date, required: true } + birth_date: {type: Date, required: true} }); assert.ok(Loki.path('birth_date').doValidateSync(null) instanceof ValidatorError); @@ -879,10 +888,10 @@ describe('schema', function() { it('objectid required', function(done) { var Loki = new Schema({ - owner: { type: ObjectId, required: true } + owner: {type: ObjectId, required: true} }); - assert.ifError(Loki.path('owner').doValidateSync( new DocumentObjectId() )); + assert.ifError(Loki.path('owner').doValidateSync(new DocumentObjectId())); assert.ok(Loki.path('owner').doValidateSync(null) instanceof ValidatorError); assert.ok(Loki.path('owner').doValidateSync(undefined) instanceof ValidatorError); @@ -891,7 +900,7 @@ describe('schema', function() { it('array required', function(done) { var Loki = new Schema({ - likes: { type: Array, required: true } + likes: {type: Array, required: true} }); assert.ok(Loki.path('likes').doValidateSync(null) instanceof ValidatorError); @@ -903,7 +912,7 @@ describe('schema', function() { it('boolean required', function(done) { var Animal = new Schema({ - isFerret: { type: Boolean, required: true } + isFerret: {type: Boolean, required: true} }); assert.ok(Animal.path('isFerret').doValidateSync(null) instanceof ValidatorError); @@ -916,12 +925,12 @@ describe('schema', function() { it('mixed required', function(done) { var Animal = new Schema({ - characteristics: { type: Mixed, required: true } + characteristics: {type: Mixed, required: true} }); assert.ok(Animal.path('characteristics').doValidateSync(null) instanceof ValidatorError); assert.ok(Animal.path('characteristics').doValidateSync(undefined) instanceof ValidatorError); - assert.ifError(Animal.path('characteristics').doValidateSync( { aggresive: true } )); + assert.ifError(Animal.path('characteristics').doValidateSync({aggresive: true})); assert.ifError(Animal.path('characteristics').doValidateSync('none available')); done(); diff --git a/test/browser/types.buffer.test_.js b/test/browser/types.buffer.test_.js index cc83386f768..ceb77870f88 100644 --- a/test/browser/types.buffer.test_.js +++ b/test/browser/types.buffer.test_.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -10,7 +9,6 @@ var MongooseBuffer = mongoose.Types.Buffer; */ describe('types.buffer', function() { - it('test that a mongoose buffer behaves and quacks like a buffer', function(done) { var a = new MongooseBuffer; @@ -18,7 +16,7 @@ describe('types.buffer', function() { assert.equal(true, a.equals(a)); a = new MongooseBuffer([195, 188, 98, 101, 114]); - var b = new MongooseBuffer("buffer shtuffs are neat"); + var b = new MongooseBuffer('buffer shtuffs are neat'); var c = new MongooseBuffer('aGVsbG8gd29ybGQ=', 'base64'); var d = new MongooseBuffer(0); diff --git a/test/browser/types.number.test_.js b/test/browser/types.number.test_.js index d343f5a10ce..932f2894ac6 100644 --- a/test/browser/types.number.test_.js +++ b/test/browser/types.number.test_.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -10,7 +9,6 @@ var SchemaNumber = mongoose.Schema.Types.Number; */ describe('types.number', function() { - it('an empty string casts to null', function(done) { var n = new SchemaNumber(); assert.strictEqual(n.cast(''), null); @@ -85,5 +83,4 @@ describe('types.number', function() { assert.strictEqual(false, !!err, err); done(); }); - }); diff --git a/test/browser/types.objectid.test_.js b/test/browser/types.objectid.test_.js index 342b3720cc4..67c9c33cf0f 100644 --- a/test/browser/types.objectid.test_.js +++ b/test/browser/types.objectid.test_.js @@ -1,94 +1,91 @@ -/* eslint no-empty: 1 */ -(function() { - var ObjectId = mongoose.Types.ObjectId; - - describe('types.objectid', function() { - it('Should Correctly convert ObjectId to itself', function(done) { - var myObject, newObject; - var selfConvertion = (function() { - myObject = new ObjectId(); - newObject = ObjectId(myObject); - }); - - assert.doesNotThrow(selfConvertion); - assert.equal(myObject, newObject); - done(); - }); +var ObjectId = mongoose.Types.ObjectId; + +describe('types.objectid', function() { + it('Should Correctly convert ObjectId to itself', function(done) { + var myObject, newObject; + var selfConvertion = function() { + myObject = new ObjectId(); + newObject = ObjectId(myObject); + }; + + assert.doesNotThrow(selfConvertion); + assert.equal(myObject, newObject); + done(); + }); - it('ObjectId should correctly create objects', function(done) { - try { - ObjectId.createFromHexString('000000000000000000000001'); - ObjectId.createFromHexString('00000000000000000000001'); - assert.ok(false); - } catch (err) { - assert.ok(err != null); - } + it('ObjectId should correctly create objects', function(done) { + try { + ObjectId.createFromHexString('000000000000000000000001'); + ObjectId.createFromHexString('00000000000000000000001'); + assert.ok(false); + } catch (err) { + assert.ok(err !== null); + } - done(); - }); + done(); + }); - it('ObjectId should correctly retrieve timestamp', function(done) { - var testDate = new Date(); - var object1 = new ObjectId(); - assert.equal(Math.floor(testDate.getTime() / 1000), Math.floor(object1.getTimestamp().getTime() / 1000)); + it('ObjectId should correctly retrieve timestamp', function(done) { + var testDate = new Date(); + var object1 = new ObjectId(); + assert.equal(Math.floor(testDate.getTime() / 1000), Math.floor(object1.getTimestamp().getTime() / 1000)); - done(); - }); + done(); + }); - it('ObjectId should have a correct cached representation of the hexString', function(done) { - ObjectId.cacheHexString = true; - var a = new ObjectId; - var __id = a.__id; - assert.equal(__id, a.toHexString()); - - // hexString - a = new ObjectId(__id); - assert.equal(__id, a.toHexString()); - - // fromHexString - a = ObjectId.createFromHexString(__id); - assert.equal(a.__id, a.toHexString()); - assert.equal(__id, a.toHexString()); - - // number - var genTime = a.generationTime; - a = new ObjectId(genTime); - __id = a.__id; - assert.equal(__id, a.toHexString()); - - // generationTime - delete a.__id; - a.generationTime = genTime; - assert.equal(__id, a.toHexString()); - - // createFromTime - a = ObjectId.createFromTime(genTime); - __id = a.__id; - assert.equal(__id, a.toHexString()); - ObjectId.cacheHexString = false; - - done(); - }); + it('ObjectId should have a correct cached representation of the hexString', function(done) { + ObjectId.cacheHexString = true; + var a = new ObjectId; + var __id = a.__id; + assert.equal(__id, a.toHexString()); + + // hexString + a = new ObjectId(__id); + assert.equal(__id, a.toHexString()); + + // fromHexString + a = ObjectId.createFromHexString(__id); + assert.equal(a.__id, a.toHexString()); + assert.equal(__id, a.toHexString()); + + // number + var genTime = a.generationTime; + a = new ObjectId(genTime); + __id = a.__id; + assert.equal(__id, a.toHexString()); + + // generationTime + delete a.__id; + a.generationTime = genTime; + assert.equal(__id, a.toHexString()); + + // createFromTime + a = ObjectId.createFromTime(genTime); + __id = a.__id; + assert.equal(__id, a.toHexString()); + ObjectId.cacheHexString = false; + + done(); + }); - it('Should fail to create ObjectId due to illegal hex code', function(done) { - try { - new ObjectId("zzzzzzzzzzzzzzzzzzzzzzzz"); - assert.ok(false); - } catch (err) { - } - - assert.equal(false, ObjectId.isValid(null)); - assert.equal(false, ObjectId.isValid({})); - assert.equal(false, ObjectId.isValid([])); - assert.equal(false, ObjectId.isValid(true)); - assert.equal(true, ObjectId.isValid(0)); - assert.equal(false, ObjectId.isValid("invalid")); - assert.equal(true, ObjectId.isValid("zzzzzzzzzzzz")); - assert.equal(false, ObjectId.isValid("zzzzzzzzzzzzzzzzzzzzzzzz")); - assert.equal(true, ObjectId.isValid("000000000000000000000000")); - - done(); + it('Should fail to create ObjectId due to illegal hex code', function(done) { + assert.throws(function() { + new ObjectId('zzzzzzzzzzzzzzzzzzzzzzzz'); }); + done(); }); -})(); + it('Should validate ObjectId', function(done) { + assert.equal(false, ObjectId.isValid(null)); + assert.equal(false, ObjectId.isValid({})); + assert.equal(false, ObjectId.isValid([])); + assert.equal(false, ObjectId.isValid(true)); + assert.equal(true, ObjectId.isValid(0)); + assert.equal(false, ObjectId.isValid('invalid')); + assert.equal(true, ObjectId.isValid('zzzzzzzzzzzz')); + assert.equal(false, ObjectId.isValid('zzzzzzzzzzzzzzzzzzzzzzzz')); + assert.equal(true, ObjectId.isValid('000000000000000000000000')); + + done(); + }); +}); diff --git a/test/cast.test.js b/test/cast.test.js new file mode 100644 index 00000000000..26289e8822c --- /dev/null +++ b/test/cast.test.js @@ -0,0 +1,103 @@ +/** + * Module dependencies. + */ + +var Schema = require('../lib/schema'); +var assert = require('power-assert'); +var cast = require('../lib/cast'); +var ObjectId = require('bson').ObjectId; + +describe('cast: ', function() { + describe('when casting an array', function() { + it('casts array with ObjectIds to $in query', function(done) { + var schema = new Schema({x: Schema.Types.ObjectId}); + var ids = [new ObjectId(), new ObjectId()]; + assert.deepEqual(cast(schema, {x: ids}), { x: { $in: ids } }); + done(); + }); + + it('casts array with ObjectIds to $in query when values are strings', function(done) { + var schema = new Schema({x: Schema.Types.ObjectId}); + var ids = [new ObjectId(), new ObjectId()]; + assert.deepEqual(cast(schema, {x: ids.map(String)}), { x: { $in: ids } }); + done(); + }); + + it('throws when ObjectIds not valid', function(done) { + var schema = new Schema({x: Schema.Types.ObjectId}); + var ids = [123, 456, 'asfds']; + assert.throws(function() { + cast(schema, {x: ids}); + }, /Cast to ObjectId failed/); + done(); + }); + + it('casts array with Strings to $in query', function(done) { + var schema = new Schema({x: String}); + var strings = ['bleep', 'bloop']; + assert.deepEqual(cast(schema, {x: strings}), { x: { $in: strings } }); + done(); + }); + + it('casts array with Strings when necessary', function(done) { + var schema = new Schema({x: String}); + var strings = [123, 456]; + assert.deepEqual(cast(schema, {x: strings}), { x: { $in: strings.map(String) } }); + done(); + }); + + it('casts array with Numbers to $in query', function(done) { + var schema = new Schema({x: Number}); + var numbers = [42, 25]; + assert.deepEqual(cast(schema, {x: numbers}), { x: { $in: numbers } }); + done(); + }); + + it('casts array with Numbers to $in query when values are strings', function(done) { + var schema = new Schema({x: Number}); + var numbers = ['42', '25']; + assert.deepEqual(cast(schema, {x: numbers}), { x: { $in: numbers.map(Number) } }); + done(); + }); + + it('throws when Numbers are not valid', function(done) { + var schema = new Schema({x: Number}); + var numbers = [123, 456, 'asfds']; + assert.throws(function() { + cast(schema, {x: numbers}); + }, /Cast to number failed for value "asfds"/); + done(); + }); + }); + + describe('bitwise query operators: ', function() { + it('with a number', function(done) { + var schema = new Schema({x: Buffer}); + assert.deepEqual(cast(schema, {x: {$bitsAllClear: 3}}), + {x: {$bitsAllClear: 3}}); + done(); + }); + + it('with an array', function(done) { + var schema = new Schema({x: Buffer}); + assert.deepEqual(cast(schema, {x: {$bitsAllSet: [2, '3']}}), + {x: {$bitsAllSet: [2, 3]}}); + done(); + }); + + it('with a buffer', function(done) { + var schema = new Schema({x: Number}); + assert.deepEqual(cast(schema, {x: {$bitsAnyClear: new Buffer([3])}}), + {x: {$bitsAnyClear: new Buffer([3])}}); + done(); + }); + + it('throws when invalid', function(done) { + var schema = new Schema({x: Number}); + assert.throws(function() { + cast(schema, {x: {$bitsAnySet: 'Not a number'}}); + }, /Cast to number failed/); + done(); + }); + }); +}); diff --git a/test/collection.capped.test.js b/test/collection.capped.test.js index 454eb32d7a4..9650f121a28 100644 --- a/test/collection.capped.test.js +++ b/test/collection.capped.test.js @@ -1,19 +1,18 @@ - /** * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , Schema = mongoose.Schema - , random = require('../lib/utils').random; +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema, + random = require('../lib/utils').random; /** * setup */ -var capped = new Schema({ key: 'string', val: 'number' }); -capped.set('capped', { size: 1000 }); +var capped = new Schema({key: 'string', val: 'number'}); +capped.set('capped', {size: 1000}); var coll = 'capped_' + random(); /** @@ -21,13 +20,22 @@ var coll = 'capped_' + random(); */ describe('collections: capped:', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + it('schemas should have option size', function(done) { assert.ok(capped.options.capped); - assert.equal(1000, capped.options.capped.size); + assert.equal(capped.options.capped.size, 1000); done(); }); it('creation', function(done) { - var db = start(); var Capped = db.model('Capped', capped, coll); Capped.collection.isCapped(function(err, isCapped) { assert.ifError(err); @@ -35,41 +43,42 @@ describe('collections: capped:', function() { // use the existing capped collection in the db (no coll creation) var Capped2 = db.model('Capped2', capped, coll); - Capped2.collection.isCapped(function(err, isCapped) { - assert.ifError(err); - assert.ok(isCapped, 'should reuse the capped collection in the db'); + Capped2.collection.isCapped(function(err1, isCapped1) { + assert.ifError(err1); + assert.ok(isCapped1, 'should reuse the capped collection in the db'); assert.equal(Capped.collection.name, Capped2.collection.name); - db.close(done); + done(); }); }); }); it('creation using a number', function(done) { - var db = start(); - var schema = new Schema({ key: 'string' }, { capped: 8192 }); + var schema = new Schema({key: 'string'}, {capped: 8192}); var Capped = db.model('Capped3', schema); Capped.collection.options(function(err, options) { assert.ifError(err); assert.ok(options.capped, 'should create a capped collection'); - assert.equal(8192, options.size); - db.close(done); + assert.equal(options.size, 8192); + done(); }); }); it('attempting to use existing non-capped collection as capped emits error', function(done) { - var db = start(); - var opts = { safe: true }; + db = start(); + var opts = {}; var conn = 'capped_existing_' + random(); db.on('open', function() { db.db.createCollection(conn, opts, function(err) { - if (err) db.close(); + if (err) { + db.close(); + } assert.ifError(err); var timer; - db.on('error', function(err) { + db.on('error', function(err1) { clearTimeout(timer); db.close(); - assert.ok(/non-capped collection exists/.test(err)); + assert.ok(/non-capped collection exists/.test(err1)); done(); }); diff --git a/test/collection.test.js b/test/collection.test.js index eb846bbf91d..3b2bf1bea00 100644 --- a/test/collection.test.js +++ b/test/collection.test.js @@ -1,25 +1,26 @@ - -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , Collection = require('../lib/collection'); +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Collection = require('../lib/collection'); describe('collections:', function() { it('should buffer commands until connection is established', function(done) { - var db = mongoose.createConnection() - , collection = db.collection('test-buffering-collection') - , connected = false - , inserted = false - , pending = 2; + var db = mongoose.createConnection(), + collection = db.collection('test-buffering-collection'), + connected = false, + inserted = false, + pending = 2; function finish() { - if (--pending) return; + if (--pending) { + return; + } assert.ok(connected); assert.ok(inserted); done(); } - collection.insert({ }, { safe: true }, function() { + collection.insert({}, {safe: true}, function() { assert.ok(connected); inserted = true; db.close(); @@ -34,8 +35,8 @@ describe('collections:', function() { }); it('methods should that throw (unimplemented)', function(done) { - var collection = new Collection('test', mongoose.connection) - , thrown = false; + var collection = new Collection('test', mongoose.connection), + thrown = false; try { collection.getIndexes(); diff --git a/test/colors.js b/test/colors.js new file mode 100644 index 00000000000..6ab35c869d9 --- /dev/null +++ b/test/colors.js @@ -0,0 +1,129 @@ +/** + * Module dependencies. + */ + +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema, + MongooseDocumentArray = mongoose.Types.DocumentArray, + EmbeddedDocument = require('../lib/types/embedded'), + DocumentArray = require('../lib/types/documentarray'); + +/** + * setup + */ + +var test = new Schema({ + string: String, + number: Number, + date: { + type: Date, + default: Date.now + } +}); + +function TestDoc(schema) { + var Subdocument = function() { + EmbeddedDocument.call(this, {}, new DocumentArray); + }; + + /** + * Inherits from EmbeddedDocument. + */ + + Subdocument.prototype.__proto__ = EmbeddedDocument.prototype; + + /** + * Set schema. + */ + + Subdocument.prototype.$__setSchema(schema || test); + + return Subdocument; +} + +/** + * Test. + */ + +describe('debug: colors', function() { + var db; + var Test; + + before(function() { + db = start(); + Test = db.model('Test', test, 'TEST'); + }); + + after(function(done) { + db.close(done); + }); + + it('Document', function(done) { + var date = new Date(); + + Test.create([{ + string: 'qwerty', + number: 123, + date: date + }, { + string: 'asdfgh', + number: 456, + date: date + }, { + string: 'zxcvbn', + number: 789, + date: date + }], function(err) { + assert.ifError(err); + Test + .find() + .lean(false) + .exec(function(err, docs) { + assert.ifError(err); + + var colorfull = require('util').inspect(docs, { + depth: null, + colors: true + }); + + var colorless = require('util').inspect(docs, { + depth: null, + colors: false + }); + + // console.log(colorfull, colorless); + + assert.notEqual(colorfull, colorless); + + done(); + }); + }); + }); + + it('MongooseDocumentArray', function() { + var Subdocument = TestDoc(); + + var sub1 = new Subdocument(); + sub1.string = 'string'; + sub1.number = 12345; + sub1.date = new Date(); + + var docs = new MongooseDocumentArray([sub1]); + + var colorfull = require('util').inspect(docs, { + depth: null, + colors: true + }); + + var colorless = require('util').inspect(docs, { + depth: null, + colors: false + }); + + // console.log(colorfull, colorless); + + assert.notEqual(colorfull, colorless); + }); +}); diff --git a/test/common.js b/test/common.js index 03403a9e419..a00a381c3ad 100644 --- a/test/common.js +++ b/test/common.js @@ -1,43 +1,47 @@ - /** * Module dependencies. */ -var mongoose = require('../') - , Collection = mongoose.Collection - , assert = require('assert') - , queryCount = 0 - , opened = 0 - , closed = 0; +Error.stackTraceLimit = 10; + +var Server = require('mongodb-topology-manager').Server; +var mongoose = require('../'); +var Collection = mongoose.Collection; +var assert = require('power-assert'); +var queryCount = 0; +var opened = 0; +var closed = 0; +var server; -if (process.env.D === '1') +if (process.env.D === '1') { mongoose.set('debug', true); +} /** * Override all Collection related queries to keep count */ -[ 'ensureIndex' - , 'findAndModify' - , 'findOne' - , 'find' - , 'insert' - , 'save' - , 'update' - , 'remove' - , 'count' - , 'distinct' - , 'isCapped' - , 'options' +[ + 'createIndex', + 'ensureIndex', + 'findAndModify', + 'findOne', + 'find', + 'insert', + 'save', + 'update', + 'remove', + 'count', + 'distinct', + 'isCapped', + 'options' ].forEach(function(method) { - var oldMethod = Collection.prototype[method]; Collection.prototype[method] = function() { queryCount++; return oldMethod.apply(this, arguments); }; - }); /** @@ -85,7 +89,9 @@ module.exports = function(options) { var conn = mongoose.createConnection(uri, options); - if (noErrorListener) return conn; + if (noErrorListener) { + return conn; + } conn.on('error', function(err) { assert.ok(err); @@ -94,6 +100,23 @@ module.exports = function(options) { return conn; }; +/*! + * ignore + */ + +before(function(done) { + module.exports.mongodVersion(function(err, version) { + if (err) { + return done(err); + } + var mongo36 = version[0] > 3 || (version[0] === 3 && version[1] >= 6); + if (mongo36) { + mongoose.set('usePushEach', true); + } + done(); + }); +}); + /*! * testing uri */ @@ -117,15 +140,21 @@ module.exports.mongodVersion = function(cb) { db.on('open', function() { var admin = db.db.admin(); admin.serverStatus(function(err, info) { - if (err) return cb(err); - var version = info.version.split('.').map(function(n) { return parseInt(n, 10); }); - cb(null, version); + if (err) { + return cb(err); + } + var version = info.version.split('.').map(function(n) { + return parseInt(n, 10); + }); + db.close(function() { + cb(null, version); + }); }); }); }; function dropDBs(done) { - var db = module.exports(); + var db = module.exports({ noErrorListener: true }); db.once('open', function() { // drop the default test database db.db.dropDatabase(function() { @@ -133,10 +162,12 @@ function dropDBs(done) { db2.db.dropDatabase(function() { // drop mongos test db if exists var mongos = process.env.MONGOOSE_MULTI_MONGOS_TEST_URI; - if (!mongos) return done(); + if (!mongos) { + return done(); + } - var db = mongoose.connect(mongos, {mongos: true }); + var db = mongoose.connect(mongos, {mongos: true}); db.once('open', function() { db.db.dropDatabase(done); }); @@ -145,11 +176,23 @@ function dropDBs(done) { }); } +before(function() { + return server.purge(); +}); + +after(function() { + this.timeout(15000); + + return server.stop(); +}); + before(function(done) { this.timeout(10 * 1000); dropDBs(done); }); -after(function(done) { - this.timeout(10 * 1000); - dropDBs(done); + +module.exports.server = server = new Server('mongod', { + port: 27000, + dbpath: './data/db/27000', + storageEngine: 'mmapv1' }); diff --git a/test/connection.auth.test.js b/test/connection.auth.test.js index e43ff7d8fe5..86897c12cf0 100644 --- a/test/connection.auth.test.js +++ b/test/connection.auth.test.js @@ -1,18 +1,16 @@ - var start = require('./common'); var mongoose = start.mongoose; -var assert = require('assert'); +var assert = require('power-assert'); describe('connection:', function() { describe('supports authSource', function() { - it('in querystring', function(done) { var conn = mongoose.createConnection(); conn._open = function() { assert.ok(conn.options); assert.ok(conn.options.auth); - assert.equal('users', conn.options.auth.authSource); - done(); + assert.equal(conn.options.auth.authSource, 'users'); + conn.close(done); }; conn.open(start.uri + '?authSource=users'); }); @@ -22,10 +20,10 @@ describe('connection:', function() { conn._open = function() { assert.ok(conn.options); assert.ok(conn.options.auth); - assert.equal('users', conn.options.auth.authSource); - done(); + assert.equal(conn.options.auth.authSource, 'users'); + conn.close(done); }; - conn.open(start.uri, { auth: { authSource: 'users' }}); + conn.open(start.uri, {auth: {authSource: 'users'}}); }); }); }); diff --git a/test/connection.reconnect.test.js b/test/connection.reconnect.test.js index c60b5e09e86..124f6186c4d 100644 --- a/test/connection.reconnect.test.js +++ b/test/connection.reconnect.test.js @@ -1,4 +1,3 @@ - var start = require('./common'); var mongoose = start.mongoose; @@ -13,9 +12,9 @@ describe('connection: manual reconnect with authReconnect: false', function() { var db = mongoose.createConnection(); - db.open(start.uri, { server: { auto_reconnect: false }}); + db.open(start.uri, {server: {auto_reconnect: false}}); - var M = db.model('autoReconnect', { name: String }); + var M = db.model('autoReconnect', {name: String}); var open = false; var times = 0; @@ -29,16 +28,22 @@ describe('connection: manual reconnect with authReconnect: false', function() { db.on('disconnected', function() { open = false; setTimeout(function() { - db.open(start.uri, { server: { auto_reconnect: false }}); + db.open(start.uri, {server: {auto_reconnect: false}}); }, 30); }); function hit() { - if (!open) return; - M.create({ name: times }, function(err, doc) { - if (err) return complete(err); - M.findOne({ _id: doc._id }, function(err) { - if (err) return complete(err); + if (!open) { + return; + } + M.create({name: times}, function(err, doc) { + if (err) { + return complete(err); + } + M.findOne({_id: doc._id}, function(err) { + if (err) { + return complete(err); + } if (times > 1) { return complete(); } @@ -52,7 +57,9 @@ describe('connection: manual reconnect with authReconnect: false', function() { } function complete(err) { - if (complete.ran) return ; + if (complete.ran) { + return; + } complete.ran = 1; done(err); } diff --git a/test/connection.test.js b/test/connection.test.js index 3129c199843..0d905b3b48e 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1,56 +1,394 @@ - /** * Module dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , Schema = mongoose.Schema - , random = require('../lib/utils').random; +var Promise = require('bluebird'); +var Q = require('q'); +var assert = require('power-assert'); +var muri = require('muri'); +var random = require('../lib/utils').random; +var server = require('./common').server; +var start = require('./common'); + +var mongoose = start.mongoose; +var Schema = mongoose.Schema; /** * Test. */ describe('connections:', function() { + this.timeout(10000); + describe('useMongoClient/openUri (gh-5304)', function() { + it('with mongoose.connect()', function(done) { + var conn = mongoose.connect('mongodb://localhost:27017/mongoosetest', { + useMongoClient: true + }); + assert.equal(conn.constructor.name, 'NativeConnection'); + + conn.then(function(conn) { + assert.equal(conn.constructor.name, 'NativeConnection'); + assert.equal(conn.host, 'localhost'); + assert.equal(conn.port, 27017); + assert.equal(conn.name, 'mongoosetest'); + + return mongoose.disconnect().then(function() { done(); }); + }).catch(done); + }); + + it('with mongoose.createConnection()', function(done) { + var conn = mongoose.createConnection('mongodb://localhost:27017/mongoosetest', { + useMongoClient: true + }); + assert.equal(conn.constructor.name, 'NativeConnection'); + + var Test = conn.model('Test', new Schema({ name: String })); + assert.equal(Test.modelName, 'Test'); + + var findPromise = Test.findOne(); + + assert.equal(typeof conn.catch, 'function'); + + conn. + then(function(conn) { + assert.equal(conn.constructor.name, 'NativeConnection'); + assert.equal(conn.host, 'localhost'); + assert.equal(conn.port, 27017); + assert.equal(conn.name, 'mongoosetest'); + + return findPromise; + }). + then(function() { + return mongoose.disconnect().then(function() { done(); }); + }). + catch(done); + }); + + it('with autoIndex (gh-5423)', function(done) { + var promise = mongoose.createConnection('mongodb://localhost:27017/mongoosetest', { + useMongoClient: true, + autoIndex: false + }); + + promise.then(function(conn) { + assert.strictEqual(conn.config.autoIndex, false); + assert.deepEqual(conn._connectionOptions, {}); + done(); + }).catch(done); + }); + + it('resolving with q (gh-5714)', function(done) { + var bootMongo = Q.defer(); + + var conn = mongoose.createConnection('mongodb://localhost:27017/mongoosetest', { + useMongoClient: true + }); + + conn.on('connected', function() { + bootMongo.resolve(this); + }); + + bootMongo.promise.then(function(_conn) { + assert.equal(_conn, conn); + done(); + }).catch(done); + }); + + describe('connection events', function() { + beforeEach(function() { + this.timeout(10000); + return server.start(). + then(function() { return server.purge(); }); + }); + + afterEach(function() { + this.timeout(10000); + return server.stop(); + }); + + it('disconnected (gh-5498) (gh-5524)', function(done) { + this.timeout(25000); + + var conn; + var numConnected = 0; + var numDisconnected = 0; + var numReconnected = 0; + var numReconnect = 0; + var numClose = 0; + conn = mongoose.createConnection('mongodb://localhost:27000/mongoosetest', { + useMongoClient: true + }); + + conn.on('connected', function() { + ++numConnected; + }); + conn.on('disconnected', function() { + ++numDisconnected; + }); + conn.on('reconnect', function() { + ++numReconnect; + }); + // Same as `reconnect`, just for backwards compat + conn.on('reconnected', function() { + ++numReconnected; + }); + conn.on('close', function() { + ++numClose; + }); + + conn. + then(function() { + assert.equal(conn.readyState, conn.states.connected); + assert.equal(numConnected, 1); + return server.stop(); + }). + then(function() { + return new Promise(function(resolve) { + setTimeout(function() { resolve(); }, 100); + }); + }). + then(function() { + assert.equal(conn.readyState, conn.states.disconnected); + assert.equal(numDisconnected, 1); + assert.equal(numReconnected, 0); + assert.equal(numReconnect, 0); + }). + then(function() { + return server.start(); + }). + then(function() { + return new Promise(function(resolve) { + setTimeout(function() { resolve(); }, 4000); + }); + }). + then(function() { + assert.equal(conn.readyState, conn.states.connected); + assert.equal(numDisconnected, 1); + assert.equal(numReconnected, 1); + assert.equal(numReconnect, 1); + assert.equal(numClose, 0); + + conn.close(); + done(); + }). + catch(done); + }); + + it('reconnectFailed (gh-4027)', function(done) { + this.timeout(25000); + + var conn; + var numReconnectFailed = 0; + var numConnected = 0; + var numDisconnected = 0; + var numReconnected = 0; + conn = mongoose.createConnection('mongodb://localhost:27000/mongoosetest', { + useMongoClient: true, + reconnectTries: 3, + reconnectInterval: 100 + }); + + conn.on('connected', function() { + ++numConnected; + }); + conn.on('disconnected', function() { + ++numDisconnected; + }); + conn.on('reconnected', function() { + ++numReconnected; + }); + conn.on('reconnectFailed', function() { + ++numReconnectFailed; + }); + + conn. + then(function() { + assert.equal(numConnected, 1); + return server.stop(); + }). + then(function() { + return new Promise(function(resolve) { + setTimeout(function() { resolve(); }, 100); + }); + }). + then(function() { + assert.equal(numDisconnected, 1); + assert.equal(numReconnected, 0); + assert.equal(numReconnectFailed, 0); + }). + then(function() { + return new Promise(function(resolve) { + setTimeout(function() { resolve(); }, 4000); + }); + }). + then(function() { + assert.equal(numDisconnected, 1); + assert.equal(numReconnected, 0); + assert.equal(numReconnectFailed, 1); + }). + then(function() { + return server.start(); + }). + then(function() { + return new Promise(function(resolve) { + setTimeout(function() { resolve(); }, 2000); + }); + }). + then(function() { + assert.equal(numDisconnected, 1); + assert.equal(numReconnected, 0); + assert.equal(numReconnectFailed, 1); + + conn.close(); + done(); + }). + catch(done); + }); + + it('timeout (gh-4513)', function(done) { + this.timeout(25000); + + var conn; + var numTimeout = 0; + var numDisconnected = 0; + conn = mongoose.createConnection('mongodb://localhost:27000/mongoosetest', { + useMongoClient: true, + socketTimeoutMS: 100, + poolSize: 1 + }); + + conn.on('timeout', function() { + ++numTimeout; + }); + + conn.on('disconnected', function() { + ++numDisconnected; + }); + + var Model = conn.model('gh4513', new Schema()); + + conn. + then(function() { + assert.equal(conn.readyState, conn.states.connected); + return Model.create({}); + }). + then(function() { + return Model.find({ $where: 'sleep(250) || true' }); + }). + then(function() { + done(new Error('expected timeout')); + }). + catch(function(error) { + assert.ok(error); + assert.ok(error.message.indexOf('timed out'), error.message); + // TODO: if autoReconnect is false, we might not actually be + // connected. See gh-5634 + assert.equal(conn.readyState, conn.states.connected); + assert.equal(numTimeout, 1); + assert.equal(numDisconnected, 0); + + conn.close(); + done(); + }); + }); + }); + }); + + describe('helpers', function() { + var conn; + + before(function() { + conn = mongoose.connect('mongodb://localhost:27017/mongoosetest_2', { + useMongoClient: true + }); + return conn; + }); + + it('dropDatabase()', function(done) { + conn.dropDatabase(function(error) { + assert.ifError(error); + done(); + }); + }); + + it('dropCollection()', function() { + return conn.db.collection('test').insertOne({ x: 1 }). + then(function() { + return conn.dropCollection('test'); + }). + then(function() { + return conn.db.collection('test').findOne(); + }). + then(function(doc) { + assert.ok(!doc); + }); + }); + + it('createCollection()', function() { + return conn.dropDatabase(). + then(function() { + return conn.createCollection('gh5712', { + capped: true, + size: 1024 + }); + }). + then(function() { + return conn.db.listCollections().toArray(); + }). + then(function(collections) { + var names = collections.map(function(c) { return c.name; }); + assert.ok(names.indexOf('gh5712') !== -1); + assert.ok(collections[names.indexOf('gh5712')].options.capped); + return conn.createCollection('gh5712_0'); + }). + then(function() { + return conn.db.listCollections().toArray(); + }). + then(function(collections) { + var names = collections.map(function(c) { return c.name; }); + assert.ok(names.indexOf('gh5712') !== -1); + }); + }); + }); + it('should allow closing a closed connection', function(done) { var db = mongoose.createConnection(); - assert.equal(0, db.readyState); + assert.equal(db.readyState, 0); db.close(done); }); it('should accept mongodb://localhost/fake', function(done) { var db = mongoose.createConnection('mongodb://localhost/fake'); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('primary', db.options.db.read_preference); - assert.equal(undefined, db.pass); - assert.equal(undefined, db.user); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27017, db.port); + db.on('error', function() { + }); + assert.ok(db instanceof mongoose.Connection); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.pass, undefined); + assert.equal(db.user, undefined); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27017); db.close(done); }); it('should accept replicaSet query param', function(done) { var db = mongoose.createConnection('mongodb://localhost/fake?replicaSet=rs0'); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('primary', db.options.db.read_preference); - assert.equal(undefined, db.pass); - assert.equal(undefined, db.user); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.pass, void 0); + assert.equal(db.user, void 0); assert.equal('fake', db.name); - assert.deepEqual([{ host: 'localhost', port: 27017 }], db.hosts); + assert.deepEqual(db.hosts, [{host: 'localhost', port: 27017}]); // Should be a replica set assert.ok(db.replica); @@ -60,115 +398,127 @@ describe('connections:', function() { it('should accept mongodb://localhost:27000/fake', function(done) { var db = mongoose.createConnection('mongodb://localhost:27000/fake'); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(27000, db.port); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.port, 27000); db.close(); done(); }); it('should accept mongodb://aaron:psw@localhost:27000/fake', function(done) { var db = mongoose.createConnection('mongodb://aaron:psw@localhost:27000/fake'); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('psw', db.pass); - assert.equal('aaron', db.user); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.pass, 'psw'); + assert.equal(db.user, 'aaron'); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); db.close(); done(); }); it('should accept mongodb://aaron:psw@localhost:27000/fake with db options', function(done) { - var db = mongoose.createConnection('mongodb://aaron:psw@localhost:27000/fake', { db: { forceServerObjectId: true }}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); + var db = mongoose.createConnection('mongodb://aaron:psw@localhost:27000/fake', {db: {forceServerObjectId: true}}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); db.close(); done(); }); it('should accept mongodb://aaron:psw@localhost:27000/fake with server options', function(done) { - var db = mongoose.createConnection('mongodb://aaron:psw@localhost:27000/fake', { server: { auto_reconnect: false }}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(false, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); + var db = mongoose.createConnection('mongodb://aaron:psw@localhost:27000/fake', {server: {auto_reconnect: false}}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); db.close(); done(); }); it('should accept unix domain sockets', function(done) { - var db = mongoose.createConnection('mongodb://aaron:psw@/tmp/mongodb-27017.sock/fake', { server: { auto_reconnect: false }}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(false, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('/tmp/mongodb-27017.sock', db.host); - assert.equal('psw', db.pass); - assert.equal('aaron', db.user); + var db = mongoose.createConnection('mongodb://aaron:psw@/tmp/mongodb-27017.sock/fake', {server: {auto_reconnect: false}}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, '/tmp/mongodb-27017.sock'); + assert.equal(db.pass, 'psw'); + assert.equal(db.user, 'aaron'); db.close(); done(); }); describe('re-opening a closed connection', function() { var mongos = process.env.MONGOOSE_SHARD_TEST_URI; - if (!mongos) return; + if (!mongos) { + return; + } var mongod = 'mongodb://localhost:27017'; - var repl1 = process.env.MONGOOSE_SET_TEST_URI; - var repl2 = repl1.replace("mongodb://", "").split(','); - repl2.push(repl2.shift()); - repl2 = "mongodb://" + repl2.join(','); - describe('with different host/port', function() { it('non-replica set', function(done) { var db = mongoose.createConnection(); db.open(mongod, function(err) { - if (err) return done(err); + if (err) { + return done(err); + } var port1 = db.port; var db1 = db.db; db.close(function(err) { - if (err) return done(err); + if (err) { + return done(err); + } db.open(mongos, function(err) { - if (err) return done(err); + if (err) { + return done(err); + } assert.notEqual(port1, db.port); assert.ok(db1 !== db.db); - assert.ok(db1.serverConfig.port != db.db.serverConfig.port); + assert.ok(db1.serverConfig.port !== db.db.serverConfig.port); var port2 = db.port; var db2 = db.db; db.close(function(err) { - if (err) return done(err); + if (err) { + return done(err); + } db.open(mongod, function(err) { - if (err) return done(err); + if (err) { + return done(err); + } assert.notEqual(port2, db.port); assert.ok(db2 !== db.db); - assert.ok(db2.serverConfig.port != db.db.serverConfig.port); + assert.ok(db2.serverConfig.port !== db.db.serverConfig.port); db.close(done); }); @@ -177,128 +527,101 @@ describe('connections:', function() { }); }); }); + }); + }); - it('replica set', function(done) { - var db = mongoose.createConnection(); - - db.openSet(repl1, function(err) { - if (err) return done(err); - - var hosts = db.hosts.slice(); - var db1 = db.db; - - db.close(function(err) { - if (err) return done(err); - - db.openSet(repl2, function(err) { - if (err) return done(err); - - db.hosts.forEach(function(host, i) { - assert.notEqual(host.port, hosts[i].port); - }); - assert.ok(db1 !== db.db); - - hosts = db.hosts.slice(); - var db2 = db.db; - - db.close(function(err) { - if (err) return done(err); - - db.openSet(repl1, function(err) { - if (err) return done(err); - - db.hosts.forEach(function(host, i) { - assert.notEqual(host.port, hosts[i].port); - }); - assert.ok(db2 !== db.db); + describe('errors', function() { + it('.catch() means error does not get thrown (gh-5229)', function(done) { + var db = mongoose.createConnection(); - db.close(); - done(); - }); - }); - }); - }); - }); + db.open('fail connection').catch(function(error) { + assert.ok(error); + done(); }); }); }); describe('should accept separated args with options', function() { it('works', function(done) { - var db = mongoose.createConnection('127.0.0.1', 'faker', 28000, { server: { auto_reconnect: true }}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('faker', db.name); - assert.equal('127.0.0.1', db.host); - assert.equal(28000, db.port); + var db = mongoose.createConnection('127.0.0.1', 'faker', 28000, {server: {auto_reconnect: true}}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'faker'); + assert.equal(db.host, '127.0.0.1'); + assert.equal(db.port, 28000); db.close(); - db = mongoose.createConnection('127.0.0.1', 'faker', { blah: 1 }); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('faker', db.name); - assert.equal('127.0.0.1', db.host); - assert.equal(27017, db.port); - assert.equal(1, db.options.blah); + db = mongoose.createConnection('127.0.0.1', 'faker', {blah: 1}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'faker'); + assert.equal(db.host, '127.0.0.1'); + assert.equal(db.port, 27017); + assert.equal(db.options.blah, 1); db.close(); done(); }); it('including user/pass', function(done) { var db = mongoose.createConnection('localhost', 'fake', 27000, {user: 'aaron', pass: 'psw'}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal('psw', db.pass); - assert.equal('aaron', db.user); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, 'psw'); + assert.equal(db.user, 'aaron'); db.close(); done(); }); it('but fails when passing user and no pass with standard authentication', function(done) { var db = mongoose.createConnection('localhost', 'fake', 27000, {user: 'no_pass'}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal(undefined, db.pass); - assert.equal(undefined, db.user); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, undefined); + assert.equal(db.user, undefined); db.close(); done(); }); it('but passes when passing user and no pass with the MONGODB-X509 authMechanism', function(done) { var db = mongoose.createConnection('localhost', 'fake', 27000, {user: 'no_pass', auth: {authMechanism: 'MONGODB-X509'}}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal(undefined, db.pass); - assert.equal('no_pass', db.user); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, undefined); + assert.equal(db.user, 'no_pass'); db.close(); done(); }); @@ -307,43 +630,46 @@ describe('connections:', function() { describe('should accept separated args without options', function() { it('works', function(done) { var db = mongoose.createConnection('127.0.0.1', 'faker', 28001); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('faker', db.name); - assert.equal('127.0.0.1', db.host); - assert.equal(28001, db.port); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'faker'); + assert.equal(db.host, '127.0.0.1'); + assert.equal(db.port, 28001); db.close(); db = mongoose.createConnection('127.0.0.1', 'faker'); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('faker', db.name); - assert.equal('127.0.0.1', db.host); - assert.equal(27017, db.port); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'faker'); + assert.equal(db.host, '127.0.0.1'); + assert.equal(db.port, 27017); db.close(); done(); }); it('and accept user/pass in hostname', function(done) { var db = mongoose.createConnection('aaron:psw@localhost', 'fake', 27000); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal('psw', db.pass); - assert.equal('aaron', db.user); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, 'psw'); + assert.equal(db.user, 'aaron'); db.close(); done(); }); @@ -353,84 +679,89 @@ describe('connections:', function() { describe('for replica sets', function() { it('work', function(done) { var conn = 'mongodb://localhost/fake?autoReconnect=false&poolSize=2' - + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' - + '&retries=10&reconnectWait=5&rs_name=replworld&readSecondary=true' - + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true' - + '&wtimeoutMS=80&readPreference=nearest&readPreferenceTags=' - + 'dc:ny,rack:1&readPreferenceTags=dc:sf'; + + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' + + '&retries=10&reconnectWait=5&rs_name=replworld&readSecondary=true' + + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true' + + '&wtimeoutMS=80&readPreference=nearest&readPreferenceTags=' + + 'dc:ny,rack:1&readPreferenceTags=dc:sf&sslValidate=true'; var db = mongoose.createConnection(conn); - db.on('error', function() {}); + db.on('error', function() { + }); db.close(); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal('object', typeof db.options.server.socketOptions); - assert.equal('object', typeof db.options.db); - assert.equal('object', typeof db.options.replset); - assert.equal('object', typeof db.options.replset.socketOptions); - assert.equal(false, db.options.server.auto_reconnect); - assert.equal(2, db.options.server.poolSize); - assert.equal(false, db.options.server.slave_ok); - assert.equal(true, db.options.server.ssl); - assert.equal(true, db.options.replset.ssl); - assert.equal(10, db.options.server.socketOptions.socketTimeoutMS); - assert.equal(10, db.options.replset.socketOptions.socketTimeoutMS); - assert.equal(12, db.options.server.socketOptions.connectTimeoutMS); - assert.equal(12, db.options.replset.socketOptions.connectTimeoutMS); - assert.equal(10, db.options.replset.retries); - assert.equal(5, db.options.replset.reconnectWait); - assert.equal('replworld', db.options.replset.rs_name); - assert.equal(true, db.options.replset.read_secondary); - assert.equal(false, db.options.db.native_parser); - assert.equal(2, db.options.db.w); - assert.equal(true, db.options.db.safe); - assert.equal(true, db.options.db.fsync); - assert.equal(true, db.options.db.journal); - assert.equal(80, db.options.db.wtimeoutMS); - assert.equal('nearest', db.options.db.read_preference); - assert.deepEqual([{ dc: 'ny', rack: 1 }, { dc: 'sf' }], db.options.db.read_preference_tags); - assert.equal(false, db.options.db.forceServerObjectId); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(typeof db.options.server.socketOptions, 'object'); + assert.equal(typeof db.options.db, 'object'); + assert.equal(typeof db.options.replset, 'object'); + assert.equal(typeof db.options.replset.socketOptions, 'object'); + assert.equal(db.options.mongos, undefined); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(db.options.server.poolSize, 2); + assert.equal(db.options.server.slave_ok, false); + assert.equal(db.options.server.ssl, true); + assert.equal(db.options.replset.ssl, true); + assert.equal(db.options.server.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.replset.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.server.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.retries, 10); + assert.equal(db.options.replset.reconnectWait, 5); + assert.equal(db.options.replset.rs_name, 'replworld'); + assert.equal(db.options.replset.read_secondary, true); + assert.equal(db.options.db.native_parser, false); + assert.equal(db.options.db.w, 2); + assert.equal(db.options.db.safe, true); + assert.equal(db.options.db.fsync, true); + assert.equal(db.options.db.journal, true); + assert.equal(db.options.db.wtimeoutMS, 80); + assert.equal(db.options.db.readPreference, 'nearest'); + assert.deepEqual([{dc: 'ny', rack: 1}, {dc: 'sf'}], db.options.db.read_preference_tags); + assert.equal(db.options.db.forceServerObjectId, false); + assert.strictEqual(db.options.server.sslValidate, true); done(); }); it('mixed with passed options', function(done) { var conn = 'mongodb://localhost/fake?poolSize=2' - + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' - + '&retries=10&reconnectWait=5&rs_name=replworld&readSecondary=true' - + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true' - + '&wtimeoutMS=80&readPreference=nearest&readPreferenceTags=' - + 'dc:ny,rack:1&readPreferenceTags=dc:sf'; - - var db = mongoose.createConnection(conn, { server: { poolSize: 3, auto_reconnect: false }}); - db.on('error', function() {}); + + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' + + '&retries=10&reconnectWait=5&rs_name=replworld&readSecondary=true' + + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true' + + '&wtimeoutMS=80&readPreference=nearest&readPreferenceTags=' + + 'dc:ny,rack:1&readPreferenceTags=dc:sf'; + + var db = mongoose.createConnection(conn, {server: {poolSize: 3, auto_reconnect: false}}); + db.on('error', function() { + }); db.close(); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal('object', typeof db.options.server.socketOptions); - assert.equal('object', typeof db.options.db); - assert.equal('object', typeof db.options.replset); - assert.equal('object', typeof db.options.replset.socketOptions); - assert.equal(false, db.options.server.auto_reconnect); - assert.equal(3, db.options.server.poolSize); - assert.equal(false, db.options.server.slave_ok); - assert.equal(true, db.options.server.ssl); - assert.equal(true, db.options.replset.ssl); - assert.equal(10, db.options.server.socketOptions.socketTimeoutMS); - assert.equal(10, db.options.replset.socketOptions.socketTimeoutMS); - assert.equal(12, db.options.server.socketOptions.connectTimeoutMS); - assert.equal(12, db.options.replset.socketOptions.connectTimeoutMS); - assert.equal(10, db.options.replset.retries); - assert.equal(5, db.options.replset.reconnectWait); - assert.equal('replworld', db.options.replset.rs_name); - assert.equal(true, db.options.replset.read_secondary); - assert.equal(false, db.options.db.native_parser); - assert.equal(2, db.options.db.w); - assert.equal(true, db.options.db.safe); - assert.equal(true, db.options.db.fsync); - assert.equal(true, db.options.db.journal); - assert.equal(80, db.options.db.wtimeoutMS); - assert.equal('nearest', db.options.db.read_preference); - assert.deepEqual([{ dc: 'ny', rack: 1 }, { dc: 'sf' }], db.options.db.read_preference_tags); - assert.equal(false, db.options.db.forceServerObjectId); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(typeof db.options.server.socketOptions, 'object'); + assert.equal(typeof db.options.db, 'object'); + assert.equal(typeof db.options.replset, 'object'); + assert.equal(typeof db.options.replset.socketOptions, 'object'); + assert.equal(db.options.mongos, undefined); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(db.options.server.poolSize, 3); + assert.equal(db.options.server.slave_ok, false); + assert.equal(db.options.server.ssl, true); + assert.equal(db.options.replset.ssl, true); + assert.equal(db.options.server.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.replset.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.server.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.retries, 10); + assert.equal(db.options.replset.reconnectWait, 5); + assert.equal(db.options.replset.rs_name, 'replworld'); + assert.equal(db.options.replset.read_secondary, true); + assert.equal(db.options.db.native_parser, false); + assert.equal(db.options.db.w, 2); + assert.equal(db.options.db.safe, true); + assert.equal(db.options.db.fsync, true); + assert.equal(db.options.db.journal, true); + assert.equal(db.options.db.wtimeoutMS, 80); + assert.equal(db.options.db.readPreference, 'nearest'); + assert.deepEqual([{dc: 'ny', rack: 1}, {dc: 'sf'}], db.options.db.read_preference_tags); + assert.equal(db.options.db.forceServerObjectId, false); done(); }); @@ -438,129 +769,253 @@ describe('connections:', function() { describe('for non replica sets', function() { it('work', function(done) { var conn = 'mongodb://localhost/fake?autoReconnect=false&poolSize=2' - + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' - + '&retries=10&reconnectWait=5&readSecondary=true' - + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true' - + '&wtimeoutMS=80&'; + + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' + + '&retries=10&reconnectWait=5&readSecondary=true' + + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true' + + '&wtimeoutMS=80&'; var db = mongoose.createConnection(conn); - db.on('error', function() {}); + db.on('error', function() { + }); db.close(); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal('object', typeof db.options.server.socketOptions); - assert.equal('object', typeof db.options.db); - assert.equal('object', typeof db.options.replset); - assert.equal('object', typeof db.options.replset.socketOptions); - assert.equal(false, db.options.server.auto_reconnect); - assert.equal(2, db.options.server.poolSize); - assert.equal(false, db.options.server.slave_ok); - assert.equal(true, db.options.server.ssl); - assert.equal(true, db.options.replset.ssl); - assert.equal(10, db.options.server.socketOptions.socketTimeoutMS); - assert.equal(10, db.options.replset.socketOptions.socketTimeoutMS); - assert.equal(12, db.options.server.socketOptions.connectTimeoutMS); - assert.equal(12, db.options.replset.socketOptions.connectTimeoutMS); - assert.equal(10, db.options.replset.retries); - assert.equal(5, db.options.replset.reconnectWait); - assert.equal(true, db.options.replset.read_secondary); - assert.equal(false, db.options.db.native_parser); - assert.equal(2, db.options.db.w); - assert.equal(true, db.options.db.safe); - assert.equal(true, db.options.db.fsync); - assert.equal(true, db.options.db.journal); - assert.equal(80, db.options.db.wtimeoutMS); - assert.equal(false, db.options.db.forceServerObjectId); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(typeof db.options.server.socketOptions, 'object'); + assert.equal(typeof db.options.db, 'object'); + assert.equal(typeof db.options.replset, 'object'); + assert.equal(typeof db.options.replset.socketOptions, 'object'); + assert.equal(db.options.mongos, undefined); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(db.options.server.poolSize, 2); + assert.equal(db.options.server.slave_ok, false); + assert.equal(db.options.server.ssl, true); + assert.equal(db.options.replset.ssl, true); + assert.equal(db.options.server.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.replset.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.server.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.retries, 10); + assert.equal(db.options.replset.reconnectWait, 5); + assert.equal(db.options.replset.read_secondary, true); + assert.equal(db.options.db.native_parser, false); + assert.equal(db.options.db.w, 2); + assert.equal(db.options.db.safe, true); + assert.equal(db.options.db.fsync, true); + assert.equal(db.options.db.journal, true); + assert.equal(db.options.db.wtimeoutMS, 80); + assert.equal(db.options.db.forceServerObjectId, false); done(); }); it('mixed with passed options', function(done) { var conn = 'mongodb://localhost/fake?autoReconnect=false&poolSize=2' - + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' - + '&retries=10&reconnectWait=5&readSecondary=true' - + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true'; + + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' + + '&retries=10&reconnectWait=5&readSecondary=true' + + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true'; - var db = mongoose.createConnection(conn, { db: { w: 3, wtimeoutMS: 80 }}); - db.on('error', function() {}); + var db = mongoose.createConnection(conn, {db: {w: 3, wtimeoutMS: 80}}); + db.on('error', function() { + }); db.close(); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal('object', typeof db.options.server.socketOptions); - assert.equal('object', typeof db.options.db); - assert.equal('object', typeof db.options.replset); - assert.equal('object', typeof db.options.replset.socketOptions); - assert.equal(false, db.options.server.auto_reconnect); - assert.equal(80, db.options.db.wtimeoutMS); - assert.equal(2, db.options.server.poolSize); - assert.equal(false, db.options.server.slave_ok); - assert.equal(true, db.options.server.ssl); - assert.equal(true, db.options.replset.ssl); - assert.equal(10, db.options.server.socketOptions.socketTimeoutMS); - assert.equal(10, db.options.replset.socketOptions.socketTimeoutMS); - assert.equal(12, db.options.server.socketOptions.connectTimeoutMS); - assert.equal(12, db.options.replset.socketOptions.connectTimeoutMS); - assert.equal(10, db.options.replset.retries); - assert.equal(5, db.options.replset.reconnectWait); - assert.equal(true, db.options.replset.read_secondary); - assert.equal(false, db.options.db.native_parser); - assert.equal(3, db.options.db.w); - assert.equal(true, db.options.db.safe); - assert.equal(true, db.options.db.fsync); - assert.equal(true, db.options.db.journal); - assert.equal(false, db.options.db.forceServerObjectId); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(typeof db.options.server.socketOptions, 'object'); + assert.equal(typeof db.options.db, 'object'); + assert.equal(typeof db.options.replset, 'object'); + assert.equal(typeof db.options.replset.socketOptions, 'object'); + assert.equal(db.options.mongos, undefined); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(db.options.db.wtimeoutMS, 80); + assert.equal(db.options.server.poolSize, 2); + assert.equal(db.options.server.slave_ok, false); + assert.equal(db.options.server.ssl, true); + assert.equal(db.options.replset.ssl, true); + assert.equal(db.options.server.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.replset.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.server.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.retries, 10); + assert.equal(db.options.replset.reconnectWait, 5); + assert.equal(db.options.replset.read_secondary, true); + assert.equal(db.options.db.native_parser, false); + assert.equal(db.options.db.w, 3); + assert.equal(db.options.db.safe, true); + assert.equal(db.options.db.fsync, true); + assert.equal(db.options.db.journal, true); + assert.equal(db.options.db.forceServerObjectId, false); done(); }); }); - }); - - describe('missing protocols', function() { - it('are allowed with replsets', function(done) { - var conn = mongoose.createConnection('localhost:12345,127.0.0.1:14326?replicaSet=bacon', function(err) { - // force missing db error so we don't actually connect. - assert.ok(err); + describe('for sharded clusters (mongos)', function() { + it('works when specifying {mongos: true} as an option', function(done) { + var conn = 'mongodb://localhost/fake?autoReconnect=false&poolSize=2' + + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' + + '&retries=10&reconnectWait=5&rs_name=replworld&readSecondary=true' + + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true' + + '&wtimeoutMS=80&readPreference=nearest&readPreferenceTags=' + + 'dc:ny,rack:1&readPreferenceTags=dc:sf&sslValidate=true'; + + var db = new mongoose.Connection(); + db.options = db.parseOptions({mongos: true}, muri(conn).options); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(typeof db.options.server.socketOptions, 'object'); + assert.equal(typeof db.options.db, 'object'); + assert.equal(typeof db.options.replset, 'object'); + assert.equal(typeof db.options.replset.socketOptions, 'object'); + assert.equal(typeof db.options.mongos, 'object'); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(db.options.server.poolSize, 2); + assert.equal(db.options.server.slave_ok, false); + assert.equal(db.options.server.ssl, true); + assert.equal(db.options.replset.ssl, true); + assert.equal(db.options.mongos.ssl, true); + assert.equal(db.options.server.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.replset.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.server.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.retries, 10); + assert.equal(db.options.replset.reconnectWait, 5); + assert.equal(db.options.replset.rs_name, 'replworld'); + assert.equal(db.options.replset.read_secondary, true); + assert.equal(db.options.db.native_parser, false); + assert.equal(db.options.db.w, 2); + assert.equal(db.options.db.safe, true); + assert.equal(db.options.db.fsync, true); + assert.equal(db.options.db.journal, true); + assert.equal(db.options.db.wtimeoutMS, 80); + assert.equal(db.options.db.readPreference, 'nearest'); + assert.deepEqual([{dc: 'ny', rack: 1}, {dc: 'sf'}], db.options.db.read_preference_tags); + assert.equal(db.options.db.forceServerObjectId, false); + assert.strictEqual(db.options.server.sslValidate, true); + assert.strictEqual(db.options.mongos.sslValidate, true); + done(); + }); + it('works when specifying mongos as a query param on the connection string', function(done) { + var newQueryParam = '&mongos=true'; + var conn = 'mongodb://localhost/fake?autoReconnect=false&poolSize=2' + + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' + + '&retries=10&reconnectWait=5&rs_name=replworld&readSecondary=true' + + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true' + + '&wtimeoutMS=80&readPreference=nearest&readPreferenceTags=' + + 'dc:ny,rack:1&readPreferenceTags=dc:sf&sslValidate=true' + + newQueryParam; + + var db = new mongoose.Connection(); + db.options = db.parseOptions({}, muri(conn).options); + assert.strictEqual(typeof db.options, 'object'); + assert.strictEqual(typeof db.options.server, 'object'); + assert.strictEqual(typeof db.options.server.socketOptions, 'object'); + assert.strictEqual(typeof db.options.db, 'object'); + assert.strictEqual(typeof db.options.replset, 'object'); + assert.strictEqual(typeof db.options.replset.socketOptions, 'object'); + assert.strictEqual(typeof db.options.mongos, 'object'); + assert.strictEqual(db.options.server.auto_reconnect, false); + assert.strictEqual(db.options.server.poolSize, 2); + assert.strictEqual(db.options.server.slave_ok, false); + assert.strictEqual(db.options.server.ssl, true); + assert.strictEqual(db.options.replset.ssl, true); + assert.strictEqual(db.options.mongos.ssl, true); + assert.strictEqual(db.options.server.socketOptions.socketTimeoutMS, 10); + assert.strictEqual(db.options.replset.socketOptions.socketTimeoutMS, 10); + assert.strictEqual(db.options.server.socketOptions.connectTimeoutMS, 12); + assert.strictEqual(db.options.replset.socketOptions.connectTimeoutMS, 12); + assert.strictEqual(db.options.replset.retries, 10); + assert.strictEqual(db.options.replset.reconnectWait, 5); + assert.strictEqual(db.options.replset.rs_name, 'replworld'); + assert.strictEqual(db.options.replset.read_secondary, true); + assert.strictEqual(db.options.db.native_parser, false); + assert.strictEqual(db.options.db.w, 2); + assert.strictEqual(db.options.db.safe, true); + assert.strictEqual(db.options.db.fsync, true); + assert.strictEqual(db.options.db.journal, true); + assert.strictEqual(db.options.db.wtimeoutMS, 80); + assert.strictEqual(db.options.db.readPreference, 'nearest'); + assert.deepEqual(db.options.db.read_preference_tags, [{dc: 'ny', rack: 1}, {dc: 'sf'}]); + assert.strictEqual(db.options.db.forceServerObjectId, false); + assert.strictEqual(db.options.server.sslValidate, true); + assert.strictEqual(db.options.mongos.sslValidate, true); + done(); + }); + it('works when specifying mongos as an object with options', function(done) { + var conn = 'mongodb://localhost/fake?autoReconnect=false&poolSize=2' + + '&slaveOk=false&ssl=true&socketTimeoutMS=10&connectTimeoutMS=12' + + '&retries=10&reconnectWait=5&rs_name=replworld&readSecondary=true' + + '&nativeParser=false&w=2&safe=true&fsync=true&journal=true' + + '&wtimeoutMS=80&readPreference=nearest&readPreferenceTags=' + + 'dc:ny,rack:1&readPreferenceTags=dc:sf&sslValidate=true'; + + var db = new mongoose.Connection(); + db.options = db.parseOptions({mongos: {w: 3,wtimeoutMS: 80}}, muri(conn).options); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(typeof db.options.server.socketOptions, 'object'); + assert.equal(typeof db.options.db, 'object'); + assert.equal(typeof db.options.replset, 'object'); + assert.equal(typeof db.options.replset.socketOptions, 'object'); + assert.equal(typeof db.options.mongos, 'object'); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(db.options.server.poolSize, 2); + assert.equal(db.options.server.slave_ok, false); + assert.equal(db.options.server.ssl, true); + assert.equal(db.options.replset.ssl, true); + assert.equal(db.options.mongos.ssl, true); + assert.equal(db.options.server.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.replset.socketOptions.socketTimeoutMS, 10); + assert.equal(db.options.server.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.socketOptions.connectTimeoutMS, 12); + assert.equal(db.options.replset.retries, 10); + assert.equal(db.options.replset.reconnectWait, 5); + assert.equal(db.options.replset.rs_name, 'replworld'); + assert.equal(db.options.replset.read_secondary, true); + assert.equal(db.options.db.native_parser, false); + assert.equal(db.options.db.w, 2); + assert.equal(db.options.db.safe, true); + assert.equal(db.options.db.fsync, true); + assert.equal(db.options.db.journal, true); + assert.equal(db.options.db.readPreference, 'nearest'); + assert.deepEqual([{dc: 'ny', rack: 1}, {dc: 'sf'}], db.options.db.read_preference_tags); + assert.equal(db.options.db.forceServerObjectId, false); + assert.strictEqual(db.options.server.sslValidate, true); + assert.strictEqual(db.options.mongos.sslValidate, true); + assert.equal(3, db.options.mongos.w); + assert.equal(80, db.options.mongos.wtimeoutMS); + done(); }); - assert.deepEqual([{host:'localhost', port:12345},{host:'127.0.0.1',port:14326}], conn.hosts); - assert.deepEqual(null, conn.host); - assert.deepEqual(null, conn.port); - setTimeout(done, 10); - }); - - it('are allowed with single connections', function(done) { - var conn = mongoose.createConnection(); - conn.doOpen = function() {}; - conn.open('localhost:12345/woot'); - assert.deepEqual('localhost', conn.host); - assert.deepEqual(12345, conn.port); - done(); }); }); describe('connect callbacks', function() { it('execute with user:pwd connection strings', function(done) { - var db = mongoose.createConnection('mongodb://aaron:psw@localhost:27000/fake', { server: { auto_reconnect: true }}, function() { + var db = mongoose.createConnection('mongodb://aaron:psw@localhost:27000/fake', {server: {auto_reconnect: true}}, function() { done(); }); - db.on('error', function(err) { assert.ok(err); }); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); + db.on('error', function(err) { + assert.ok(err); + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); db.close(); }); it('execute without user:pwd connection strings', function(done) { - var db = mongoose.createConnection('mongodb://localhost/fake', function() {}); - db.on('error', function(err) { assert.ok(err); }); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal(undefined, db.user); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27017, db.port); + var db = mongoose.createConnection('mongodb://localhost/fake', function() { + }); + db.on('error', function(err) { + assert.ok(err); + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.user, undefined); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27017); db.close(); - setTimeout(done,10); + setTimeout(done, 10); }); it('should return an error if malformed uri passed', function(done) { var db = mongoose.createConnection('mongodb:///fake', function(err) { @@ -574,62 +1029,62 @@ describe('connections:', function() { var db = mongoose.createConnection('mongodb://u:p@localhost', function() { done(); }); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('admin', db.name); - assert.equal('localhost', db.host); - assert.equal(27017, db.port); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'admin'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27017); db.close(); }); it('should fire when individual args are passed', function(done) { - var db = mongoose.createConnection('127.0.0.1', 'faker', 28000, { server: { auto_reconnect: false }}, function() { + var db = mongoose.createConnection('127.0.0.1', 'faker', 28000, {server: {auto_reconnect: false}}, function() { done(); }); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(false, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('faker', db.name); - assert.equal('127.0.0.1', db.host); - assert.equal(28000, db.port); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'faker'); + assert.equal(db.host, '127.0.0.1'); + assert.equal(db.port, 28000); db.close(); }); it('should fire when no options are passed', function(done) { var db = mongoose.createConnection('127.0.0.1', 'faker', 28000, function() { done(); }); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('faker', db.name); - assert.equal('127.0.0.1', db.host); - assert.equal(28000, db.port); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'faker'); + assert.equal(db.host, '127.0.0.1'); + assert.equal(db.port, 28000); db.close(); }); it('should fire when default port utilized', function(done) { var db = mongoose.createConnection('127.0.0.1', 'faker', done); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('faker', db.name); - assert.equal('127.0.0.1', db.host); - assert.equal(27017, db.port); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'faker'); + assert.equal(db.host, '127.0.0.1'); + assert.equal(db.port, 27017); db.close(); }); }); describe('errors', function() { it('event fires with one listener', function(done) { - this.timeout(1000); - var db = start({ uri: 'mongodb://whatever23939.localhost/fakeeee?connectTimeoutMS=500', noErrorListener: 1 }); + this.timeout(1500); + var db = start({uri: 'mongodb://whatever23939.localhost/fakeeee?connectTimeoutMS=500', noErrorListener: 1}); db.on('error', function() { // this callback has no params which triggered the bug #759 db.close(); @@ -658,16 +1113,16 @@ describe('connections:', function() { assert.ok(MyModel.schema instanceof Schema); assert.ok(MyModel.prototype.schema instanceof Schema); - var m = new MyModel({name:'aaron'}); - assert.equal('aaron', m.name); + var m = new MyModel({name: 'aaron'}); + assert.equal(m.name, 'aaron'); done(); }); it('should properly assign the db', function(done) { - var A = mongoose.model('testing853a', new Schema({x:String}), 'testing853-1'); - var B = mongoose.model('testing853b', new Schema({x:String}), 'testing853-2'); + var A = mongoose.model('testing853a', new Schema({x: String}), 'testing853-1'); + var B = mongoose.model('testing853b', new Schema({x: String}), 'testing853-2'); var C = B.model('testing853a'); - assert.ok(C == A); + assert.ok(C === A); done(); }); @@ -709,15 +1164,15 @@ describe('connections:', function() { }); it('uses the passed schema when global model exists with same name (gh-1209)', function(done) { - var s1 = new Schema({ one: String }); - var s2 = new Schema({ two: Number }); + var s1 = new Schema({one: String}); + var s2 = new Schema({two: Number}); var db = start(); var A = mongoose.model('gh-1209-a', s1); var B = db.model('gh-1209-a', s2); - assert.ok(A.schema != B.schema); + assert.ok(A.schema !== B.schema); assert.ok(A.schema.paths.one); assert.ok(B.schema.paths.two); assert.ok(!B.schema.paths.one); @@ -726,7 +1181,7 @@ describe('connections:', function() { // reset delete db.models['gh-1209-a']; var C = db.model('gh-1209-a'); - assert.ok(C.schema == A.schema); + assert.ok(C.schema === A.schema); db.close(); done(); @@ -734,11 +1189,11 @@ describe('connections:', function() { describe('get existing model with not existing collection in db', function() { it('must return exiting collection with all collection options', function(done) { - mongoose.model('some-th-1458', new Schema({test:String},{capped:{size:1000, max:10}})); + mongoose.model('some-th-1458', new Schema({test: String}, {capped: {size: 1000, max: 10}})); var db = start(); var m = db.model('some-th-1458'); - assert.equal(m.collection.opts.capped.size, 1000); - assert.equal(m.collection.opts.capped.max, 10); + assert.equal(1000, m.collection.opts.capped.size); + assert.equal(10, m.collection.opts.capped.max); db.close(); done(); }); @@ -748,15 +1203,15 @@ describe('connections:', function() { describe('when model name already exists', function() { it('returns a new uncached model', function(done) { var db = start(); - var s1 = new Schema({ a: [] }); + var s1 = new Schema({a: []}); var name = 'non-cached-collection-name'; var A = db.model(name, s1); var B = db.model(name); var C = db.model(name, 'alternate'); - assert.ok(A.collection.name == B.collection.name); - assert.ok(A.collection.name != C.collection.name); - assert.ok(db.models[name].collection.name != C.collection.name); - assert.ok(db.models[name].collection.name == A.collection.name); + assert.ok(A.collection.name === B.collection.name); + assert.ok(A.collection.name !== C.collection.name); + assert.ok(db.models[name].collection.name !== C.collection.name); + assert.ok(db.models[name].collection.name === A.collection.name); db.close(); done(); }); @@ -766,8 +1221,8 @@ describe('connections:', function() { describe('passing object literal schemas', function() { it('works', function(done) { var db = start(); - var A = db.model('A', { n: [{ age: 'number' }]}); - var a = new A({ n: [{ age: '47' }] }); + var A = db.model('A', {n: [{age: 'number'}]}); + var a = new A({n: [{age: '47'}]}); assert.strictEqual(47, a.n[0].age); a.save(function(err) { assert.ifError(err); @@ -786,7 +1241,9 @@ describe('connections:', function() { it('accepts uris, dbname, options', function(done) { var m = new mongoose.Mongoose; var uris = process.env.MONGOOSE_SET_TEST_URI; - if (!uris) return done(); + if (!uris) { + return done(); + } m.connection.on('error', done); m.connection.on('open', function() { @@ -794,7 +1251,7 @@ describe('connections:', function() { }); try { - m.connect(uris, 'mongoose_test', { server: { auto_reconnect: true }}); + m.connect(uris, 'mongoose_test', {server: {auto_reconnect: true}}); } catch (err) { done(err); } @@ -802,25 +1259,31 @@ describe('connections:', function() { describe('auth', function() { it('from uri', function(done) { var uris = process.env.MONGOOSE_SET_TEST_URI; - if (!uris) return done(); + if (!uris) { + return done(); + } var db = mongoose.createConnection(); - db.openSet('mongodb://aaron:psw@localhost:27000,b,c', { server: { auto_reconnect: false }}); - db.on('error', function() {}); - assert.equal('aaron', db.user); - assert.equal('psw', db.pass); + db.openSet('mongodb://aaron:psw@localhost:27000,b,c', {server: {auto_reconnect: false}}); + db.on('error', function() { + }); + assert.equal(db.user, 'aaron'); + assert.equal(db.pass, 'psw'); db.close(); done(); }); it('form options', function(done) { var uris = process.env.MONGOOSE_SET_TEST_URI; - if (!uris) return done(); + if (!uris) { + return done(); + } var db = mongoose.createConnection(); - db.openSet('mongodb://aaron:psw@localhost:27000,b,c', { user: 'tester', pass: 'testpsw' }); - db.on('error', function() {}); - assert.equal('tester', db.user); - assert.equal('testpsw', db.pass); + db.openSet('mongodb://aaron:psw@localhost:27000,b,c', {user: 'tester', pass: 'testpsw'}); + db.on('error', function() { + }); + assert.equal(db.user, 'tester'); + assert.equal(db.pass, 'testpsw'); db.close(); done(); }); @@ -828,26 +1291,29 @@ describe('connections:', function() { it('handles unix domain sockets', function(done) { var url = 'mongodb://aaron:psw@/tmp/mongodb-27018.sock,/tmp/mongodb-27019.sock/fake?replicaSet=bacon'; - var db = mongoose.createConnection(url, { server: { auto_reconnect: false }}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(false, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); + var db = mongoose.createConnection(url, {server: {auto_reconnect: false}}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, false); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); assert.ok(Array.isArray(db.hosts)); - assert.equal('/tmp/mongodb-27018.sock', db.hosts[0].ipc); - assert.equal('/tmp/mongodb-27019.sock', db.hosts[1].ipc); - assert.equal('psw', db.pass); - assert.equal('aaron', db.user); + assert.equal(db.hosts[0].ipc, '/tmp/mongodb-27018.sock'); + assert.equal(db.hosts[1].ipc, '/tmp/mongodb-27019.sock'); + assert.equal(db.pass, 'psw'); + assert.equal(db.user, 'aaron'); db.close(); done(); }); it('can reopen a disconnected replica set (gh-1263)', function(done) { var uris = process.env.MONGOOSE_SET_TEST_URI; - if (!uris) return done(); + if (!uris) { + return done(); + } var conn = mongoose.createConnection(); @@ -855,10 +1321,14 @@ describe('connections:', function() { try { conn.openSet(uris, 'mongoose_test', {}, function(err) { - if (err) return done(err); + if (err) { + return done(err); + } conn.close(function(err) { - if (err) return done(err); + if (err) { + return done(err); + } conn.openSet(uris, 'mongoose_test', {}, function() { conn.close(done); @@ -871,28 +1341,107 @@ describe('connections:', function() { }); }); + it('connecting to single mongos (gh-3537)', function(done) { + var db = mongoose.createConnection('localhost:27017', {mongos: true}); + assert.ok(db.db.serverConfig instanceof mongoose.mongo.Mongos); + db.on('error', function() { + done(); + }); + }); + + it('force close (gh-5664)', function(done) { + var opts = { useMongoClient: true }; + var db = mongoose.createConnection('mongodb://localhost:27017/test', opts); + var coll = db.collection('Test'); + db.then(function() { + setTimeout(function() { + coll.insertOne({x:1}, function(error) { + assert.ok(error); + assert.ok(error.message.indexOf('pool was destroyed') !== -1, error.message); + done(); + }); + }, 100); + + // Force close + db.close(true); + }); + }); + + it('force close with connection created after close (gh-5664)', function(done) { + var opts = { useMongoClient: true }; + var db = mongoose.createConnection('mongodb://localhost:27017/test', opts); + db.then(function() { + setTimeout(function() { + // TODO: enforce error.message, right now get a confusing error + /*db.collection('Test').insertOne({x:1}, function(error) { + assert.ok(error); + + //assert.ok(error.message.indexOf('pool was destroyed') !== -1, error.message); + done(); + });*/ + + var threw = false; + try { + db.collection('Test').insertOne({x:1}, function() {}); + } catch (error) { + threw = true; + assert.ok(error); + } + + assert.ok(threw); + done(); + }, 100); + + // Force close + db.close(true); + }); + }); + + it('bufferCommands (gh-5720)', function(done) { + var opts = { useMongoClient: true, bufferCommands: false }; + var db = mongoose.createConnection('mongodb://localhost:27017/test', opts); + + var M = db.model('gh5720', new Schema({})); + assert.ok(!M.collection.buffer); + db.close(); + + opts = { useMongoClient: true, bufferCommands: true }; + db = mongoose.createConnection('mongodb://localhost:27017/test', opts); + M = db.model('gh5720', new Schema({}, { bufferCommands: false })); + assert.ok(!M.collection.buffer); + db.close(); + + opts = { useMongoClient: true, bufferCommands: true }; + db = mongoose.createConnection('mongodb://localhost:27017/test', opts); + M = db.model('gh5720', new Schema({})); + assert.ok(M.collection.buffer); + db.close(done); + }); + describe('connecting to multiple mongos nodes (gh-1037)', function() { var mongos = process.env.MONGOOSE_MULTI_MONGOS_TEST_URI; - if (!mongos) return console.log('Not testing multi-mongos support'); + if (!mongos) { + return console.log('Not testing multi-mongos support'); + } it('works', function(done) { this.timeout(3000); var m = new mongoose.Mongoose; - m.connect(mongos, { mongos: true }, function(err) { + m.connect(mongos, {mongos: true}, function(err) { assert.ifError(err); var s = m.connection.db.serverConfig; assert.ok(s instanceof mongoose.mongo.Mongos); - assert.equal(2, s.servers.length); + assert.equal(s.servers.length, 2); - var M = m.model('TestMultipleMongos', { name: String }, 'test-multi-mongos-' + random()); - M.create({ name: 'works' }, function(err, d) { + var M = m.model('TestMultipleMongos', {name: String}, 'test-multi-mongos-' + random()); + M.create({name: 'works'}, function(err, d) { assert.ifError(err); - M.findOne({ name: 'works' }, function(err, doc) { + M.findOne({name: 'works'}, function(err, doc) { assert.ifError(err); - assert.equal(doc.id, d.id); + assert.equal(d.id, doc.id); m.disconnect(done); }); }); @@ -903,24 +1452,24 @@ describe('connections:', function() { describe('modelNames()', function() { it('returns names of all models registered on it', function(done) { var m = new mongoose.Mongoose; - m.model('root', { x: String }); - var another = m.model('another', { x: String }); - another.discriminator('discriminated', new Schema({ x: String })); + m.model('root', {x: String}); + var another = m.model('another', {x: String}); + another.discriminator('discriminated', new Schema({x: String})); var db = m.createConnection(); - db.model('something', { x: String }); + db.model('something', {x: String}); var names = db.modelNames(); assert.ok(Array.isArray(names)); - assert.equal(1, names.length); - assert.equal('something', names[0]); + assert.equal(names.length, 1); + assert.equal(names[0], 'something'); names = m.modelNames(); assert.ok(Array.isArray(names)); - assert.equal(3, names.length); - assert.equal('root', names[0]); - assert.equal('another', names[1]); - assert.equal('discriminated', names[2]); + assert.equal(names.length, 3); + assert.equal(names[0], 'root'); + assert.equal(names[1], 'another'); + assert.equal(names[2], 'discriminated'); db.close(done); }); @@ -932,16 +1481,16 @@ describe('connections:', function() { var db2 = db.useDb('mongoose2'); - assert.equal(db2.name, 'mongoose2'); - assert.equal(db.name, 'mongoose1'); + assert.equal('mongoose2', db2.name); + assert.equal('mongoose1', db.name); - assert.equal(db.port, db2.port); - assert.equal(db.replica, db2.replica); - assert.equal(db.hosts, db2.hosts); - assert.equal(db.host, db2.host); - assert.equal(db.port, db2.port); - assert.equal(db.user, db2.user); - assert.equal(db.pass, db2.pass); + assert.equal(db2.port, db.port); + assert.equal(db2.replica, db.replica); + assert.equal(db2.hosts, db.hosts); + assert.equal(db2.host, db.host); + assert.equal(db2.port, db.port); + assert.equal(db2.user, db.user); + assert.equal(db2.pass, db.pass); assert.deepEqual(db.options, db2.options); db2.close(done); @@ -952,27 +1501,27 @@ describe('connections:', function() { var db2 = db.useDb('mongoose-test-2'); var schema = new Schema({ - body : String, - thing : Number + body: String, + thing: Number }); var m1 = db.model('testMod', schema); var m2 = db2.model('testMod', schema); - m1.create({ body : 'this is some text', thing : 1 }, function(err, i1) { + m1.create({body: 'this is some text', thing: 1}, function(err, i1) { assert.ifError(err); - m2.create({ body : 'this is another body', thing : 2 }, function(err, i2) { + m2.create({body: 'this is another body', thing: 2}, function(err, i2) { assert.ifError(err); m1.findById(i1.id, function(err, item1) { assert.ifError(err); - assert.equal(item1.body, 'this is some text'); - assert.equal(item1.thing, 1); + assert.equal('this is some text', item1.body); + assert.equal(1, item1.thing); m2.findById(i2.id, function(err, item2) { assert.ifError(err); - assert.equal(item2.body, 'this is another body'); - assert.equal(item2.thing, 2); + assert.equal('this is another body', item2.body); + assert.equal(2, item2.thing); // validate the doc doesn't exist in the other db m1.findById(i2.id, function(err, nothing) { @@ -1033,7 +1582,6 @@ describe('connections:', function() { function close() { db.close(done); } - }); it('emits open events on both', function(done) { @@ -1136,7 +1684,6 @@ describe('connections:', function() { done(); }); db.close(); - }); it('closes correctly for all dbs, closing secondary db', function(done) { @@ -1147,7 +1694,6 @@ describe('connections:', function() { done(); }); db2.close(); - }); }); @@ -1156,19 +1702,20 @@ describe('connections:', function() { describe('when username and password are undefined', function() { it('should return false', function(done) { var db = mongoose.createConnection('localhost', 'fake', 27000, {}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal(undefined, db.pass); - assert.equal(undefined, db.user); - - assert.equal(false, db.shouldAuthenticate()); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, undefined); + assert.equal(db.user, undefined); + + assert.equal(db.shouldAuthenticate(), false); db.close(); done(); @@ -1176,20 +1723,21 @@ describe('connections:', function() { }); describe('when username and password are empty strings', function() { it('should return false', function(done) { - var db = mongoose.createConnection('localhost', 'fake', 27000, { user: '', pass: ''}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal(undefined, db.pass); - assert.equal(undefined, db.user); - - assert.equal(false, db.shouldAuthenticate()); + var db = mongoose.createConnection('localhost', 'fake', 27000, {user: '', pass: ''}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, undefined); + assert.equal(db.user, undefined); + + assert.equal(db.shouldAuthenticate(), false); db.close(); done(); @@ -1197,20 +1745,21 @@ describe('connections:', function() { }); describe('when only username is defined', function() { it('should return false', function(done) { - var db = mongoose.createConnection('localhost', 'fake', 27000, { user: 'user' }); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal(undefined, db.pass); - assert.equal(undefined, db.user); - - assert.equal(false, db.shouldAuthenticate()); + var db = mongoose.createConnection('localhost', 'fake', 27000, {user: 'user'}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, undefined); + assert.equal(db.user, undefined); + + assert.equal(db.shouldAuthenticate(), false); db.close(); done(); @@ -1218,20 +1767,21 @@ describe('connections:', function() { }); describe('when both username and password are defined', function() { it('should return false', function(done) { - var db = mongoose.createConnection('localhost', 'fake', 27000, { user: 'user', pass: 'pass' }); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal('pass', db.pass); - assert.equal('user', db.user); - - assert.equal(true, db.shouldAuthenticate()); + var db = mongoose.createConnection('localhost', 'fake', 27000, {user: 'user', pass: 'pass'}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, 'pass'); + assert.equal(db.user, 'user'); + + assert.equal(db.shouldAuthenticate(), true); db.close(); done(); @@ -1242,19 +1792,20 @@ describe('connections:', function() { describe('when username and password are undefined', function() { it('should return false', function(done) { var db = mongoose.createConnection('localhost', 'fake', 27000, {}); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal(undefined, db.pass); - assert.equal(undefined, db.user); - - assert.equal(false, db.shouldAuthenticate()); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, undefined); + assert.equal(db.user, undefined); + + assert.equal(db.shouldAuthenticate(), false); db.close(); done(); @@ -1262,20 +1813,21 @@ describe('connections:', function() { }); describe('when only username is defined', function() { it('should return false', function(done) { - var db = mongoose.createConnection('localhost', 'fake', 27000, { user: 'user', auth: { authMechanism: 'MONGODB-X509' } }); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal(undefined, db.pass); - assert.equal('user', db.user); - - assert.equal(true, db.shouldAuthenticate()); + var db = mongoose.createConnection('localhost', 'fake', 27000, {user: 'user', auth: {authMechanism: 'MONGODB-X509'}}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, undefined); + assert.equal(db.user, 'user'); + + assert.equal(db.shouldAuthenticate(), true); db.close(); done(); @@ -1283,20 +1835,21 @@ describe('connections:', function() { }); describe('when both username and password are defined', function() { it('should return false', function(done) { - var db = mongoose.createConnection('localhost', 'fake', 27000, { user: 'user', pass: 'pass', auth: { authMechanism: 'MONGODB-X509' } }); - db.on('error', function() {}); - assert.equal('object', typeof db.options); - assert.equal('object', typeof db.options.server); - assert.equal(true, db.options.server.auto_reconnect); - assert.equal('object', typeof db.options.db); - assert.equal(false, db.options.db.forceServerObjectId); - assert.equal('fake', db.name); - assert.equal('localhost', db.host); - assert.equal(27000, db.port); - assert.equal('pass', db.pass); - assert.equal('user', db.user); - - assert.equal(true, db.shouldAuthenticate()); + var db = mongoose.createConnection('localhost', 'fake', 27000, {user: 'user', pass: 'pass', auth: {authMechanism: 'MONGODB-X509'}}); + db.on('error', function() { + }); + assert.equal(typeof db.options, 'object'); + assert.equal(typeof db.options.server, 'object'); + assert.equal(db.options.server.auto_reconnect, true); + assert.equal(typeof db.options.db, 'object'); + assert.equal(db.options.db.forceServerObjectId, false); + assert.equal(db.name, 'fake'); + assert.equal(db.host, 'localhost'); + assert.equal(db.port, 27000); + assert.equal(db.pass, 'pass'); + assert.equal(db.user, 'user'); + + assert.equal(db.shouldAuthenticate(), true); db.close(); done(); diff --git a/test/crash.test.js b/test/crash.test.js index 6a5a758c8c3..a5c0fe7d90c 100644 --- a/test/crash.test.js +++ b/test/crash.test.js @@ -1,16 +1,15 @@ - // GH-407 -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose; describe('crash: (gh-407)', function() { it('test mongodb crash with invalid objectid string', function(done) { - var db = mongoose.createConnection("mongodb://localhost/test-crash"); + var db = mongoose.createConnection('mongodb://localhost/test-crash'); var IndexedGuy = new mongoose.Schema({ - name: { type: String } + name: {type: String} }); var Guy = db.model('Guy', IndexedGuy); @@ -26,12 +25,12 @@ describe('crash: (gh-407)', function() { db.close(done); try { - assert.equal('Cast to ObjectId failed for value "" at path "_id"', err.message); + assert.equal(err.message, + 'Cast to ObjectId failed for value "" at path "_id" for model "Guy"'); } catch (er) { console.error(err); throw er; } }); - }); }); diff --git a/test/docs/defaults.test.js b/test/docs/defaults.test.js new file mode 100644 index 00000000000..ded1e0a43be --- /dev/null +++ b/test/docs/defaults.test.js @@ -0,0 +1,115 @@ +var assert = require('power-assert'); +var mongoose = require('../../'); + +describe('defaults docs', function () { + var db; + var Schema = mongoose.Schema; + + before(function () { + db = mongoose.createConnection('mongodb://localhost:27017/mongoose_test'); + }); + + after(function (done) { + db.close(done); + }); + + /** + * Your schemas can define default values for certain paths. If you create + * a new document without that path set, the default will kick in. + */ + it('Declaring defaults in your schema', function(done) { + var schema = new Schema({ + name: String, + role: { type: String, default: 'guitarist' } + }); + + var Person = db.model('Person', schema); + + var axl = new Person({ name: 'Axl Rose', role: 'singer' }); + assert.equal(axl.role, 'singer'); + + var slash = new Person({ name: 'Slash' }); + assert.equal(slash.role, 'guitarist'); + + var izzy = new Person({ name: 'Izzy', role: undefined }); + assert.equal(izzy.role, 'guitarist'); + + Person.create(axl, slash, function(error) { + assert.ifError(error); + Person.find({ role: 'guitarist' }, function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 1); + assert.equal(docs[0].name, 'Slash'); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); + }); + + /** + * You can also set the `default` schema option to a function. Mongoose will + * execute that function and use the return value as the default. + */ + it('Default functions', function() { + var schema = new Schema({ + title: String, + date: { + type: Date, + // `Date.now()` returns the current unix timestamp as a number + default: Date.now + } + }); + + var BlogPost = db.model('BlogPost', schema); + + var post = new BlogPost({title: '5 Best Arnold Schwarzenegger Movies'}); + + // The post has a default Date set to now + assert.ok(post.date.getTime() >= Date.now() - 1000); + assert.ok(post.date.getTime() <= Date.now()); + }); + + /** + * By default, mongoose only applies defaults when you create a new document. + * It will **not** set defaults if you use `update()` and + * `findOneAndUpdate()`. However, mongoose 4.x lets you opt-in to this + * behavior using the `setDefaultsOnInsert` option. + * + * ## Important + * + * The `setDefaultsOnInsert` option relies on the + * [MongoDB `$setOnInsert` operator](https://docs.mongodb.org/manual/reference/operator/update/setOnInsert/). + * The `$setOnInsert` operator was introduced in MongoDB 2.4. If you're + * using MongoDB server < 2.4.0, do **not** use `setDefaultsOnInsert`. + */ + it('The `setDefaultsOnInsert` option', function(done) { + var schema = new Schema({ + title: String, + genre: {type: String, default: 'Action'} + }); + + var Movie = db.model('Movie', schema); + + var query = {}; + var update = {title: 'The Terminator'}; + var options = { + // Return the document after updates are applied + new: true, + // Create a document if one isn't found. Required + // for `setDefaultsOnInsert` + upsert: true, + setDefaultsOnInsert: true + }; + + Movie. + findOneAndUpdate(query, update, options, function (error, doc) { + assert.ifError(error); + assert.equal(doc.title, 'The Terminator'); + assert.equal(doc.genre, 'Action'); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); +}); diff --git a/test/docs/discriminators.test.js b/test/docs/discriminators.test.js index 942da175be6..fb4bac84f6a 100644 --- a/test/docs/discriminators.test.js +++ b/test/docs/discriminators.test.js @@ -1,29 +1,37 @@ -var assert = require('assert'); +'use strict'; + +var assert = require('power-assert'); var async = require('async'); var mongoose = require('../../'); -describe('discriminator docs', function() { +var Schema = mongoose.Schema; + +describe('discriminator docs', function () { var Event; var ClickedLinkEvent; var SignedUpEvent; - var db = mongoose.createConnection('mongodb://localhost:27017/mongoose_test'); + var db; - before(function(done) { - var options = { discriminatorKey: 'kind' }; + before(function (done) { + db = mongoose.createConnection('mongodb://localhost:27017/mongoose_test'); - var eventSchema = new mongoose.Schema({ time: Date }, options); + var eventSchema = new mongoose.Schema({time: Date}); Event = db.model('_event', eventSchema); ClickedLinkEvent = Event.discriminator('ClickedLink', - new mongoose.Schema({ url: String }, options)); + new mongoose.Schema({url: String})); SignedUpEvent = Event.discriminator('SignedUp', - new mongoose.Schema({ username: String }, options)); + new mongoose.Schema({username: String})); done(); }); - beforeEach(function(done) { + after(function (done) { + db.close(done); + }); + + beforeEach(function (done) { Event.remove({}, done); }); @@ -40,24 +48,24 @@ describe('discriminator docs', function() { * model whose schema is the union of the base schema and the * discriminator schema. */ - it('The `model.discriminator()` function', function(done) { - var options = { discriminatorKey: 'kind' }; + it('The `model.discriminator()` function', function (done) { + var options = {discriminatorKey: 'kind'}; - var eventSchema = new mongoose.Schema({ time: Date }, options); + var eventSchema = new mongoose.Schema({time: Date}, options); var Event = mongoose.model('Event', eventSchema); // ClickedLinkEvent is a special type of Event that has // a URL. var ClickedLinkEvent = Event.discriminator('ClickedLink', - new mongoose.Schema({ url: String }, options)); + new mongoose.Schema({url: String}, options)); // When you create a generic event, it can't have a URL field... - var genericEvent = new Event({ time: Date.now(), url: 'google.com' }); + var genericEvent = new Event({time: Date.now(), url: 'google.com'}); assert.ok(!genericEvent.url); // But a ClickedLinkEvent can var clickedEvent = - new ClickedLinkEvent({ time: Date.now(), url: 'google.com' }); + new ClickedLinkEvent({time: Date.now(), url: 'google.com'}); assert.ok(clickedEvent.url); // acquit:ignore:start @@ -71,23 +79,23 @@ describe('discriminator docs', function() { * stored in the same collection as generic events and `ClickedLinkEvent` * instances. */ - it('Discriminators save to the Event model\'s collection', function(done) { - var event1 = new Event({ time: Date.now() }); - var event2 = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' }); - var event3 = new SignedUpEvent({ time: Date.now(), user: 'testuser' }); + it('Discriminators save to the Event model\'s collection', function (done) { + var event1 = new Event({time: Date.now()}); + var event2 = new ClickedLinkEvent({time: Date.now(), url: 'google.com'}); + var event3 = new SignedUpEvent({time: Date.now(), user: 'testuser'}); - var save = function(doc, callback) { - doc.save(function(error, doc) { + var save = function (doc, callback) { + doc.save(function (error, doc) { callback(error, doc); }); }; - async.map([event1, event2, event3], save, function(error) { + async.map([event1, event2, event3], save, function (error) { // acquit:ignore:start assert.ifError(error); // acquit:ignore:end - Event.count({}, function(error, count) { + Event.count({}, function (error, count) { // acquit:ignore:start assert.ifError(error); // acquit:ignore:end @@ -102,18 +110,18 @@ describe('discriminator docs', function() { /** * The way mongoose tells the difference between the different * discriminator models is by the 'discriminator key', which is - * `kind` by default. Mongoose adds a String path called `kind` + * `__t` by default. Mongoose adds a String path called `__t` * to your schemas that it uses to track which discriminator * this document is an instance of. */ - it('Discriminator keys', function(done) { - var event1 = new Event({ time: Date.now() }); - var event2 = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' }); - var event3 = new SignedUpEvent({ time: Date.now(), user: 'testuser' }); + it('Discriminator keys', function (done) { + var event1 = new Event({time: Date.now()}); + var event2 = new ClickedLinkEvent({time: Date.now(), url: 'google.com'}); + var event3 = new SignedUpEvent({time: Date.now(), user: 'testuser'}); - assert.ok(!event1.kind); - assert.equal(event2.kind, 'ClickedLink'); - assert.equal(event3.kind, 'SignedUp'); + assert.ok(!event1.__t); + assert.equal(event2.__t, 'ClickedLink'); + assert.equal(event3.__t, 'SignedUp'); // acquit:ignore:start done(); @@ -125,23 +133,23 @@ describe('discriminator docs', function() { * to queries. In other words, `find()`, `count()`, `aggregate()`, etc. * are smart enough to account for discriminators. */ - it('Discriminators add the discriminator key to queries', function(done) { - var event1 = new Event({ time: Date.now() }); - var event2 = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' }); - var event3 = new SignedUpEvent({ time: Date.now(), user: 'testuser' }); + it('Discriminators add the discriminator key to queries', function (done) { + var event1 = new Event({time: Date.now()}); + var event2 = new ClickedLinkEvent({time: Date.now(), url: 'google.com'}); + var event3 = new SignedUpEvent({time: Date.now(), user: 'testuser'}); - var save = function(doc, callback) { - doc.save(function(error, doc) { + var save = function (doc, callback) { + doc.save(function (error, doc) { callback(error, doc); }); }; - async.map([event1, event2, event3], save, function(error) { + async.map([event1, event2, event3], save, function (error) { // acquit:ignore:start assert.ifError(error); // acquit:ignore:end - ClickedLinkEvent.find({}, function(error, docs) { + ClickedLinkEvent.find({}, function (error, docs) { // acquit:ignore:start assert.ifError(error); // acquit:ignore:end @@ -160,24 +168,24 @@ describe('discriminator docs', function() { * However, you can also attach middleware to the discriminator schema * without affecting the base schema. */ - it('Discriminators copy pre and post hooks', function(done) { - var options = { discriminatorKey: 'kind' }; + it('Discriminators copy pre and post hooks', function (done) { + var options = {discriminatorKey: 'kind'}; - var eventSchema = new mongoose.Schema({ time: Date }, options); + var eventSchema = new mongoose.Schema({time: Date}, options); var eventSchemaCalls = 0; - eventSchema.pre('validate', function(next) { + eventSchema.pre('validate', function (next) { ++eventSchemaCalls; next(); }); var Event = mongoose.model('GenericEvent', eventSchema); - var clickedLinkSchema = new mongoose.Schema({ url: String }, options); + var clickedLinkSchema = new mongoose.Schema({url: String}, options); var clickedSchemaCalls = 0; - clickedLinkSchema.pre('validate', function(next) { + clickedLinkSchema.pre('validate', function (next) { ++clickedSchemaCalls; next(); }); - var ClickedLinkEvent = Event.discriminator('ClickedLink', + var ClickedLinkEvent = Event.discriminator('ClickedLinkEvent', clickedLinkSchema); var event1 = new ClickedLinkEvent(); @@ -195,4 +203,205 @@ describe('discriminator docs', function() { }); }); }); + + /** + * A discriminator's fields are the union of the base schema's fields and + * the discriminator schema's fields, and the discriminator schema's fields + * take precedence. There is one exception: the default `_id` field. + * + * You can work around this by setting the `_id` option to false in the + * discriminator schema as shown below. + */ + it('Handling custom _id fields', function (done) { + var options = {discriminatorKey: 'kind'}; + + // Base schema has a String `_id` and a Date `time`... + var eventSchema = new mongoose.Schema({_id: String, time: Date}, + options); + var Event = mongoose.model('BaseEvent', eventSchema); + + var clickedLinkSchema = new mongoose.Schema({ + url: String, + time: String + }, options); + // But the discriminator schema has a String `time`, and an implicitly added + // ObjectId `_id`. + assert.ok(clickedLinkSchema.path('_id')); + assert.equal(clickedLinkSchema.path('_id').instance, 'ObjectID'); + var ClickedLinkEvent = Event.discriminator('ChildEventBad', + clickedLinkSchema); + + var event1 = new ClickedLinkEvent({ _id: 'custom id', time: '4pm' }); + // Woops, clickedLinkSchema overwrites the `time` path, but **not** + // the `_id` path because that was implicitly added. + assert.ok(typeof event1._id === 'string'); + assert.ok(typeof event1.time === 'string'); + + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + + /** + * When you use `Model.create()`, mongoose will pull the correct type from + * the discriminator key for you. + */ + it('Using discriminators with `Model.create()`', function(done) { + var Schema = mongoose.Schema; + var shapeSchema = new Schema({ + name: String + }, { discriminatorKey: 'kind' }); + + var Shape = db.model('Shape', shapeSchema); + + var Circle = Shape.discriminator('Circle', + new Schema({ radius: Number })); + var Square = Shape.discriminator('Square', + new Schema({ side: Number })); + + var shapes = [ + { name: 'Test' }, + { kind: 'Circle', radius: 5 }, + { kind: 'Square', side: 10 } + ]; + Shape.create(shapes, function(error, shapes) { + assert.ifError(error); + assert.ok(shapes[0] instanceof Shape); + assert.ok(shapes[1] instanceof Circle); + assert.equal(shapes[1].radius, 5); + assert.ok(shapes[2] instanceof Square); + assert.equal(shapes[2].side, 10); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); + + /** + * You can also define discriminators on embedded document arrays. + * Embedded discriminators are different because the different discriminator + * types are stored in the same document array (within a document) rather + * than the same collection. In other words, embedded discriminators let + * you store subdocuments matching different schemas in the same array. + * + * As a general best practice, make sure you declare any hooks on your + * schemas **before** you use them. You should **not** call `pre()` or + * `post()` after calling `discriminator()` + */ + it('Embedded discriminators in arrays', function(done) { + var eventSchema = new Schema({ message: String }, + { discriminatorKey: 'kind', _id: false }); + + var batchSchema = new Schema({ events: [eventSchema] }); + + // `batchSchema.path('events')` gets the mongoose `DocumentArray` + var docArray = batchSchema.path('events'); + + // The `events` array can contain 2 different types of events, a + // 'clicked' event that requires an element id that was clicked... + var clickedSchema = new Schema({ + element: { + type: String, + required: true + } + }, { _id: false }); + // Make sure to attach any hooks to `eventSchema` and `clickedSchema` + // **before** calling `discriminator()`. + var Clicked = docArray.discriminator('Clicked', clickedSchema); + + // ... and a 'purchased' event that requires the product that was purchased. + var Purchased = docArray.discriminator('Purchased', new Schema({ + product: { + type: String, + required: true + } + }, { _id: false })); + + var Batch = db.model('EventBatch', batchSchema); + + // Create a new batch of events with different kinds + var batch = { + events: [ + { kind: 'Clicked', element: '#hero', message: 'hello' }, + { kind: 'Purchased', product: 'action-figure-1', message: 'world' } + ] + }; + + Batch.create(batch). + then(function(doc) { + assert.equal(doc.events.length, 2); + + assert.equal(doc.events[0].element, '#hero'); + assert.equal(doc.events[0].message, 'hello'); + assert.ok(doc.events[0] instanceof Clicked); + + assert.equal(doc.events[1].product, 'action-figure-1'); + assert.equal(doc.events[1].message, 'world'); + assert.ok(doc.events[1] instanceof Purchased); + + doc.events.push({ kind: 'Purchased', product: 'action-figure-2' }); + return doc.save(); + }). + then(function(doc) { + assert.equal(doc.events.length, 3); + + assert.equal(doc.events[2].product, 'action-figure-2'); + assert.ok(doc.events[2] instanceof Purchased); + + done(); + }). + catch(done); + }); + + /** + * Recursive embedded discriminators + */ + it('Recursive embedded discriminators in arrays', function(done) { + var singleEventSchema = new Schema({ message: String }, + { discriminatorKey: 'kind', _id: false }); + + var eventListSchema = new Schema({ events: [singleEventSchema] }); + + var subEventSchema = new Schema({ + sub_events: [singleEventSchema] + }, { _id: false }); + + var SubEvent = subEventSchema.path('sub_events').discriminator('SubEvent', subEventSchema) + eventListSchema.path('events').discriminator('SubEvent', subEventSchema); + + var Eventlist = db.model('EventList', eventListSchema); + + // Create a new batch of events with different kinds + var list = { + events: [ + { kind: 'SubEvent', sub_events: [{kind:'SubEvent', sub_events:[], message:'test1'}], message: 'hello' }, + { kind: 'SubEvent', sub_events: [{kind:'SubEvent', sub_events:[{kind:'SubEvent', sub_events:[], message:'test3'}], message:'test2'}], message: 'world' } + ] + }; + + Eventlist.create(list). + then(function(doc) { + assert.equal(doc.events.length, 2); + + assert.equal(doc.events[0].sub_events[0].message, 'test1'); + assert.equal(doc.events[0].message, 'hello'); + assert.ok(doc.events[0].sub_events[0] instanceof SubEvent); + + assert.equal(doc.events[1].sub_events[0].sub_events[0].message, 'test3'); + assert.equal(doc.events[1].message, 'world'); + assert.ok(doc.events[1].sub_events[0].sub_events[0] instanceof SubEvent); + + doc.events.push({kind:'SubEvent', sub_events:[{kind:'SubEvent', sub_events:[], message:'test4'}], message:'pushed'}); + return doc.save(); + }). + then(function(doc) { + assert.equal(doc.events.length, 3); + + assert.equal(doc.events[2].message, 'pushed'); + assert.ok(doc.events[2].sub_events[0] instanceof SubEvent); + + done(); + }). + catch(done); + }); }); diff --git a/test/docs/es6_gateway.test.js b/test/docs/es6_gateway.test.js new file mode 100644 index 00000000000..b0532ce1fd2 --- /dev/null +++ b/test/docs/es6_gateway.test.js @@ -0,0 +1,5 @@ +'use strict'; + +if (parseInt(process.versions.node.split('.')[0], 10) >= 4) { + require('./schemas.test.es6.js'); +} diff --git a/test/docs/promises.test.js b/test/docs/promises.test.js index b7031dae2b0..e0496c2f38e 100644 --- a/test/docs/promises.test.js +++ b/test/docs/promises.test.js @@ -1,24 +1,26 @@ var PromiseProvider = require('../../lib/promise_provider'); -var assert = require('assert'); -var async = require('async'); +var assert = require('power-assert'); var mongoose = require('../../'); -describe('promises docs', function() { +describe('promises docs', function () { var Band; - var db = mongoose.createConnection('mongodb://localhost:27017/mongoose_test'); + var db; - before(function(done) { - Band = db.model('band-promises', { name: String, members: [String] }); + before(function (done) { + db = mongoose.createConnection('mongodb://localhost:27017/mongoose_test'); + + Band = db.model('band-promises', {name: String, members: [String]}); done(); }); - beforeEach(function(done) { + beforeEach(function (done) { Band.remove({}, done); }); - after(function() { + after(function (done) { PromiseProvider.reset(); + db.close(done); }); /** @@ -30,7 +32,7 @@ describe('promises docs', function() { * For backwards compatibility, Mongoose 4 returns [mpromise](https://www.npmjs.com/package/mpromise) * promises by default. */ - it('Built-in Promises', function(done) { + it('Built-in Promises', function (done) { var gnr = new Band({ name: "Guns N' Roses", members: ['Axl', 'Slash'] @@ -39,7 +41,7 @@ describe('promises docs', function() { var promise = gnr.save(); assert.ok(promise instanceof require('mpromise')); - promise.then(function(doc) { + promise.then(function (doc) { assert.equal(doc.name, "Guns N' Roses"); // acquit:ignore:start done(); @@ -52,8 +54,8 @@ describe('promises docs', function() { * function for `yield` and async/await. If you need * a fully-fledged promise, use the `.exec()` function. */ - it('Queries are not promises', function(done) { - var query = Band.findOne({ name: "Guns N' Roses" }); + it('Queries are not promises', function (done) { + var query = Band.findOne({name: "Guns N' Roses"}); assert.ok(!(query instanceof require('mpromise'))); // acquit:ignore:start @@ -61,7 +63,7 @@ describe('promises docs', function() { // acquit:ignore:end // A query is not a fully-fledged promise, but it does have a `.then()`. - query.then(function(doc) { + query.then(function (doc) { // use doc // acquit:ignore:start assert.ok(!doc); @@ -73,7 +75,7 @@ describe('promises docs', function() { var promise = query.exec(); assert.ok(promise instanceof require('mpromise')); - promise.then(function(doc) { + promise.then(function (doc) { // use doc // acquit:ignore:start assert.ok(!doc); @@ -99,13 +101,13 @@ describe('promises docs', function() { * often differs from practice. If you find a bug, open * [an issue on GitHub](https://github.com/Automattic/mongoose/issues) */ - it('Plugging in your own Promises Library', function(done) { + it('Plugging in your own Promises Library', function (done) { // acquit:ignore:start if (!global.Promise) { return done(); } // acquit:ignore:end - var query = Band.findOne({ name: "Guns N' Roses" }); + var query = Band.findOne({name: "Guns N' Roses"}); // Use native promises mongoose.Promise = global.Promise; @@ -123,4 +125,32 @@ describe('promises docs', function() { done(); // acquit:ignore:end }); + + /** + * The `mongoose.Promise` property sets the promises mongoose uses. However, + * this does **not** affect the underlying MongoDB driver. If you use the + * underlying driver, for instance `Model.collection.db.insert()`, you + * need to do a little extra work to change the underlying promises library. + * Note that the below code assumes mongoose >= 4.4.4. + */ + it('Promises for the MongoDB Driver', function (done) { + // acquit:ignore:start + if (!global.Promise) { + return done(); + } + // acquit:ignore:end + var uri = 'mongodb://localhost:27017/mongoose_test'; + // Use bluebird + var options = { promiseLibrary: require('bluebird') }; + var db = mongoose.createConnection(uri, options); + + Band = db.model('band-promises', { name: String }); + + db.on('open', function() { + assert.equal(Band.collection.findOne().constructor, require('bluebird')); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); }); diff --git a/test/docs/schemas.test.es6.js b/test/docs/schemas.test.es6.js new file mode 100644 index 00000000000..152c176c3b7 --- /dev/null +++ b/test/docs/schemas.test.es6.js @@ -0,0 +1,72 @@ +'use strict'; + +var assert = require('assert'); +var mongoose = require('../../'); + +describe('Advanced Schemas', function () { + var db; + var Schema = mongoose.Schema; + + before(function() { + db = mongoose.createConnection('mongodb://localhost:27017/mongoose_test'); + }); + + after(function(done) { + db.close(done); + }); + + /** + * Mongoose allows creating schemas from [ES6 classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes). + * The `loadClass()` function lets you pull in methods, + * statics, and virtuals from an ES6 class. A class method maps to a schema + * method, a static method maps to a schema static, and getters/setters map + * to virtuals. + */ + it('Creating from ES6 Classes Using `loadClass()`', function(done) { + const schema = new Schema({ firstName: String, lastName: String }); + + class PersonClass { + // `fullName` becomes a virtual + get fullName() { + return `${this.firstName} ${this.lastName}`; + } + + set fullName(v) { + const firstSpace = v.indexOf(' '); + this.firstName = v.split(' ')[0]; + this.lastName = firstSpace === -1 ? '' : v.substr(firstSpace + 1); + } + + // `getFullName()` becomes a document method + getFullName() { + return `${this.firstName} ${this.lastName}`; + } + + // `findByFullName()` becomes a static + static findByFullName(name) { + const firstSpace = name.indexOf(' '); + const firstName = name.split(' ')[0]; + const lastName = firstSpace === -1 ? '' : name.substr(firstSpace + 1); + return this.findOne({ firstName, lastName }); + } + } + + schema.loadClass(PersonClass); + var Person = db.model('Person', schema); + + Person.create({ firstName: 'Jon', lastName: 'Snow' }). + then(doc => { + assert.equal(doc.fullName, 'Jon Snow'); + doc.fullName = 'Jon Stark'; + assert.equal(doc.firstName, 'Jon'); + assert.equal(doc.lastName, 'Stark'); + return Person.findByFullName('Jon Snow'); + }). + then(doc => { + assert.equal(doc.fullName, 'Jon Snow'); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); +}); diff --git a/test/docs/schematypes.test.js b/test/docs/schematypes.test.js new file mode 100644 index 00000000000..28834d9d315 --- /dev/null +++ b/test/docs/schematypes.test.js @@ -0,0 +1,66 @@ +var assert = require('assert'); +var mongoose = require('../../'); + +describe('schemaTypes', function () { + var db; + var Schema = mongoose.Schema; + + before(function() { + db = mongoose.createConnection('mongodb://localhost:27017/mongoose_test'); + }); + + after(function(done) { + db.close(done); + }); + + /** + * _New in Mongoose 4.4.0:_ Mongoose supports custom types. Before you + * reach for a custom type, however, know that a custom type is overkill + * for most use cases. You can do most basic tasks with + * [custom getters/setters](http://mongoosejs.com/docs/2.7.x/docs/getters-setters.html), + * [virtuals](http://mongoosejs.com/docs/guide.html#virtuals), and + * [single embedded docs](http://mongoosejs.com/docs/subdocs.html#single-embedded). + * + * Let's take a look at an example of a basic schema type: a 1-byte integer. + * To create a new schema type, you need to inherit from `mongoose.SchemaType` + * and add the corresponding property to `mongoose.Schema.Types`. The one + * method you need to implement is the `cast()` method. + */ + it('Creating a Basic Custom Schema Type', function() { + function Int8(key, options) { + mongoose.SchemaType.call(this, key, options, 'Int8'); + } + Int8.prototype = Object.create(mongoose.SchemaType.prototype); + + // `cast()` takes a parameter that can be anything. You need to + // validate the provided `val` and throw a `CastError` if you + // can't convert it. + Int8.prototype.cast = function(val) { + var _val = Number(val); + if (isNaN(_val)) { + throw new Error('Int8: ' + val + ' is not a number'); + } + _val = Math.round(_val); + if (_val < -0x80 || _val > 0x7F) { + throw new Error('Int8: ' + val + + ' is outside of the range of valid 8-bit ints'); + } + return _val; + }; + + // Don't forget to add `Int8` to the type registry + mongoose.Schema.Types.Int8 = Int8; + + var testSchema = new Schema({ test: Int8 }); + var Test = mongoose.model('CustomTypeExample', testSchema); + + var t = new Test(); + t.test = 'abc'; + assert.ok(t.validateSync()); + assert.equal(t.validateSync().errors['test'].name, 'CastError'); + assert.equal(t.validateSync().errors['test'].message, + 'Cast to Int8 failed for value "abc" at path "test"'); + assert.equal(t.validateSync().errors['test'].reason.message, + 'Int8: abc is not a number'); + }); +}); diff --git a/test/docs/validation.test.js b/test/docs/validation.test.js new file mode 100644 index 00000000000..17bfcd3da35 --- /dev/null +++ b/test/docs/validation.test.js @@ -0,0 +1,609 @@ +var assert = require('assert'); +var mongoose = require('../../'); + +var Promise = global.Promise || require('bluebird'); + +describe('validation docs', function() { + var db; + var Schema = mongoose.Schema; + + before(function() { + db = mongoose.createConnection('mongodb://localhost:27017/mongoose_test', { + useMongoClient: true, + poolSize: 1 + }); + }); + + after(function(done) { + db.close(done); + }); + + /** + * Before we get into the specifics of validation syntax, please keep the following rules in mind: + * + * - Validation is defined in the [SchemaType](./schematypes.html) + * - Validation is [middleware](./middleware.html). Mongoose registers validation as a `pre('save')` hook on every schema by default. + * - You can manually run validation using `doc.validate(callback)` or `doc.validateSync()` + * - Validators are not run on undefined values. The only exception is the [`required` validator](./api.html#schematype_SchemaType-required). + * - Validation is asynchronously recursive; when you call [Model#save](./api.html#model_Model-save), sub-document validation is executed as well. If an error occurs, your [Model#save](./api.html#model_Model-save) callback receives it + * - Validation is customizable + */ + + it('Validation', function(done) { + var schema = new Schema({ + name: { + type: String, + required: true + } + }); + var Cat = db.model('Cat', schema); + + // This cat has no name :( + var cat = new Cat(); + cat.save(function(error) { + assert.equal(error.errors['name'].message, + 'Path `name` is required.'); + + error = cat.validateSync(); + assert.equal(error.errors['name'].message, + 'Path `name` is required.'); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); + + /** + * Mongoose has several built-in validators. + * + * - All [SchemaTypes](./schematypes.html) have the built-in [required](./api.html#schematype_SchemaType-required) validator. The required validator uses the [SchemaType's `checkRequired()` function](./api.html#schematype_SchemaType-checkRequired) to determine if the value satisfies the required validator. + * - [Numbers](./api.html#schema-number-js) have [min](./api.html#schema_number_SchemaNumber-min) and [max](./api.html#schema_number_SchemaNumber-max) validators. + * - [Strings](./api.html#schema-string-js) have [enum](./api.html#schema_string_SchemaString-enum), [match](./api.html#schema_string_SchemaString-match), [maxlength](./api.html#schema_string_SchemaString-maxlength) and [minlength](./api.html#schema_string_SchemaString-minlength) validators. + * + * Each of the validator links above provide more information about how to enable them and customize their error messages. + */ + + it('Built-in Validators', function(done) { + var breakfastSchema = new Schema({ + eggs: { + type: Number, + min: [6, 'Too few eggs'], + max: 12 + }, + bacon: { + type: Number, + required: [true, 'Why no bacon?'] + }, + drink: { + type: String, + enum: ['Coffee', 'Tea'], + required: function() { + return this.bacon > 3; + } + } + }); + var Breakfast = db.model('Breakfast', breakfastSchema); + + var badBreakfast = new Breakfast({ + eggs: 2, + bacon: 0, + drink: 'Milk' + }); + var error = badBreakfast.validateSync(); + assert.equal(error.errors['eggs'].message, + 'Too few eggs'); + assert.ok(!error.errors['bacon']); + assert.equal(error.errors['drink'].message, + '`Milk` is not a valid enum value for path `drink`.'); + + badBreakfast.bacon = 5; + badBreakfast.drink = null; + + error = badBreakfast.validateSync(); + assert.equal(error.errors['drink'].message, 'Path `drink` is required.'); + + badBreakfast.bacon = null; + error = badBreakfast.validateSync(); + assert.equal(error.errors['bacon'].message, 'Why no bacon?'); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + + /** + * A common gotcha for beginners is that the `unique` option for schemas + * is *not* a validator. It's a convenient helper for building [MongoDB unique indexes](https://docs.mongodb.com/manual/core/index-unique/). + * See the [FAQ](/docs/faq.html) for more information. + */ + + it('The `unique` Option is Not a Validator', function(done) { + var uniqueUsernameSchema = new Schema({ + username: { + type: String, + unique: true + } + }); + var U1 = db.model('U1', uniqueUsernameSchema); + var U2 = db.model('U2', uniqueUsernameSchema); + // acquit:ignore:start + var remaining = 3; + // acquit:ignore:end + + var dup = [{ username: 'Val' }, { username: 'Val' }]; + U1.create(dup, function(error) { + // Will save successfully! + // acquit:ignore:start + assert.ifError(error); + --remaining || done(); + // acquit:ignore:end + }); + + // Need to wait for the index to finish building before saving, + // otherwise unique constraints may be violated. + U2.on('index', function(error) { + assert.ifError(error); + U2.create(dup, function(error) { + // Will error, but will *not* be a mongoose validation error, it will be + // a duplicate key error. + assert.ok(error); + assert.ok(!error.errors); + assert.ok(error.message.indexOf('duplicate key error') !== -1); + // acquit:ignore:start + --remaining || done(); + // acquit:ignore:end + }); + }); + + // There's also a promise-based equivalent to the event emitter API. + // The `init()` function is idempotent and returns a promise that + // will resolve once indexes are done building; + U2.init().then(function() { + U2.create(dup, function(error) { + // Will error, but will *not* be a mongoose validation error, it will be + // a duplicate key error. + assert.ok(error); + assert.ok(!error.errors); + assert.ok(error.message.indexOf('duplicate key error') !== -1); + // acquit:ignore:start + --remaining || done(); + // acquit:ignore:end + }); + }); + }); + + /** + * If the built-in validators aren't enough, you can define custom validators + * to suit your needs. + * + * Custom validation is declared by passing a validation function. + * You can find detailed instructions on how to do this in the + * [`SchemaType#validate()` API docs](./api.html#schematype_SchemaType-validate). + */ + it('Custom Validators', function(done) { + var userSchema = new Schema({ + phone: { + type: String, + validate: { + validator: function(v) { + return /\d{3}-\d{3}-\d{4}/.test(v); + }, + message: '{VALUE} is not a valid phone number!' + }, + required: [true, 'User phone number required'] + } + }); + + var User = db.model('user', userSchema); + var user = new User(); + var error; + + user.phone = '555.0123'; + error = user.validateSync(); + assert.equal(error.errors['phone'].message, + '555.0123 is not a valid phone number!'); + + user.phone = ''; + error = user.validateSync(); + assert.equal(error.errors['phone'].message, + 'User phone number required'); + + user.phone = '201-555-0123'; + // Validation succeeds! Phone number is defined + // and fits `DDD-DDD-DDDD` + error = user.validateSync(); + assert.equal(error, null); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + + /** + * Custom validators can also be asynchronous. If your validator function + * takes 2 arguments, mongoose will assume the 2nd argument is a callback. + * + * Even if you don't want to use asynchronous validators, be careful, + * because mongoose 4 will assume that **all** functions that take 2 + * arguments are asynchronous, like the + * [`validator.isEmail` function](https://www.npmjs.com/package/validator). + * This behavior is considered deprecated as of 4.9.0, and you can shut + * it off by specifying `isAsync: false` on your custom validator. + */ + it('Async Custom Validators', function(done) { + var userSchema = new Schema({ + phone: { + type: String, + validate: { + // `isAsync` is not strictly necessary in mongoose 4.x, but relying + // on 2 argument validators being async is deprecated. Set the + // `isAsync` option to `true` to make deprecation warnings go away. + isAsync: true, + validator: function(v, cb) { + setTimeout(function() { + var phoneRegex = /\d{3}-\d{3}-\d{4}/; + var msg = v + ' is not a valid phone number!'; + // First argument is a boolean, whether validator succeeded + // 2nd argument is an optional error message override + cb(phoneRegex.test(v), msg); + }, 5); + }, + // Default error message, overridden by 2nd argument to `cb()` above + message: 'Default error message' + }, + required: [true, 'User phone number required'] + }, + name: { + type: String, + // You can also make a validator async by returning a promise. If you + // return a promise, do **not** specify the `isAsync` option. + validate: function(v) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + resolve(false); + }, 5); + }); + } + } + }); + + var User = db.model('User', userSchema); + var user = new User(); + var error; + + user.phone = '555.0123'; + user.name = 'test'; + user.validate(function(error) { + assert.ok(error); + assert.equal(error.errors['phone'].message, + '555.0123 is not a valid phone number!'); + assert.equal(error.errors['name'].message, + 'Validator failed for path `name` with value `test`'); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); + + /** + * Errors returned after failed validation contain an `errors` object + * whose values are `ValidatorError` objects. Each + * [ValidatorError](./api.html#error-validation-js) has `kind`, `path`, + * `value`, and `message` properties. + * A ValidatorError also may have a `reason` property. If an error was + * thrown in the validator, this property will contain the error that was + * thrown. + */ + + it('Validation Errors', function(done) { + var toySchema = new Schema({ + color: String, + name: String + }); + + var validator = function(value) { + return /red|white|gold/i.test(value); + }; + toySchema.path('color').validate(validator, + 'Color `{VALUE}` not valid', 'Invalid color'); + toySchema.path('name').validate(function(v) { + if (v !== 'Turbo Man') { + throw new Error('Need to get a Turbo Man for Christmas'); + } + return true; + }, 'Name `{VALUE}` is not valid'); + + var Toy = db.model('Toy', toySchema); + + var toy = new Toy({ color: 'Green', name: 'Power Ranger' }); + + toy.save(function (err) { + // `err` is a ValidationError object + // `err.errors.color` is a ValidatorError object + assert.equal(err.errors.color.message, 'Color `Green` not valid'); + assert.equal(err.errors.color.kind, 'Invalid color'); + assert.equal(err.errors.color.path, 'color'); + assert.equal(err.errors.color.value, 'Green'); + + assert.equal(err.errors.name.message, 'Name `Power Ranger` is not valid'); + assert.equal(err.errors.name.value, 'Power Ranger'); + assert.equal(err.errors.name.reason.message, + 'Need to get a Turbo Man for Christmas'); + + assert.equal(err.name, 'ValidationError'); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); + + /** + * Defining validators on nested objects in mongoose is tricky, because + * nested objects are not fully fledged paths. + */ + + it('Required Validators On Nested Objects', function(done) { + var personSchema = new Schema({ + name: { + first: String, + last: String + } + }); + + assert.throws(function() { + // This throws an error, because 'name' isn't a full fledged path + personSchema.path('name').required(true); + }, /Cannot.*'required'/); + + // To make a nested object required, use a single nested schema + var nameSchema = new Schema({ + first: String, + last: String + }); + + personSchema = new Schema({ + name: { + type: nameSchema, + required: true + } + }); + + var Person = db.model('Person', personSchema); + + var person = new Person(); + var error = person.validateSync(); + assert.ok(error.errors['name']); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + + /** + * In the above examples, you learned about document validation. Mongoose also + * supports validation for `update()` and `findOneAndUpdate()` operations. + * In Mongoose 4.x, update validators are off by default - you need to specify + * the `runValidators` option. + * + * To turn on update validators, set the `runValidators` option for + * `update()` or `findOneAndUpdate()`. Be careful: update validators + * are off by default because they have several caveats. + */ + it('Update Validators', function(done) { + var toySchema = new Schema({ + color: String, + name: String + }); + + var Toy = db.model('Toys', toySchema); + + Toy.schema.path('color').validate(function (value) { + return /blue|green|white|red|orange|periwinkle/i.test(value); + }, 'Invalid color'); + + var opts = { runValidators: true }; + Toy.update({}, { color: 'bacon' }, opts, function (err) { + assert.equal(err.errors.color.message, + 'Invalid color'); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); + + /** + * There are a couple of key differences between update validators and + * document validators. In the color validation function above, `this` refers + * to the document being validated when using document validation. + * However, when running update validators, the document being updated + * may not be in the server's memory, so by default the value of `this` is + * not defined. + */ + + it('Update Validators and `this`', function(done) { + var toySchema = new Schema({ + color: String, + name: String + }); + + toySchema.path('color').validate(function(value) { + // When running in `validate()` or `validateSync()`, the + // validator can access the document using `this`. + // Does **not** work with update validators. + if (this.name.toLowerCase().indexOf('red') !== -1) { + return value !== 'red'; + } + return true; + }); + + var Toy = db.model('ActionFigure', toySchema); + + var toy = new Toy({ color: 'red', name: 'Red Power Ranger' }); + var error = toy.validateSync(); + assert.ok(error.errors['color']); + + var update = { color: 'red', name: 'Red Power Ranger' }; + var opts = { runValidators: true }; + + Toy.update({}, update, opts, function(error) { + // The update validator throws an error: + // "TypeError: Cannot read property 'toLowerCase' of undefined", + // because `this` is **not** the document being updated when using + // update validators + assert.ok(error); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); + + /** + * The `context` option lets you set the value of `this` in update validators + * to the underlying query. + */ + + it('The `context` option', function(done) { + // acquit:ignore:start + var toySchema = new Schema({ + color: String, + name: String + }); + // acquit:ignore:end + toySchema.path('color').validate(function(value) { + // When running update validators with the `context` option set to + // 'query', `this` refers to the query object. + if (this.getUpdate().$set.name.toLowerCase().indexOf('red') !== -1) { + return value === 'red'; + } + return true; + }); + + var Toy = db.model('Figure', toySchema); + + var update = { color: 'blue', name: 'Red Power Ranger' }; + // Note the context option + var opts = { runValidators: true, context: 'query' }; + + Toy.update({}, update, opts, function(error) { + assert.ok(error.errors['color']); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); + + /** + * The other key difference that update validators only run on the paths + * specified in the update. For instance, in the below example, because + * 'name' is not specified in the update operation, update validation will + * succeed. + * + * When using update validators, `required` validators **only** fail when + * you try to explicitly `$unset` the key. + */ + + it('Update Validator Paths', function(done) { + // acquit:ignore:start + var outstanding = 2; + // acquit:ignore:end + var kittenSchema = new Schema({ + name: { type: String, required: true }, + age: Number + }); + + var Kitten = db.model('Kitten', kittenSchema); + + var update = { color: 'blue' }; + var opts = { runValidators: true }; + Kitten.update({}, update, opts, function(err) { + // Operation succeeds despite the fact that 'name' is not specified + // acquit:ignore:start + --outstanding || done(); + // acquit:ignore:end + }); + + var unset = { $unset: { name: 1 } }; + Kitten.update({}, unset, opts, function(err) { + // Operation fails because 'name' is required + assert.ok(err); + assert.ok(err.errors['name']); + // acquit:ignore:start + --outstanding || done(); + // acquit:ignore:end + }); + }); + + /** + * One final detail worth noting: update validators **only** run on the + * following update operators: + * + * - `$set` + * - `$unset` + * - `$push` (>= 4.8.0) + * - `$addToSet` (>= 4.8.0) + * - `$pull` (>= 4.12.0) + * - `$pullAll` (>= 4.12.0) + * + * For instance, the below update will succeed, regardless of the value of + * `number`, because update validators ignore `$inc`. Also, `$push`, + * `$addToSet`, `$pull`, and `$pullAll` validation does **not** run any + * validation on the array itself, only individual elements of the array. + */ + + it('Update Validators Only Run On Specified Paths', function(done) { + var testSchema = new Schema({ + number: { type: Number, max: 0 }, + arr: [{ message: { type: String, maxLength: 10 } }] + }); + + // Update validators won't check this, so you can still `$push` 2 elements + // onto the array, so long as they don't have a `message` that's too long. + testSchema.path('arr').validate(function(v) { + return v.length < 2; + }); + + var Test = db.model('Test', testSchema); + + var update = { $inc: { number: 1 } }; + var opts = { runValidators: true }; + Test.update({}, update, opts, function(error) { + // There will never be a validation error here + update = { $push: [{ message: 'hello' }, { message: 'world' }] }; + Test.update({}, update, opts, function(error) { + // This will never error either even though the array will have at + // least 2 elements. + // acquit:ignore:start + assert.ifError(error); + done(); + // acquit:ignore:end + }); + }); + }); + + /** + * New in 4.8.0: update validators also run on `$push` and `$addToSet` + */ + + it('On $push and $addToSet', function(done) { + var testSchema = new Schema({ + numbers: [{ type: Number, max: 0 }], + docs: [{ + name: { type: String, required: true } + }] + }); + + var Test = db.model('TestPush', testSchema); + + var update = { + $push: { + numbers: 1, + docs: { name: null } + } + }; + var opts = { runValidators: true }; + Test.update({}, update, opts, function(error) { + assert.ok(error.errors['numbers']); + assert.ok(error.errors['docs']); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); +}); diff --git a/test/document.hooks.test.js b/test/document.hooks.test.js index 0ad70da64f5..948831f8f39 100644 --- a/test/document.hooks.test.js +++ b/test/document.hooks.test.js @@ -1,15 +1,14 @@ - /** * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , Schema = mongoose.Schema - , ObjectId = Schema.ObjectId - , Document = require('../lib/document') - , EmbeddedDocument = require('../lib/types/embedded'); +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema, + ObjectId = Schema.ObjectId, + Document = require('../lib/document'), + EmbeddedDocument = require('../lib/types/embedded'); /** * Test Document constructor. @@ -29,30 +28,30 @@ TestDocument.prototype.__proto__ = Document.prototype; * Set a dummy schema to simulate compilation. */ -var em = new Schema({ title: String, body: String }); +var em = new Schema({title: String, body: String}); em.virtual('works').get(function() { return 'em virtual works'; }); var schema = new Schema({ - test : String - , oids : [ObjectId] - , numbers : [Number] - , nested : { - age : Number - , cool : ObjectId - , deep : { x: String } - , path : String - , setr : String - } - , nested2 : { - nested: String - , yup : { - nested : Boolean - , yup : String - , age : Number - } - } - , em: [em] + test: String, + oids: [ObjectId], + numbers: [Number], + nested: { + age: Number, + cool: ObjectId, + deep: {x: String}, + path: String, + setr: String + }, + nested2: { + nested: String, + yup: { + nested: Boolean, + yup: String, + age: Number + } + }, + em: [em] }); TestDocument.prototype.$__setSchema(schema); @@ -80,30 +79,30 @@ TestDocument.prototype.hooksTest = function(fn) { describe('document: hooks:', function() { it('step order', function(done) { - var doc = new TestDocument() - , steps = 0; + var doc = new TestDocument(), + steps = 0; // serial - doc.pre('hooksTest', function(next) { + doc.$pre('hooksTest', function(next) { steps++; setTimeout(function() { // make sure next step hasn't executed yet - assert.equal(1, steps); + assert.equal(steps, 1); next(); }, 50); }); - doc.pre('hooksTest', function(next) { + doc.$pre('hooksTest', function(next) { steps++; next(); }); // parallel - doc.pre('hooksTest', true, function(next, done) { + doc.$pre('hooksTest', true, function(next, done) { steps++; - assert.equal(3, steps); + assert.equal(steps, 3); setTimeout(function() { - assert.equal(4, steps); + assert.equal(steps, 4); }, 10); setTimeout(function() { steps++; @@ -112,10 +111,10 @@ describe('document: hooks:', function() { next(); }); - doc.pre('hooksTest', true, function(next, done) { + doc.$pre('hooksTest', true, function(next, done) { steps++; setTimeout(function() { - assert.equal(4, steps); + assert.equal(steps, 4); }, 10); setTimeout(function() { steps++; @@ -126,46 +125,45 @@ describe('document: hooks:', function() { doc.hooksTest(function(err) { assert.ifError(err); - assert.equal(6, steps); + assert.equal(steps, 6); done(); }); - }); it('calling next twice does not break', function(done) { - var doc = new TestDocument() - , steps = 0; + var doc = new TestDocument(), + steps = 0; - doc.pre('hooksTest', function(next) { + doc.$pre('hooksTest', function(next) { steps++; next(); next(); }); - doc.pre('hooksTest', function(next) { + doc.$pre('hooksTest', function(next) { steps++; next(); }); doc.hooksTest(function(err) { assert.ifError(err); - assert.equal(2, steps); + assert.equal(steps, 2); done(); }); }); it('calling done twice does not break', function(done) { - var doc = new TestDocument() - , steps = 0; + var doc = new TestDocument(), + steps = 0; - doc.pre('hooksTest', true, function(next, done) { + doc.$pre('hooksTest', true, function(next, done) { steps++; next(); done(); done(); }); - doc.pre('hooksTest', true, function(next, done) { + doc.$pre('hooksTest', true, function(next, done) { steps++; next(); done(); @@ -174,32 +172,32 @@ describe('document: hooks:', function() { doc.hooksTest(function(err) { assert.ifError(err); - assert.equal(2, steps); + assert.equal(steps, 2); done(); }); }); it('errors from a serial hook', function(done) { - var doc = new TestDocument() - , steps = 0; + var doc = new TestDocument(), + steps = 0; - doc.pre('hooksTest', function(next) { + doc.$pre('hooksTest', function(next) { steps++; next(); }); - doc.pre('hooksTest', function(next) { + doc.$pre('hooksTest', function(next) { steps++; next(new Error); }); - doc.pre('hooksTest', function() { + doc.$pre('hooksTest', function() { steps++; }); doc.hooksTest(function(err) { assert.ok(err instanceof Error); - assert.equal(2, steps); + assert.equal(steps, 2); done(); }); }); @@ -207,7 +205,7 @@ describe('document: hooks:', function() { it('errors from last serial hook', function(done) { var doc = new TestDocument(); - doc.pre('hooksTest', function(next) { + doc.$pre('hooksTest', function(next) { next(new Error); }); @@ -220,32 +218,32 @@ describe('document: hooks:', function() { it('mutating incoming args via middleware', function(done) { var doc = new TestDocument(); - doc.pre('set', function(next, path, val) { + doc.$pre('set', function(next, path, val) { next(path, 'altered-' + val); }); doc.set('test', 'me'); - assert.equal('altered-me', doc.test); + assert.equal(doc.test, 'altered-me'); done(); }); it('test hooks system errors from a parallel hook', function(done) { - var doc = new TestDocument() - , steps = 0; + var doc = new TestDocument(), + steps = 0; - doc.pre('hooksTest', true, function(next, done) { + doc.$pre('hooksTest', true, function(next, done) { steps++; next(); done(); }); - doc.pre('hooksTest', true, function(next, done) { + doc.$pre('hooksTest', true, function(next, done) { steps++; next(); done(); }); - doc.pre('hooksTest', true, function(next, done) { + doc.$pre('hooksTest', true, function(next, done) { steps++; next(); done(new Error); @@ -253,7 +251,7 @@ describe('document: hooks:', function() { doc.hooksTest(function(err) { assert.ok(err instanceof Error); - assert.equal(3, steps); + assert.equal(steps, 3); done(); }); }); @@ -261,12 +259,12 @@ describe('document: hooks:', function() { it('passing two arguments to a method subject to hooks and return value', function(done) { var doc = new TestDocument(); - doc.pre('hooksTest', function(next) { + doc.$pre('hooksTest', function(next) { next(); }); doc.hooksTest(function(err, args) { - assert.equal(2, args.length); + assert.equal(args.length, 2); assert.equal(args[1], 'test'); done(); }, 'test'); @@ -275,35 +273,35 @@ describe('document: hooks:', function() { it('hooking set works with document arrays (gh-746)', function(done) { var db = start(); - var child = new Schema({ text: String }); + var child = new Schema({text: String}); child.pre('set', function(next, path, value, type) { next(path, value, type); }); var schema = new Schema({ - name: String - , e: [child] + name: String, + e: [child] }); var S = db.model('docArrayWithHookedSet', schema); - var s = new S({ name: "test" }); - s.e = [{ text: 'hi' }]; + var s = new S({name: 'test'}); + s.e = [{text: 'hi'}]; s.save(function(err) { assert.ifError(err); - S.findById(s.id, function(err ,s) { + S.findById(s.id, function(err, s) { assert.ifError(err); - s.e = [{ text: 'bye' }]; + s.e = [{text: 'bye'}]; s.save(function(err) { assert.ifError(err); S.findById(s.id, function(err, s) { db.close(); assert.ifError(err); - assert.equal('bye', s.e[0].text); + assert.equal(s.e[0].text, 'bye'); done(); }); }); @@ -315,7 +313,7 @@ describe('document: hooks:', function() { var db = start(); var presave = false; - var child = new Schema({ text: { type: String, required: true }}); + var child = new Schema({text: {type: String, required: true}}); child.pre('save', function(next) { presave = true; @@ -323,19 +321,19 @@ describe('document: hooks:', function() { }); var schema = new Schema({ - name: String - , e: [child] + name: String, + e: [child] }); var S = db.model('docArrayWithHookedSave', schema); - var s = new S({ name: 'hi', e: [{}] }); + var s = new S({name: 'hi', e: [{}]}); s.save(function(err) { db.close(); try { assert.ok(err); assert.ok(err.errors['e.0.text']); - assert.equal(false, presave); + assert.equal(presave, false); done(); } catch (e) { done(e); @@ -345,8 +343,8 @@ describe('document: hooks:', function() { it('post remove hooks on subdocuments work', function(done) { var db = start(); - var sub = Schema({ _id: Number }); - var called = { pre: 0, post: 0 }; + var sub = new Schema({_id: Number}); + var called = {pre: 0, post: 0}; sub.pre('remove', function(next) { called.pre++; @@ -358,42 +356,42 @@ describe('document: hooks:', function() { assert.ok(doc instanceof Document); }); - var par = Schema({ sub: [sub], name: String }); + var par = new Schema({sub: [sub], name: String}); var M = db.model('post-remove-hooks-sub', par); - var m = new M({ sub: [{ _id: 1 }, { _id: 2 }] }); + var m = new M({sub: [{_id: 1}, {_id: 2}]}); m.save(function(err) { assert.ifError(err); - assert.equal(0, called.pre); - assert.equal(0, called.post); + assert.equal(called.pre, 0); + assert.equal(called.post, 0); - M.findById(m, function(err, doc) { - assert.ifError(err); + M.findById(m, function(err1, doc) { + assert.ifError(err1); doc.sub.id(1).remove(); - doc.save(function(err) { - assert.ifError(err); - assert.equal(1, called.pre); - assert.equal(1, called.post); + doc.save(function(err2) { + assert.ifError(err2); + assert.equal(called.pre, 1); + assert.equal(called.post, 1); // does not get called when not removed doc.name = 'changed1'; - doc.save(function(err) { - assert.ifError(err); - assert.equal(1, called.pre); - assert.equal(1, called.post); + doc.save(function(err3) { + assert.ifError(err3); + assert.equal(called.pre, 1); + assert.equal(called.post, 1); doc.sub.id(2).remove(); - doc.remove(function(err) { - assert.ifError(err); - assert.equal(2, called.pre); - assert.equal(2, called.post); + doc.remove(function(err4) { + assert.ifError(err4); + assert.equal(called.pre, 2); + assert.equal(called.post, 2); // does not get called twice - doc.remove(function(err) { - assert.ifError(err); - assert.equal(2, called.pre); - assert.equal(2, called.post); + doc.remove(function(err5) { + assert.ifError(err5); + assert.equal(called.pre, 2); + assert.equal(called.post, 2); db.close(done); }); }); @@ -413,8 +411,8 @@ describe('document: hooks:', function() { var Bar = db.model('gh-1335-2', BarSchema); var b = new Bar(); - b.pre('save', function(next) { - if (this.isNew && 0 === this.foos.length) { + b.$pre('save', function(next) { + if (this.isNew && this.foos.length === 0) { this.foos = undefined; } next(); @@ -430,8 +428,7 @@ describe('document: hooks:', function() { }); }); - it('post save hooks on subdocuments work (gh-915)', function(done) { - + it('post save hooks on subdocuments work (gh-915) (gh-3780)', function(done) { var doneCalled = false; var _done = function(e) { if (!doneCalled) { @@ -440,7 +437,7 @@ describe('document: hooks:', function() { } }; var db = start(); - var called = { post: 0 }; + var called = {post: 0}; var subSchema = new Schema({ name: String @@ -450,8 +447,7 @@ describe('document: hooks:', function() { called.post++; try { assert.ok(doc instanceof EmbeddedDocument); - } - catch (e) { + } catch (e) { _done(e); } }); @@ -462,22 +458,24 @@ describe('document: hooks:', function() { var M = db.model('post-save-hooks-sub', postSaveHooks); - var m = new M({ subs: [ - { name: 'mee' }, - { name: 'moo' } - ] }); + var m = new M({ + subs: [ + {name: 'mee'}, + {name: 'moo'} + ] + }); m.save(function(err) { assert.ifError(err); - assert.equal(2, called.post); + assert.equal(called.post, 2); called.post = 0; M.findById(m, function(err, doc) { assert.ifError(err); - doc.subs.push({ name: 'maa' }); + doc.subs.push({name: 'maa'}); doc.save(function(err) { assert.ifError(err); - assert.equal(4, called.post); + assert.equal(called.post, 3); _done(); }); @@ -485,8 +483,9 @@ describe('document: hooks:', function() { }); }); - it("pre save hooks should run in parallel", function(done) { - // we set the time out to be double that of the validator - 1 (so that running in serial will be greater then that) + it('pre save hooks should run in parallel', function(done) { + // we set the time out to be double that of the validator - 1 + // (so that running in serial will be greater than that) this.timeout(1000); var db = start(), count = 0; @@ -499,7 +498,7 @@ describe('document: hooks:', function() { count++; next(); if (count === 3) { - done(new Error("gaga")); + done(new Error('gaga')); } else { done(); } @@ -508,31 +507,23 @@ describe('document: hooks:', function() { var MWPSH = db.model('mwpsh', new Schema({subs: [SchemaWithPreSaveHook]})); var m = new MWPSH({ - subs: [ - { - preference: "xx" - } - , - { - preference: "yy" - } - , - { - preference: "1" - } - , - { - preference: "2" - } - ] + subs: [{ + preference: 'xx' + }, { + preference: 'yy' + }, { + preference: '1' + }, { + preference: '2' + }] }); m.save(function(err) { db.close(); try { - assert.equal(err.message, "gaga"); - assert.equal(count, 4); + assert.equal(err.message, 'gaga'); + assert.ok(count >= 3); done(); } catch (e) { done(e); @@ -541,7 +532,7 @@ describe('document: hooks:', function() { }); it('parallel followed by serial (gh-2521)', function(done) { - var schema = Schema({ name: String }); + var schema = new Schema({name: String}); schema.pre('save', true, function(next, done) { process.nextTick(function() { @@ -559,7 +550,7 @@ describe('document: hooks:', function() { var db = start(); var People = db.model('gh-2521', schema, 'gh-2521'); - var p = new People({ name: 'Val' }); + var p = new People({name: 'Val'}); p.save(function(error) { assert.ifError(error); db.close(done); @@ -567,7 +558,7 @@ describe('document: hooks:', function() { }); it('runs post hooks after function (gh-2949)', function(done) { - var schema = Schema({ name: String }); + var schema = new Schema({name: String}); var postCount = 0; schema.post('init', function(doc) { @@ -578,8 +569,8 @@ describe('document: hooks:', function() { var db = start(); var People = db.model('gh-2949', schema, 'gh-2949'); - People.create({ name: 'Val' }, function(err, doc) { - People.findOne({ _id: doc._id }, function() { + People.create({name: 'Val'}, function(err, doc) { + People.findOne({_id: doc._id}, function() { assert.equal(postCount, 1); db.close(done); }); @@ -587,10 +578,10 @@ describe('document: hooks:', function() { }); it('pre-init hooks work', function(done) { - var schema = Schema({ text: String }); + var schema = new Schema({text: String}); schema.pre('init', function(next, data) { - data.text = "pre init'd"; + data.text = 'pre init\'d'; next(); }); @@ -598,13 +589,12 @@ describe('document: hooks:', function() { Parent = db.model('Parent', schema); Parent.create({ - text: "not init'd" + text: 'not init\'d' }, function(err, doc) { - - Parent.findOne({ _id: doc._id }, function(err, doc) { + Parent.findOne({_id: doc._id}, function(err, doc) { db.close(); - assert.strictEqual(doc.text, "pre init'd"); + assert.strictEqual(doc.text, 'pre init\'d'); done(); }); @@ -612,7 +602,7 @@ describe('document: hooks:', function() { }); it('post save handles multiple args (gh-3155)', function(done) { - var schema = Schema({}); + var schema = new Schema({}); schema.post('save', function(item, next) { next(); @@ -630,7 +620,7 @@ describe('document: hooks:', function() { }); it('pre-init hooks on subdocuments work', function(done) { - var childSchema = Schema({ age: Number }); + var childSchema = new Schema({age: Number}); childSchema.pre('init', function(next, data) { ++data.age; @@ -639,16 +629,15 @@ describe('document: hooks:', function() { return this; }); - var parentSchema = Schema({ name: String, children: [childSchema] }); + var parentSchema = new Schema({name: String, children: [childSchema]}); var db = start(), Parent = db.model('ParentWithChildren', parentSchema); Parent.create({ name: 'Bob', - children: [{ age: 8 }, { age: 5 }] + children: [{age: 8}, {age: 5}] }, function(err, doc) { - - Parent.findOne({ _id: doc._id }, function(err, doc) { + Parent.findOne({_id: doc._id}, function(err, doc) { db.close(); assert.strictEqual(doc.children.length, 2); @@ -663,7 +652,7 @@ describe('document: hooks:', function() { }); it('pre-save hooks fire on subdocs before their parent doc', function(done) { - var childSchema = Schema({ name: String, count: Number }); + var childSchema = new Schema({name: String, count: Number}); childSchema.pre('save', function(next) { ++this.count; @@ -672,31 +661,33 @@ describe('document: hooks:', function() { return this; }); - var parentSchema = Schema({ + var parentSchema = new Schema({ cumulativeCount: Number, children: [childSchema] }); parentSchema.pre('save', function(next) { this.cumulativeCount = this.children.reduce(function(seed, child) { - return seed += child.count; + seed += child.count; + return seed; }, 0); next(); }); var db = start(), Parent = db.model('ParentWithChildren', parentSchema), - doc = new Parent({ children: [{ count: 0, name: 'a' }, { count: 1, name: 'b' }] }); + doc = new Parent({children: [{count: 0, name: 'a'}, {count: 1, name: 'b'}]}); - doc.save(function(err, doc) { + doc.save(function(err, doc1) { db.close(); try { - assert.strictEqual(doc.children[0].count, 1); - assert.strictEqual(doc.children[1].count, 2); - assert.strictEqual(doc.cumulativeCount, 3); + assert.strictEqual(doc1.children[0].count, 1); + assert.strictEqual(doc1.children[1].count, 2); + assert.strictEqual(doc1.cumulativeCount, 3); } catch (e) { - return done(e); + done(e); + return; } done(); @@ -705,15 +696,15 @@ describe('document: hooks:', function() { describe('gh-3284', function() { it('should call pre hooks on nested subdoc', function(done) { - var self = this; + var _this = this; var childSchema = new Schema({ title: String }); ['init', 'save', 'validate'].forEach(function(type) { - childSchema.pre(type, function(obj, next) { - self['pre' + type + 'Called'] = true; + childSchema.pre(type, function(next) { + _this['pre' + type + 'Called'] = true; next(); }); }); @@ -724,9 +715,9 @@ describe('document: hooks:', function() { } }); - mongoose.model('gh-3284', parentSchema); - var db = start(); + db.model('gh-3284', parentSchema); + var Parent = db.model('gh-3284'); var parent = new Parent({ @@ -741,9 +732,9 @@ describe('document: hooks:', function() { return Parent.findById(parent._id); }).then(function() { db.close(); - assert.ok(self.preinitCalled); - assert.ok(self.prevalidateCalled); - assert.ok(self.presaveCalled); + assert.ok(_this.preinitCalled); + assert.ok(_this.prevalidateCalled); + assert.ok(_this.presaveCalled); done(); }); }); @@ -756,7 +747,7 @@ describe('document: hooks:', function() { var preCalls = []; bookSchema.pre('set', function(next, path, val) { - preCalls.push({ path: path, val: val }); + preCalls.push({path: path, val: val}); next(); }); @@ -772,11 +763,35 @@ describe('document: hooks:', function() { done(); }); + it('sync exceptions get passed as errors (gh-5738)', function(done) { + var bookSchema = new Schema({ title: String }); + + /* eslint-disable no-unused-vars */ + bookSchema.pre('save', function(next) { + throw new Error('woops!'); + }); + + var Book = mongoose.model('gh5738', bookSchema); + + var book = new Book({ title: 'Professional AngularJS' }); + book.save(function(error) { + assert.ok(error); + assert.equal(error.message, 'woops!'); + done(); + }); + }); + it('nested subdocs only fire once (gh-3281)', function(done) { var L3Schema = new Schema({ title: String }); + var calls = 0; + L3Schema.pre('save', function(next) { + ++calls; + return next(); + }); + var L2Schema = new Schema({ items: [L3Schema] }); @@ -785,31 +800,51 @@ describe('document: hooks:', function() { items: [L2Schema] }); - var calls = 0; - L3Schema.pre('save', function(next) { - ++calls; - return next(); - }); - var db = start(); var L1 = db.model('gh3281', L1Schema); var data = { - items: [ - { - items: [ - { - title: 'test' - } - ] - } - ] + items: [{ + items: [{ + title: 'test' + }] + }] }; L1.create(data, function(error) { assert.ifError(error); assert.equal(calls, 1); - done(); + db.close(done); + }); + }); + + it('remove hooks for single nested (gh-3754)', function(done) { + var db = start(); + var postCount = 0; + var PhotoSchema = new mongoose.Schema({ + bla: String + }); + + PhotoSchema.post('remove', function() { + ++postCount; + }); + + var PersonSchema = new mongoose.Schema({ + photo: PhotoSchema + }); + + var Person = db.model('Person', PersonSchema); + + Person.create({photo: {bla: 'test'}}, function(error, person) { + assert.ifError(error); + person.photo.remove(); + person.save(function(error1) { + assert.ifError(error1); + setTimeout(function() { + assert.equal(postCount, 1); + done(); + }, 0); + }); }); }); }); diff --git a/test/document.isselected.test.js b/test/document.isselected.test.js index 2fdb4aed055..9d2846fdeb5 100644 --- a/test/document.isselected.test.js +++ b/test/document.isselected.test.js @@ -1,15 +1,15 @@ - /** * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , Schema = mongoose.Schema - , ObjectId = Schema.ObjectId - , Document = require('../lib/document') - , DocumentObjectId = mongoose.Types.ObjectId; +var start = require('./common'); +var mongoose = start.mongoose; +var assert = require('power-assert'); +var EventEmitter = require('events').EventEmitter; +var Schema = mongoose.Schema; +var ObjectId = Schema.ObjectId; +var Document = require('../lib/document'); +var DocumentObjectId = mongoose.Types.ObjectId; /** * Test Document constructor. @@ -25,35 +25,39 @@ function TestDocument() { TestDocument.prototype.__proto__ = Document.prototype; +for (var i in EventEmitter.prototype) { + TestDocument[i] = EventEmitter.prototype[i]; +} + /** * Set a dummy schema to simulate compilation. */ -var em = new Schema({ title: String, body: String }); +var em = new Schema({title: String, body: String}); em.virtual('works').get(function() { return 'em virtual works'; }); var schema = new Schema({ - test : String - , oids : [ObjectId] - , numbers : [Number] - , nested : { - age : Number - , cool : ObjectId - , deep : { x: String } - , path : String - , setr : String - } - , nested2 : { - nested: String - , yup : { - nested : Boolean - , yup : String - , age : Number - } - } - , em: [em] - , date: Date + test: String, + oids: [ObjectId], + numbers: [Number], + nested: { + age: Number, + cool: ObjectId, + deep: {x: String}, + path: String, + setr: String + }, + nested2: { + nested: String, + yup: { + nested: Boolean, + yup: String, + age: Number + } + }, + em: [em], + date: Date }); TestDocument.prototype.$__setSchema(schema); @@ -72,7 +76,7 @@ schema.path('nested.setr').set(function(v) { schema.path('date').set(function(v) { // should not have been cast to a Date yet - assert.equal('string', typeof v); + assert.equal(typeof v, 'string'); return v; }); @@ -93,16 +97,16 @@ describe('document', function() { var doc = new TestDocument(); doc.init({ - test : 'test' - , numbers : [4,5,6,7] - , nested : { - age : 5 - , cool : DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c') - , path : 'my path' - , deep : { x: 'a string' } - } - , notapath: 'i am not in the schema' - , em: [{ title: 'gocars' }] + test: 'test', + numbers: [4, 5, 6, 7], + nested: { + age: 5, + cool: DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c'), + path: 'my path', + deep: {x: 'a string'} + }, + notapath: 'i am not in the schema', + em: [{title: 'gocars'}] }); assert.ok(doc.isSelected('_id')); @@ -126,19 +130,19 @@ describe('document', function() { assert.ok(doc.isSelected('em.nonpath')); // not a path var selection = { - 'test': 1 - , 'numbers': 1 - , 'nested.deep': 1 - , 'oids': 1 + test: 1, + numbers: 1, + 'nested.deep': 1, + oids: 1 }; doc = new TestDocument(undefined, selection); doc.init({ - test : 'test' - , numbers : [4,5,6,7] - , nested : { - deep : { x: 'a string' } + test: 'test', + numbers: [4, 5, 6, 7], + nested: { + deep: {x: 'a string'} } }); @@ -169,7 +173,7 @@ describe('document', function() { doc = new TestDocument(undefined, selection); doc.init({ - em: [{ title: 'one' }] + em: [{title: 'one'}] }); assert.ok(doc.isSelected('_id')); @@ -193,20 +197,20 @@ describe('document', function() { assert.ok(!doc.isSelected('em.nonpath')); selection = { - 'em': 0 + em: 0 }; doc = new TestDocument(undefined, selection); doc.init({ - test : 'test' - , numbers : [4,5,6,7] - , nested : { - age : 5 - , cool : DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c') - , path : 'my path' - , deep : { x: 'a string' } - } - , notapath: 'i am not in the schema' + test: 'test', + numbers: [4, 5, 6, 7], + nested: { + age: 5, + cool: DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c'), + path: 'my path', + deep: {x: 'a string'} + }, + notapath: 'i am not in the schema' }); assert.ok(doc.isSelected('_id')); @@ -230,26 +234,26 @@ describe('document', function() { assert.ok(!doc.isSelected('em.nonpath')); selection = { - '_id': 0 + _id: 0 }; doc = new TestDocument(undefined, selection); doc.init({ - test : 'test' - , numbers : [4,5,6,7] - , nested : { - age : 5 - , cool : DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c') - , path : 'my path' - , deep : { x: 'a string' } - } - , notapath: 'i am not in the schema' + test: 'test', + numbers: [4, 5, 6, 7], + nested: { + age: 5, + cool: DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c'), + path: 'my path', + deep: {x: 'a string'} + }, + notapath: 'i am not in the schema' }); assert.ok(!doc.isSelected('_id')); assert.ok(doc.isSelected('nested.deep.x.no')); - doc = new TestDocument({ test: 'boom' }); + doc = new TestDocument({test: 'boom'}); assert.ok(doc.isSelected('_id')); assert.ok(doc.isSelected('test')); assert.ok(doc.isSelected('numbers')); @@ -271,16 +275,16 @@ describe('document', function() { assert.ok(doc.isSelected('em.nonpath')); selection = { - '_id': 1 + _id: 1 }; doc = new TestDocument(undefined, selection); - doc.init({ _id: 'test' }); + doc.init({_id: 'test'}); assert.ok(doc.isSelected('_id')); assert.ok(!doc.isSelected('test')); - doc = new TestDocument({ test: 'boom' }, true); + doc = new TestDocument({test: 'boom'}, true); assert.ok(doc.isSelected('_id')); assert.ok(doc.isSelected('test')); assert.ok(doc.isSelected('numbers')); @@ -302,21 +306,21 @@ describe('document', function() { assert.ok(doc.isSelected('em.nonpath')); selection = { - '_id': 1, - 'n': 1 + _id: 1, + n: 1 }; doc = new TestDocument(undefined, selection); doc.init({ - test : 'test' - , numbers : [4,5,6,7] - , nested : { - age : 5 - , cool : DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c') - , path : 'my path' - , deep : { x: 'a string' } - } - , notapath: 'i am not in the schema' + test: 'test', + numbers: [4, 5, 6, 7], + nested: { + age: 5, + cool: DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c'), + path: 'my path', + deep: {x: 'a string'} + }, + notapath: 'i am not in the schema' }); assert.ok(doc.isSelected('_id')); @@ -327,4 +331,29 @@ describe('document', function() { done(); }); + + it('isDirectSelected (gh-5063)', function(done) { + var selection = { + test: 1, + numbers: 1, + 'nested.deep': 1, + oids: 1 + }; + + var doc = new TestDocument(undefined, selection); + + doc.init({ + test: 'test', + numbers: [4, 5, 6, 7], + nested: { + deep: {x: 'a string'} + } + }); + + assert.ok(doc.isDirectSelected('nested.deep')); + assert.ok(!doc.isDirectSelected('nested.cool')); + assert.ok(!doc.isDirectSelected('nested')); + + done(); + }); }); diff --git a/test/document.modified.test.js b/test/document.modified.test.js index 669831ed026..1f1ff26e24d 100644 --- a/test/document.modified.test.js +++ b/test/document.modified.test.js @@ -1,15 +1,14 @@ - /** * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ObjectId = Schema.ObjectId - , DocumentObjectId = mongoose.Types.ObjectId; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema, + ObjectId = Schema.ObjectId, + DocumentObjectId = mongoose.Types.ObjectId; /** * Setup. @@ -18,45 +17,48 @@ var start = require('./common') var Comments = new Schema; Comments.add({ - title : String - , date : Date - , body : String - , comments : [Comments] + title: String, + date: Date, + body: String, + comments: [Comments] }); var BlogPost = new Schema({ - title : String - , author : String - , slug : String - , date : Date - , meta : { - date : Date - , visitors : Number - } - , published : Boolean - , mixed : {} - , numbers : [Number] - , owners : [ObjectId] - , comments : [Comments] - , nested : { array: [Number] } + title: String, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number + }, + published: Boolean, + mixed: {}, + numbers: [Number], + owners: [ObjectId], + comments: [Comments], + nested: {array: [Number]} }); BlogPost -.path('title') -.get(function(v) { - if (v) return v.toUpperCase(); -}); + .path('title') + .get(function(v) { + if (v) { + return v.toUpperCase(); + } + return v; + }); BlogPost -.virtual('titleWithAuthor') -.get(function() { - return this.get('title') + ' by ' + this.get('author'); -}) -.set(function(val) { - var split = val.split(' by '); - this.set('title', split[0]); - this.set('author', split[1]); -}); + .virtual('titleWithAuthor') + .get(function() { + return this.get('title') + ' by ' + this.get('author'); + }) + .set(function(val) { + var split = val.split(' by '); + this.set('title', split[0]); + this.set('author', split[1]); + }); BlogPost.method('cool', function() { return this; @@ -74,8 +76,8 @@ var collection = 'blogposts_' + random(); describe('document modified', function() { describe('modified states', function() { it('reset after save', function(done) { - var db = start() - , B = db.model(modelName, collection); + var db = start(), + B = db.model(modelName, collection); var b = new B; @@ -84,12 +86,12 @@ describe('document modified', function() { assert.strictEqual(null, err); b.numbers.push(3); - b.save(function(err) { - assert.strictEqual(null, err); + b.save(function(err1) { + assert.strictEqual(null, err1); - B.findById(b, function(err, b) { - assert.strictEqual(null, err); - assert.equal(2, b.numbers.length); + B.findById(b, function(err2, b) { + assert.strictEqual(null, err2); + assert.equal(b.numbers.length, 2); db.close(); done(); @@ -99,17 +101,17 @@ describe('document modified', function() { }); it('of embedded docs reset after save', function(done) { - var db = start() - , BlogPost = db.model(modelName, collection); + var db = start(), + BlogPost = db.model(modelName, collection); - var post = new BlogPost({ title: 'hocus pocus' }); - post.comments.push({ title: 'Humpty Dumpty', comments: [{title: 'nested'}] }); + var post = new BlogPost({title: 'hocus pocus'}); + post.comments.push({title: 'Humpty Dumpty', comments: [{title: 'nested'}]}); post.save(function(err) { db.close(); assert.strictEqual(null, err); var mFlag = post.comments[0].isModified('title'); - assert.equal(false, mFlag); - assert.equal(false, post.isModified('title')); + assert.equal(mFlag, false); + assert.equal(post.isModified('title'), false); done(); }); }); @@ -120,7 +122,7 @@ describe('document modified', function() { var db = start(); var MyModel = db.model('test', - { name: { type: String, default: 'Val '} }); + {name: {type: String, default: 'Val '}}); var m = new MyModel(); assert.ok(m.$isDefault('name')); db.close(done); @@ -141,90 +143,90 @@ describe('document modified', function() { threw = true; } - assert.equal(false, threw); + assert.equal(threw, false); done(); }); it('when modifying keys', function(done) { - var db = start() - , BlogPost = db.model(modelName, collection); + var db = start(), + BlogPost = db.model(modelName, collection); db.close(); var post = new BlogPost; post.init({ - title : 'Test' - , slug : 'test' - , date : new Date + title: 'Test', + slug: 'test', + date: new Date }); - assert.equal(false, post.isModified('title')); + assert.equal(post.isModified('title'), false); post.set('title', 'test'); - assert.equal(true, post.isModified('title')); + assert.equal(post.isModified('title'), true); - assert.equal(false, post.isModified('date')); - post.set('date', new Date(post.date + 10)); - assert.equal(true, post.isModified('date')); + assert.equal(post.isModified('date'), false); + post.set('date', new Date(post.date.getTime() + 10)); + assert.equal(post.isModified('date'), true); - assert.equal(false, post.isModified('meta.date')); + assert.equal(post.isModified('meta.date'), false); done(); }); it('setting a key identically to its current value should not dirty the key', function(done) { - var db = start() - , BlogPost = db.model(modelName, collection); + var db = start(), + BlogPost = db.model(modelName, collection); db.close(); var post = new BlogPost; post.init({ - title : 'Test' - , slug : 'test' - , date : new Date + title: 'Test', + slug: 'test', + date: new Date }); - assert.equal(false, post.isModified('title')); + assert.equal(post.isModified('title'), false); post.set('title', 'Test'); - assert.equal(false, post.isModified('title')); + assert.equal(post.isModified('title'), false); done(); }); describe('on DocumentArray', function() { it('work', function(done) { - var db = start() - , BlogPost = db.model(modelName, collection); + var db = start(), + BlogPost = db.model(modelName, collection); var post = new BlogPost(); post.init({ - title : 'Test' - , slug : 'test' - , comments : [ { title: 'Test', date: new Date, body: 'Test' } ] + title: 'Test', + slug: 'test', + comments: [{title: 'Test', date: new Date, body: 'Test'}] }); - assert.equal(false, post.isModified('comments.0.title')); + assert.equal(post.isModified('comments.0.title'), false); post.get('comments')[0].set('title', 'Woot'); - assert.equal(true, post.isModified('comments')); - assert.equal(false, post.isDirectModified('comments')); - assert.equal(true, post.isModified('comments.0.title')); - assert.equal(true, post.isDirectModified('comments.0.title')); + assert.equal(post.isModified('comments'), true); + assert.equal(post.isDirectModified('comments'), false); + assert.equal(post.isModified('comments.0.title'), true); + assert.equal(post.isDirectModified('comments.0.title'), true); db.close(done); }); it('with accessors', function(done) { - var db = start() - , BlogPost = db.model(modelName, collection); + var db = start(), + BlogPost = db.model(modelName, collection); var post = new BlogPost(); post.init({ - title : 'Test' - , slug : 'test' - , comments : [ { title: 'Test', date: new Date, body: 'Test' } ] + title: 'Test', + slug: 'test', + comments: [{title: 'Test', date: new Date, body: 'Test'}] }); - assert.equal(false, post.isModified('comments.0.body')); + assert.equal(post.isModified('comments.0.body'), false); post.get('comments')[0].body = 'Woot'; - assert.equal(true, post.isModified('comments')); - assert.equal(false, post.isDirectModified('comments')); - assert.equal(true, post.isModified('comments.0.body')); - assert.equal(true, post.isDirectModified('comments.0.body')); + assert.equal(post.isModified('comments'), true); + assert.equal(post.isDirectModified('comments'), false); + assert.equal(post.isModified('comments.0.body'), true); + assert.equal(post.isDirectModified('comments.0.body'), true); db.close(); done(); @@ -234,47 +236,47 @@ describe('document modified', function() { describe('on MongooseArray', function() { it('atomic methods', function(done) { // COMPLETEME - var db = start() - , BlogPost = db.model(modelName, collection); + var db = start(), + BlogPost = db.model(modelName, collection); db.close(); var post = new BlogPost(); - assert.equal(false, post.isModified('owners')); + assert.equal(post.isModified('owners'), false); post.get('owners').push(new DocumentObjectId); - assert.equal(true, post.isModified('owners')); + assert.equal(post.isModified('owners'), true); done(); }); it('native methods', function(done) { // COMPLETEME - var db = start() - , BlogPost = db.model(modelName, collection); + var db = start(), + BlogPost = db.model(modelName, collection); db.close(); var post = new BlogPost; - assert.equal(false, post.isModified('owners')); + assert.equal(post.isModified('owners'), false); done(); }); }); it('on entire document', function(done) { - var db = start() - , BlogPost = db.model(modelName, collection); + var db = start(), + BlogPost = db.model(modelName, collection); var doc = { - title : 'Test' - , slug : 'test' - , date : new Date - , meta : { - date : new Date - , visitors : 5 - } - , published : true - , mixed : { x: [ { y: [1,'yes', 2] } ] } - , numbers : [] - , owners : [new DocumentObjectId, new DocumentObjectId] - , comments : [ - { title: 'Test', date: new Date, body: 'Test' } - , { title: 'Super', date: new Date, body: 'Cool' } + title: 'Test', + slug: 'test', + date: new Date, + meta: { + date: new Date, + visitors: 5 + }, + published: true, + mixed: {x: [{y: [1, 'yes', 2]}]}, + numbers: [], + owners: [new DocumentObjectId, new DocumentObjectId], + comments: [ + {title: 'Test', date: new Date, body: 'Test'}, + {title: 'Super', date: new Date, body: 'Cool'} ] }; @@ -283,26 +285,26 @@ describe('document modified', function() { BlogPost.findById(post.id, function(err, postRead) { db.close(); assert.ifError(err); - //set the same data again back to the document. - //expected result, nothing should be set to modified - assert.equal(false, postRead.isModified('comments')); - assert.equal(false, postRead.isNew); + // set the same data again back to the document. + // expected result, nothing should be set to modified + assert.equal(postRead.isModified('comments'), false); + assert.equal(postRead.isNew, false); postRead.set(postRead.toObject()); - assert.equal(false, postRead.isModified('title')); - assert.equal(false, postRead.isModified('slug')); - assert.equal(false, postRead.isModified('date')); - assert.equal(false, postRead.isModified('meta.date')); - assert.equal(false, postRead.isModified('meta.visitors')); - assert.equal(false, postRead.isModified('published')); - assert.equal(false, postRead.isModified('mixed')); - assert.equal(false, postRead.isModified('numbers')); - assert.equal(false, postRead.isModified('owners')); - assert.equal(false, postRead.isModified('comments')); + assert.equal(postRead.isModified('title'), false); + assert.equal(postRead.isModified('slug'), false); + assert.equal(postRead.isModified('date'), false); + assert.equal(postRead.isModified('meta.date'), false); + assert.equal(postRead.isModified('meta.visitors'), false); + assert.equal(postRead.isModified('published'), false); + assert.equal(postRead.isModified('mixed'), false); + assert.equal(postRead.isModified('numbers'), false); + assert.equal(postRead.isModified('owners'), false); + assert.equal(postRead.isModified('comments'), false); var arr = postRead.comments.slice(); - arr[2] = postRead.comments.create({ title: 'index' }); + arr[2] = postRead.comments.create({title: 'index'}); postRead.comments = arr; - assert.equal(true, postRead.isModified('comments')); + assert.equal(postRead.isModified('comments'), true); done(); }); }); @@ -312,7 +314,7 @@ describe('document modified', function() { var db = start(); var parentSchema = new Schema({ - child: { type: Schema.Types.ObjectId, ref: 'gh-1530-2' } + child: {type: Schema.Types.ObjectId, ref: 'gh-1530-2'} }); var Parent = db.model('gh-1530-1', parentSchema); var childSchema = new Schema({ @@ -345,17 +347,17 @@ describe('document modified', function() { assert.ifError(error); assert.ok(p.child); assert.ok(typeof p.child.name === 'undefined'); - assert.equal(0, preCalls); - assert.equal(0, postCalls); - Child.findOne({ name: 'Luke' }, function(error, child) { + assert.equal(preCalls, 0); + assert.equal(postCalls, 0); + Child.findOne({name: 'Luke'}, function(error, child) { assert.ifError(error); assert.ok(!child); originalParent.child.save(function(error) { assert.ifError(error); - Child.findOne({ name: 'Luke' }, function(error, child) { + Child.findOne({name: 'Luke'}, function(error, child) { assert.ifError(error); assert.ok(child); - assert.equal(child._id.toString(), p.child.toString()); + assert.equal(p.child.toString(), child._id.toString()); db.close(done); }); }); @@ -369,53 +371,158 @@ describe('document modified', function() { var parentSchema = new Schema({ name: String, - child: { type: Schema.Types.ObjectId, ref: 'Child'} + child: {type: Schema.Types.ObjectId, ref: 'Child'} }); var Parent = db.model('Parent', parentSchema, 'parents'); var Child = db.model('Child', parentSchema, 'children'); - var child = new Child({ name: 'Mary' }); - var p = new Parent({ name: 'Alex', child: child }); + var child = new Child({name: 'Mary'}); + var p = new Parent({name: 'Alex', child: child}); - assert.equal(p.populated('child').toString(), child._id.toString()); + assert.equal(child._id.toString(), p.populated('child').toString()); db.close(done); }); + describe('manually populating arrays', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + it('gh-1530 for arrays (gh-3575)', function(done) { + var parentSchema = new Schema({ + name: String, + children: [{type: Schema.Types.ObjectId, ref: 'Child'}] + }); + + var Parent = db.model('Parent', parentSchema, 'parents'); + var Child = db.model('Child', parentSchema, 'children'); + + var child = new Child({name: 'Luke'}); + var p = new Parent({name: 'Anakin', children: [child]}); + + assert.equal('Luke', p.children[0].name); + assert.ok(p.populated('children')); + done(); + }); + + it('setting nested arrays (gh-3721)', function(done) { + var userSchema = new Schema({ + name: {type: Schema.Types.String} + }); + var User = db.model('User', userSchema); + + var accountSchema = new Schema({ + roles: [{ + name: {type: Schema.Types.String}, + users: [{type: Schema.Types.ObjectId, ref: 'User'}] + }] + }); + + var Account = db.model('Account', accountSchema); + + var user = new User({name: 'Test'}); + var account = new Account({ + roles: [ + {name: 'test group', users: [user]} + ] + }); + + assert.ok(account.roles[0].users[0].isModified); + done(); + }); + + it('with discriminators (gh-3575)', function(done) { + var shapeSchema = new mongoose.Schema({}, {discriminatorKey: 'kind'}); + + var Shape = mongoose.model('gh3575', shapeSchema); + + var Circle = Shape.discriminator('gh3575_0', new mongoose.Schema({ + radius: { type: Number } + }, {discriminatorKey: 'kind'})); + + var fooSchema = new mongoose.Schema({ + bars: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'gh3575' + }] + }); + + var Foo = mongoose.model('Foo', fooSchema); + + var test = new Foo({}); + test.bars = [new Circle({}), new Circle({})]; + + assert.ok(test.populated('bars')); + assert.ok(test.bars[0]._id); + assert.ok(test.bars[1]._id); + + done(); + }); + + it('updates embedded doc parents upon direct assignment (gh-5189)', function(done) { + var db = start(); + var familySchema = new Schema({ + children: [{name: {type: String, required: true}}] + }); + var Family = db.model('Family', familySchema); + Family.create({ + children: [ + {name: 'John'}, + {name: 'Mary'} + ] + }, function(err, family) { + family.set({children: family.children.slice(1)}); + family.children.forEach(function(child) { + child.set({name: 'Maryanne'}); + }); + + assert.equal(family.validateSync(), undefined); + done(); + }); + }); + }); + it('should support setting mixed paths by string (gh-1418)', function(done) { var db = start(); - var BlogPost = db.model('1418', new Schema({ mixed: {} })); + var BlogPost = db.model('1418', new Schema({mixed: {}})); var b = new BlogPost; - b.init({ mixed: {} }); + b.init({mixed: {}}); var path = 'mixed.path'; assert.ok(!b.isModified(path)); b.set(path, 3); assert.ok(b.isModified(path)); - assert.equal(3, b.get(path)); + assert.equal(b.get(path), 3); b = new BlogPost; - b.init({ mixed: {} }); + b.init({mixed: {}}); path = 'mixed.9a'; b.set(path, 4); assert.ok(b.isModified(path)); - assert.equal(4, b.get(path)); + assert.equal(b.get(path), 4); - b = new BlogPost({ mixed: {} }); + b = new BlogPost({mixed: {}}); b.save(function(err) { assert.ifError(err); path = 'mixed.9a.x'; b.set(path, 8); assert.ok(b.isModified(path)); - assert.equal(8, b.get(path)); + assert.equal(b.get(path), 8); b.save(function(err) { assert.ifError(err); BlogPost.findById(b, function(err, doc) { assert.ifError(err); - assert.equal(8, doc.get(path)); + assert.equal(doc.get(path), 8); db.close(done); }); }); @@ -425,36 +532,78 @@ describe('document modified', function() { it('should mark multi-level nested schemas as modified (gh-1754)', function(done) { var db = start(); - var grandChildSchema = Schema({ - name : String + var grandChildSchema = new Schema({ + name: String }); - var childSchema = Schema({ - name : String, - grandChild : [grandChildSchema] + var childSchema = new Schema({ + name: String, + grandChild: [grandChildSchema] }); - var parentSchema = Schema({ - name : String, - child : [childSchema] + var parentSchema = new Schema({ + name: String, + child: [childSchema] }); var Parent = db.model('gh-1754', parentSchema); Parent.create( - { child: [{ name: 'Brian', grandChild: [{ name: 'Jake' }] }] }, - function(error, p) { - assert.ifError(error); - assert.ok(p); - assert.equal(1, p.child.length); - assert.equal(1, p.child[0].grandChild.length); - p.child[0].grandChild[0].name = 'Jason'; - assert.ok(p.isModified('child.0.grandChild.0.name')); - p.save(function(error, inDb) { + {child: [{name: 'Brian', grandChild: [{name: 'Jake'}]}]}, + function(error, p) { assert.ifError(error); - assert.equal('Jason', inDb.child[0].grandChild[0].name); - db.close(done); + assert.ok(p); + assert.equal(p.child.length, 1); + assert.equal(p.child[0].grandChild.length, 1); + p.child[0].grandChild[0].name = 'Jason'; + assert.ok(p.isModified('child.0.grandChild.0.name')); + p.save(function(error1, inDb) { + assert.ifError(error1); + assert.equal(inDb.child[0].grandChild[0].name, 'Jason'); + db.close(done); + }); + }); + }); + + it('should reset the modified state after calling unmarkModified', function(done) { + var db = start(); + var BlogPost = db.model(modelName, collection); + + var b = new BlogPost(); + assert.equal(b.isModified('author'), false); + b.author = 'foo'; + assert.equal(b.isModified('author'), true); + assert.equal(b.isModified(), true); + b.unmarkModified('author'); + assert.equal(b.isModified('author'), false); + assert.equal(b.isModified(), false); + + b.save(function(err) { + assert.strictEqual(null, err); + + BlogPost.findById(b._id, function(err2, b2) { + assert.strictEqual(null, err2); + + assert.equal(b2.isModified('author'), false); + assert.equal(b2.isModified(), false); + b2.author = 'bar'; + assert.equal(b2.isModified('author'), true); + assert.equal(b2.isModified(), true); + b2.unmarkModified('author'); + assert.equal(b2.isModified('author'), false); + assert.equal(b2.isModified(), false); + + b2.save(function(err3) { + assert.strictEqual(err3, null); + BlogPost.findById(b._id, function(err4, b3) { + assert.strictEqual(err4, null); + // was not saved because modified state was unset + assert.equal(b3.author, 'foo'); + db.close(); + done(); + }); }); }); + }); }); }); }); diff --git a/test/document.populate.test.js b/test/document.populate.test.js index e5efcc16b69..9833e5115eb 100644 --- a/test/document.populate.test.js +++ b/test/document.populate.test.js @@ -3,14 +3,14 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , utils = require('../lib/utils') - , random = utils.random - , Schema = mongoose.Schema - , ObjectId = Schema.ObjectId - , Document = require('../lib/document'); +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + utils = require('../lib/utils'), + random = utils.random, + Schema = mongoose.Schema, + ObjectId = Schema.ObjectId, + Document = require('../lib/document'); /** * Setup. @@ -34,31 +34,31 @@ TestDocument.prototype.__proto__ = Document.prototype; * Set a dummy schema to simulate compilation. */ -var em = new Schema({ title: String, body: String }); +var em = new Schema({title: String, body: String}); em.virtual('works').get(function() { return 'em virtual works'; }); var schema = new Schema({ - test : String - , oids : [ObjectId] - , numbers : [Number] - , nested : { - age : Number - , cool : ObjectId - , deep : { x: String } - , path : String - , setr : String - } - , nested2 : { - nested: String - , yup : { - nested : Boolean - , yup : String - , age : Number - } - } - , em: [em] - , date: Date + test: String, + oids: [ObjectId], + numbers: [Number], + nested: { + age: Number, + cool: ObjectId, + deep: {x: String}, + path: String, + setr: String + }, + nested2: { + nested: String, + yup: { + nested: Boolean, + yup: String, + age: Number + } + }, + em: [em], + date: Date }); TestDocument.prototype.$__setSchema(schema); @@ -67,21 +67,21 @@ TestDocument.prototype.$__setSchema(schema); */ var User = new Schema({ - name : String - , email : String - , gender : { type: String, enum: ['male', 'female'], default: 'male' } - , age : { type: Number, default: 21 } - , blogposts : [{ type: ObjectId, ref: 'doc.populate.b' }] -}, { collection: 'doc.populate.us' }); + name: String, + email: String, + gender: {type: String, enum: ['male', 'female'], default: 'male'}, + age: {type: Number, default: 21}, + blogposts: [{type: ObjectId, ref: 'doc.populate.b'}] +}, {collection: 'doc.populate.us'}); /** * Comment subdocument schema. */ var Comment = new Schema({ - asers : [{ type: ObjectId, ref: 'doc.populate.u' }] - , _creator : { type: ObjectId, ref: 'doc.populate.u' } - , content : String + asers: [{type: ObjectId, ref: 'doc.populate.u'}], + _creator: {type: ObjectId, ref: 'doc.populate.u'}, + content: String }); /** @@ -89,10 +89,10 @@ var Comment = new Schema({ */ var BlogPost = new Schema({ - _creator : { type: ObjectId, ref: 'doc.populate.u' } - , title : String - , comments : [Comment] - , fans : [{ type: ObjectId, ref: 'doc.populate.u' }] + _creator: {type: ObjectId, ref: 'doc.populate.u'}, + title: String, + comments: [Comment], + fans: [{type: ObjectId, ref: 'doc.populate.u'}] }); mongoose.model('doc.populate.b', BlogPost); @@ -111,13 +111,13 @@ describe('document.populate', function() { _id = new mongoose.Types.ObjectId; User.create({ - name : 'Phoenix' - , email : 'phx@az.com' - , blogposts: [_id] + name: 'Phoenix', + email: 'phx@az.com', + blogposts: [_id] }, { - name : 'Newark' - , email : 'ewr@nj.com' - , blogposts: [_id] + name: 'Newark', + email: 'ewr@nj.com', + blogposts: [_id] }, function(err, u1, u2) { assert.ifError(err); @@ -125,10 +125,10 @@ describe('document.populate', function() { user2 = u2; B.create({ - title : 'the how and why' - , _creator : user1 - , fans: [user1, user2] - , comments: [{ _creator: user2, content: 'user2' }, { _creator: user1, content: 'user1' }] + title: 'the how and why', + _creator: user1, + fans: [user1, user2], + comments: [{_creator: user2, content: 'user2'}, {_creator: user1, content: 'user1'}] }, function(err, p) { assert.ifError(err); post = p; @@ -147,17 +147,17 @@ describe('document.populate', function() { B.findById(post, function(err, post) { assert.ifError(err); post.populate('_creator'); - assert.equal(1, Object.keys(post.$__.populate).length); + assert.equal(Object.keys(post.$__.populate).length, 1); assert.ok('_creator' in post.$__.populate); post.populate('_creator'); - assert.equal(1, Object.keys(post.$__.populate).length); + assert.equal(Object.keys(post.$__.populate).length, 1); assert.ok('_creator' in post.$__.populate); post.populate('_creator fans'); - assert.equal(2, Object.keys(post.$__.populate).length); + assert.equal(Object.keys(post.$__.populate).length, 2); assert.ok('_creator' in post.$__.populate); assert.ok('fans' in post.$__.populate); - post.populate({ path: '_creator' }); - assert.equal(2, Object.keys(post.$__.populate).length); + post.populate({path: '_creator'}); + assert.equal(Object.keys(post.$__.populate).length, 2); assert.ok('_creator' in post.$__.populate); assert.ok('fans' in post.$__.populate); done(); @@ -167,12 +167,12 @@ describe('document.populate', function() { B.findById(post, function(err, post) { assert.ifError(err); post.populate('_creator'); - assert.equal(1, Object.keys(post.$__.populate).length); - assert.equal(undefined, post.$__.populate._creator.select); - post.populate({ path: '_creator', select: 'name' }); - assert.equal(1, Object.keys(post.$__.populate).length); + assert.equal(Object.keys(post.$__.populate).length, 1); + assert.equal(post.$__.populate._creator.select, undefined); + post.populate({path: '_creator', select: 'name'}); + assert.equal(Object.keys(post.$__.populate).length, 1); assert.ok('_creator' in post.$__.populate); - assert.equal('name', post.$__.populate._creator.select); + assert.equal(post.$__.populate._creator.select, 'name'); done(); }); }); @@ -187,7 +187,7 @@ describe('document.populate', function() { assert.ifError(err); assert.ok(!post.$__.populate); assert.ok(post._creator); - assert.equal(String(creator_id), String(post._creator._id)); + assert.equal(String(post._creator._id), String(creator_id)); done(); }); }); @@ -209,9 +209,9 @@ describe('document.populate', function() { post.populate('_creator fans', function(err) { assert.ifError(err); assert.ok(post._creator); - assert.equal(String(creator_id), String(post._creator._id)); - assert.equal(String(creator_id), String(post.fans[0]._id)); - assert.equal(String(alt_id), String(post.fans[1]._id)); + assert.equal(String(post._creator._id), String(creator_id)); + assert.equal(String(post.fans[0]._id), String(creator_id)); + assert.equal(String(post.fans[1]._id), String(alt_id)); done(); }); }); @@ -225,8 +225,8 @@ describe('document.populate', function() { post.populate('_creator').populate(function(err) { assert.ifError(err); assert.ok(post._creator); - assert.equal(String(creator_id), String(post._creator._id)); - assert.equal(String(alt_id), String(post.fans[1])); + assert.equal(String(post._creator._id), String(creator_id)); + assert.equal(String(post.fans[1]), String(alt_id)); done(); }); }); @@ -236,17 +236,17 @@ describe('document.populate', function() { B.findById(post, function(err, post) { var param = {}; param.select = '-email'; - param.options = { sort: 'name' }; + param.options = {sort: 'name'}; param.path = '_creator fans'; // 2 paths var creator_id = post._creator; var alt_id = post.fans[1]; post.populate(param, function(err, post) { assert.ifError(err); - assert.equal(2, post.fans.length); - assert.equal(String(creator_id), String(post._creator._id)); - assert.equal(String(creator_id), String(post.fans[1]._id)); - assert.equal(String(alt_id), String(post.fans[0]._id)); + assert.equal(post.fans.length, 2); + assert.equal(String(post._creator._id), String(creator_id)); + assert.equal(String(post.fans[1]._id), String(creator_id)); + assert.equal(String(post.fans[0]._id), String(alt_id)); assert.ok(!post.fans[0].email); assert.ok(!post.fans[1].email); assert.ok(!post.fans[0].isInit('email')); @@ -263,17 +263,17 @@ describe('document.populate', function() { var param = {}; param.select = '-email'; - param.options = { sort: 'name' }; + param.options = {sort: 'name'}; param.path = '_creator'; post.populate(param); param.path = 'fans'; post.populate(param, function(err, post) { assert.ifError(err); - assert.equal(2, post.fans.length); - assert.equal(String(creator_id), String(post._creator._id)); - assert.equal(String(creator_id), String(post.fans[1]._id)); - assert.equal(String(alt_id), String(post.fans[0]._id)); + assert.equal(post.fans.length, 2); + assert.equal(String(post._creator._id), String(creator_id)); + assert.equal(String(post.fans[1]._id), String(creator_id)); + assert.equal(String(post.fans[0]._id), String(alt_id)); assert.ok(!post.fans[0].email); assert.ok(!post.fans[1].email); assert.ok(!post.fans[0].isInit('email')); @@ -287,7 +287,7 @@ describe('document.populate', function() { B.findById(post, function(err, post) { var param = {}; param.select = '-email'; - param.options = { sort: 'name' }; + param.options = {sort: 'name'}; param.path = '_creator fans'; param.model = 'doc.populate.u2'; @@ -295,10 +295,10 @@ describe('document.populate', function() { var alt_id = post.fans[1]; post.populate(param, function(err, post) { assert.ifError(err); - assert.equal(2, post.fans.length); - assert.equal(String(creator_id), String(post._creator._id)); - assert.equal(String(creator_id), String(post.fans[1]._id)); - assert.equal(String(alt_id), String(post.fans[0]._id)); + assert.equal(post.fans.length, 2); + assert.equal(String(post._creator._id), String(creator_id)); + assert.equal(String(post.fans[1]._id), String(creator_id)); + assert.equal(String(post.fans[0]._id), String(alt_id)); assert.ok(!post.fans[0].email); assert.ok(!post.fans[1].email); assert.ok(!post.fans[0].isInit('email')); @@ -318,10 +318,10 @@ describe('document.populate', function() { post.setValue('idontexist', user1._id); // populate the non-schema value by passing an explicit model - post.populate({ path: 'idontexist', model: 'doc.populate.u' }, function(err, post) { + post.populate({path: 'idontexist', model: 'doc.populate.u'}, function(err, post) { assert.ifError(err); assert.ok(post); - assert.equal(post.get('idontexist')._id, user1._id.toString()); + assert.equal(user1._id.toString(), post.get('idontexist')._id); assert.equal(post.get('idontexist').name, 'Phoenix'); done(); }); @@ -363,29 +363,29 @@ describe('document.populate', function() { var db = start(); var UserSchema = new Schema({ - _id: String - , name: String + _id: String, + name: String }); var NoteSchema = new Schema({ - author: { type: String, ref: 'UserWithStringId' } - , body: String + author: {type: String, ref: 'UserWithStringId'}, + body: String }); var User = db.model('UserWithStringId', UserSchema, random()); var Note = db.model('NoteWithStringId', NoteSchema, random()); - var alice = new User({_id: 'alice', name: "Alice In Wonderland"}); + var alice = new User({_id: 'alice', name: 'Alice In Wonderland'}); alice.save(function(err) { assert.ifError(err); - var note = new Note({ author: 'alice', body: "Buy Milk" }); + var note = new Note({author: 'alice', body: 'Buy Milk'}); note.populate('author', function(err) { db.close(); assert.ifError(err); assert.ok(note.author); - assert.equal('alice', note.author._id); + assert.equal(note.author._id, 'alice'); assert.equal(note.author.name, 'Alice In Wonderland'); done(); }); @@ -396,36 +396,36 @@ describe('document.populate', function() { var db = start(); var UserSchema = new Schema({ - _id: Buffer - , name: String + _id: Buffer, + name: String }); var NoteSchema = new Schema({ - author: { type: Buffer, ref: 'UserWithBufferId' } - , body: String + author: {type: Buffer, ref: 'UserWithBufferId'}, + body: String }); var User = db.model('UserWithBufferId', UserSchema, random()); var Note = db.model('NoteWithBufferId', NoteSchema, random()); - var alice = new User({_id: new mongoose.Types.Buffer('YWxpY2U=', 'base64'), name: "Alice"}); + var alice = new User({_id: new mongoose.Types.Buffer('YWxpY2U=', 'base64'), name: 'Alice'}); alice.save(function(err) { assert.ifError(err); - var note = new Note({author: 'alice', body: "Buy Milk"}); + var note = new Note({author: 'alice', body: 'Buy Milk'}); note.save(function(err) { assert.ifError(err); Note.findById(note.id, function(err, note) { assert.ifError(err); - assert.equal('alice', note.author); + assert.equal(note.author, 'alice'); note.populate('author', function(err, note) { db.close(); assert.ifError(err); - assert.equal(note.body,'Buy Milk'); + assert.equal(note.body, 'Buy Milk'); assert.ok(note.author); - assert.equal(note.author.name,'Alice'); + assert.equal(note.author.name, 'Alice'); done(); }); }); @@ -437,30 +437,30 @@ describe('document.populate', function() { var db = start(); var UserSchema = new Schema({ - _id: Number - , name: String + _id: Number, + name: String }); var NoteSchema = new Schema({ - author: { type: Number, ref: 'UserWithNumberId' } - , body: String + author: {type: Number, ref: 'UserWithNumberId'}, + body: String }); var User = db.model('UserWithNumberId', UserSchema, random()); var Note = db.model('NoteWithNumberId', NoteSchema, random()); - var alice = new User({_id: 2359, name: "Alice"}); + var alice = new User({_id: 2359, name: 'Alice'}); alice.save(function(err) { assert.ifError(err); - var note = new Note({author: 2359, body: "Buy Milk"}); + var note = new Note({author: 2359, body: 'Buy Milk'}); note.populate('author').populate(function(err, note) { db.close(); assert.ifError(err); assert.ok(note.author); - assert.equal(2359, note.author._id); - assert.equal(note.author.name,'Alice'); + assert.equal(note.author._id, 2359); + assert.equal('Alice', note.author.name); done(); }); }); @@ -473,9 +473,9 @@ describe('document.populate', function() { var id1 = post.comments[1]._creator; post.populate('comments._creator', function(err, post) { assert.ifError(err); - assert.equal(2, post.comments.length); - assert.equal(id0, post.comments[0]._creator.id); - assert.equal(id1, post.comments[1]._creator.id); + assert.equal(post.comments.length, 2); + assert.equal(post.comments[0]._creator.id, id0); + assert.equal(post.comments[1]._creator.id, id1); done(); }); }); @@ -484,13 +484,13 @@ describe('document.populate', function() { describe('of new document', function() { it('should save just the populated _id (gh-1442)', function(done) { - var b = new B({ _creator: user1 }); + var b = new B({_creator: user1}); b.populate('_creator', function(err, b) { if (err) return done(err); - assert.equal('Phoenix', b._creator.name); + assert.equal(b._creator.name, 'Phoenix'); b.save(function(err) { assert.ifError(err); - B.collection.findOne({ _id: b._id }, function(err, b) { + B.collection.findOne({_id: b._id}, function(err, b) { assert.ifError(err); assert.equal(b._creator, String(user1._id)); done(); @@ -508,21 +508,21 @@ describe('document.populate', function() { }); var Band = db.model('gh3308_0', { - guitarist: { type: Schema.Types.ObjectId, ref: 'gh3308' } + guitarist: {type: Schema.Types.ObjectId, ref: 'gh3308'} }); - var slash = new Person({ name: 'Slash' }); - var gnr = new Band({ guitarist: slash._id }); + var slash = new Person({name: 'Slash'}); + var gnr = new Band({guitarist: slash._id}); gnr.guitarist = slash; assert.equal(gnr.guitarist.name, 'Slash'); assert.ok(gnr.populated('guitarist')); - var buckethead = new Person({ name: 'Buckethead' }); + var buckethead = new Person({name: 'Buckethead'}); gnr.guitarist = buckethead._id; assert.ok(!gnr.populated('guitarist')); - done(); + db.close(done); }); describe('gh-2214', function() { @@ -537,35 +537,40 @@ describe('document.populate', function() { var Person = db.model('gh-2214-2', { name: String, cars: [ - { - type: Schema.Types.ObjectId, - ref: 'gh-2214-1' - } + { + type: Schema.Types.ObjectId, + ref: 'gh-2214-1' + } ] }); - var car, joe; + var car; + var joe; joe = new Person({ - name: "Joe" + name: 'Joe' }); car = new Car({ - model: "BMW", - color: "red" + model: 'BMW', + color: 'red' }); joe.cars.push(car); - return joe.save(function() { - return car.save(function() { - return Person.findById(joe.id, function(err, joe) { - return joe.populate("cars", function() { + joe.save(function(error) { + assert.ifError(error); + car.save(function(error) { + assert.ifError(error); + Person.findById(joe.id, function(error, joe) { + assert.ifError(error); + joe.populate('cars', function(error) { + assert.ifError(error); car = new Car({ - model: "BMW", - color: "black" + model: 'BMW', + color: 'black' }); joe.cars.push(car); assert.ok(joe.isModified('cars')); - db.close(); done(); + db.close(); }); }); }); @@ -582,15 +587,15 @@ describe('document.populate', function() { var Band = db.model('gh2509_2', { name: String, - members: [{ type: Schema.Types.ObjectId, ref: 'gh2509_1' }], - lead: { type: Schema.Types.ObjectId, ref: 'gh2509_1' } + members: [{type: Schema.Types.ObjectId, ref: 'gh2509_1'}], + lead: {type: Schema.Types.ObjectId, ref: 'gh2509_1'} }); - var people = [{ name: 'Axl Rose' }, { name: 'Slash' }]; + var people = [{name: 'Axl Rose'}, {name: 'Slash'}]; Person.create(people, function(error, docs) { assert.ifError(error); var band = { - name: "Guns N' Roses", + name: 'Guns N\' Roses', members: [docs[0]._id, docs[1]], lead: docs[0]._id }; @@ -608,10 +613,63 @@ describe('document.populate', function() { band.depopulate('lead'); assert.ok(!band.lead.name); assert.equal(band.lead.toString(), docs[0]._id.toString()); - done(); + db.close(done); }); }); }); }); }); + + it('does not allow you to call populate() on nested docs (gh-4552)', function(done) { + var EmbeddedSchema = new Schema({ + reference: { + type: mongoose.Schema.ObjectId, + ref: 'Reference' + } + }); + + var ModelSchema = new Schema({ + embedded: EmbeddedSchema + }); + + var Model = db.model('gh4552', ModelSchema); + + var m = new Model({}); + m.embedded = {}; + assert.throws(function() { + m.embedded.populate('reference'); + }, /on nested docs/); + done(); + }); + + it('handles pulling from populated array (gh-3579)', function(done) { + var db = start(); + var barSchema = new Schema({name: String}); + + var Bar = db.model('gh3579', barSchema); + + var fooSchema = new Schema({ + bars: [{ + type: Schema.Types.ObjectId, + ref: 'gh3579' + }] + }); + + var Foo = db.model('gh3579_0', fooSchema); + + Bar.create([{name: 'bar1'}, {name: 'bar2'}], function(error, docs) { + assert.ifError(error); + var foo = new Foo({bars: [docs[0], docs[1]]}); + foo.bars.pull(docs[0]._id); + foo.save(function(error) { + assert.ifError(error); + Foo.findById(foo._id, function(error, foo) { + assert.ifError(error); + assert.equal(foo.bars.length, 1); + assert.equal(foo.bars[0].toString(), docs[1]._id.toString()); + db.close(done); + }); + }); + }); + }); }); diff --git a/test/document.strict.test.js b/test/document.strict.test.js index f76648de480..8470d59207b 100644 --- a/test/document.strict.test.js +++ b/test/document.strict.test.js @@ -1,13 +1,12 @@ - /** * Test dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , random = require('../lib/utils').random - , Schema = mongoose.Schema; +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + random = require('../lib/utils').random, + Schema = mongoose.Schema; describe('document: strict mode:', function() { describe('should work', function() { @@ -17,14 +16,14 @@ describe('document: strict mode:', function() { db = start(); var raw = { - ts : { type: Date, default: Date.now } - , content: String - , mixed: {} - , deepMixed: { '4a': {}} - , arrayMixed: [] + ts: {type: Date, default: Date.now}, + content: String, + mixed: {}, + deepMixed: {'4a': {}}, + arrayMixed: [] }; - var lax = new Schema(raw, { strict: false }); + var lax = new Schema(raw, {strict: false, minimize: false}); var strict = new Schema(raw); Lax = db.model('Lax', lax); @@ -35,29 +34,42 @@ describe('document: strict mode:', function() { db.close(done); }); - it('when creating models with non-strict schemas', function(done) { - var l = new Lax({content: 'sample', rouge: 'data'}); - assert.equal(false, l.$__.strictMode); + it('when creating models with non-strict schemas (gh-4274)', function(done) { + var l = new Lax({ content: 'sample', rouge: 'data', items: {} }); + assert.equal(l.$__.strictMode, false); var lo = l.toObject(); assert.ok('ts' in l); assert.ok('ts' in lo); - assert.equal('sample', l.content); - assert.equal('sample', lo.content); - assert.equal('data', l.rouge); - assert.equal('data', lo.rouge); - done(); + assert.equal(l.content, 'sample'); + assert.equal(lo.content, 'sample'); + assert.equal(l.rouge, 'data'); + assert.equal(lo.rouge, 'data'); + assert.deepEqual(l.items, {}); + assert.deepEqual(lo.items, {}); + + l.save(function(error) { + assert.ifError(error); + Lax.findById(l).exec(function(error, doc) { + assert.ifError(error); + var lo = doc.toObject(); + assert.equal(lo.content, 'sample'); + assert.equal(lo.rouge, 'data'); + assert.deepEqual(lo.items, {}); + done(); + }); + }); }); it('when creating models with strict schemas', function(done) { var s = new Strict({content: 'sample', rouge: 'data'}); - assert.equal(true, s.$__.strictMode); + assert.equal(s.$__.strictMode, true); var so = s.toObject(); assert.ok('ts' in s); assert.ok('ts' in so); - assert.equal('sample', s.content); - assert.equal('sample', so.content); + assert.equal(s.content, 'sample'); + assert.equal(so.content, 'sample'); assert.ok(!('rouge' in s)); assert.ok(!('rouge' in so)); assert.ok(!s.rouge); @@ -71,23 +83,23 @@ describe('document: strict mode:', function() { assert.ok(instance.$__.strictMode); instance = instance.toObject(); - assert.equal('sample', instance.content); + assert.equal(instance.content, 'sample'); assert.ok(!instance.rouge); assert.ok('ts' in instance); // hydrate works as normal, but supports the schema level flag. var s2 = new Strict({content: 'sample', rouge: 'data'}, false); - assert.equal(false, s2.$__.strictMode); + assert.equal(s2.$__.strictMode, false); s2 = s2.toObject(); assert.ok('ts' in s2); - assert.equal('sample', s2.content); + assert.equal(s2.content, 'sample'); assert.ok('rouge' in s2); // testing init var s3 = new Strict(); s3.init({content: 'sample', rouge: 'data'}); s3.toObject(); - assert.equal('sample', s3.content); + assert.equal(s3.content, 'sample'); assert.ok(!('rouge' in s3)); assert.ok(!s3.rouge); done(); @@ -96,7 +108,7 @@ describe('document: strict mode:', function() { it('when using Model#create', function(done) { // strict on create Strict.create({content: 'sample2', rouge: 'data'}, function(err, doc) { - assert.equal('sample2', doc.content); + assert.equal(doc.content, 'sample2'); assert.ok(!('rouge' in doc)); assert.ok(!doc.rouge); done(); @@ -112,34 +124,34 @@ describe('document: strict mode:', function() { var db = start(); var lax = new Schema({ - name: { last: String } - }, { strict: false }); + name: {last: String} + }, {strict: false}); var strict = new Schema({ - name: { last: String } + name: {last: String} }); var Lax = db.model('NestedLax', lax, 'nestdoc' + random()); var Strict = db.model('NestedStrict', strict, 'nestdoc' + random()); var l = new Lax; - l.set('name', { last: 'goose', hack: 'xx' }); + l.set('name', {last: 'goose', hack: 'xx'}); l = l.toObject(); - assert.equal('goose', l.name.last); - assert.equal('xx', l.name.hack); + assert.equal(l.name.last, 'goose'); + assert.equal(l.name.hack, 'xx'); var s = new Strict; - s.set({ name: { last: 'goose', hack: 'xx' }}); + s.set({name: {last: 'goose', hack: 'xx'}}); s = s.toObject(); - assert.equal('goose', s.name.last); + assert.equal(s.name.last, 'goose'); assert.ok(!('hack' in s.name)); assert.ok(!s.name.hack); s = new Strict; - s.set('name', { last: 'goose', hack: 'xx' }); + s.set('name', {last: 'goose', hack: 'xx'}); s.set('shouldnt.exist', ':('); s = s.toObject(); - assert.equal('goose', s.name.last); + assert.equal(s.name.last, 'goose'); assert.ok(!('hack' in s.name)); assert.ok(!s.name.hack); assert.ok(!s.shouldnt); @@ -150,30 +162,30 @@ describe('document: strict mode:', function() { var db = start(); var lax = new Schema({ - ts : { type: Date, default: Date.now } - , content: String - }, { strict: false }); + ts: {type: Date, default: Date.now}, + content: String + }, {strict: false}); var strict = new Schema({ - ts : { type: Date, default: Date.now } - , content: String + ts: {type: Date, default: Date.now}, + content: String }); - var Lax = db.model('EmbeddedLax', new Schema({ dox: [lax] }, { strict: false }), 'embdoc' + random()); - var Strict = db.model('EmbeddedStrict', new Schema({ dox: [strict] }, { strict: false }), 'embdoc' + random()); + var Lax = db.model('EmbeddedLax', new Schema({dox: [lax]}, {strict: false}), 'embdoc' + random()); + var Strict = db.model('EmbeddedStrict', new Schema({dox: [strict]}, {strict: false}), 'embdoc' + random()); - var l = new Lax({ dox: [{content: 'sample', rouge: 'data'}] }); - assert.equal(false, l.dox[0].$__.strictMode); + var l = new Lax({dox: [{content: 'sample', rouge: 'data'}]}); + assert.equal(l.dox[0].$__.strictMode, false); l = l.dox[0].toObject(); - assert.equal('sample', l.content); - assert.equal('data', l.rouge); + assert.equal(l.content, 'sample'); + assert.equal(l.rouge, 'data'); assert.ok(l.rouge); - var s = new Strict({ dox: [{content: 'sample', rouge: 'data'}] }); - assert.equal(true, s.dox[0].$__.strictMode); + var s = new Strict({dox: [{content: 'sample', rouge: 'data'}]}); + assert.equal(s.dox[0].$__.strictMode, true); s = s.dox[0].toObject(); assert.ok('ts' in s); - assert.equal('sample', s.content); + assert.equal(s.content, 'sample'); assert.ok(!('rouge' in s)); assert.ok(!s.rouge); @@ -181,13 +193,13 @@ describe('document: strict mode:', function() { var s3 = new Strict(); s3.init({dox: [{content: 'sample', rouge: 'data'}]}); s3.toObject(); - assert.equal('sample', s3.dox[0].content); + assert.equal(s3.dox[0].content, 'sample'); assert.ok(!('rouge' in s3.dox[0])); assert.ok(!s3.dox[0].rouge); // strict on create - Strict.create({dox:[{content: 'sample2', rouge: 'data'}]}, function(err, doc) { - assert.equal('sample2', doc.dox[0].content); + Strict.create({dox: [{content: 'sample2', rouge: 'data'}]}, function(err, doc) { + assert.equal(doc.dox[0].content, 'sample2'); assert.ok(!('rouge' in doc.dox[0])); assert.ok(!doc.dox[0].rouge); db.close(done); @@ -197,12 +209,12 @@ describe('document: strict mode:', function() { it('virtuals', function(done) { var db = start(); - var getCount = 0 - , setCount = 0; + var getCount = 0, + setCount = 0; var strictSchema = new Schema({ - email: String - , prop: String + email: String, + prop: String }); strictSchema @@ -219,21 +231,21 @@ describe('document: strict mode:', function() { var StrictModel = db.model('StrictVirtual', strictSchema); var strictInstance = new StrictModel({ - email: 'hunter@skookum.com' - , myvirtual: 'test' + email: 'hunter@skookum.com', + myvirtual: 'test' }); - assert.equal(0, getCount); - assert.equal(1, setCount); + assert.equal(getCount, 0); + assert.equal(setCount, 1); strictInstance.myvirtual = 'anotherone'; - assert.equal(0, getCount); - assert.equal(2, setCount); + assert.equal(getCount, 0); + assert.equal(setCount, 2); var temp = strictInstance.myvirtual; - assert.equal(typeof temp, 'string'); - assert.equal(1, getCount); - assert.equal(2, setCount); + assert.equal('string', typeof temp); + assert.equal(getCount, 1); + assert.equal(setCount, 2); db.close(done); }); @@ -246,25 +258,25 @@ describe('document: strict mode:', function() { }); var Strict = db.model('Strict', strict); - var s = new Strict({ bool: true }); + var s = new Strict({bool: true}); // insert non-schema property var doc = s.toObject(); doc.notInSchema = true; - Strict.collection.insert(doc, { w: 1 }, function(err) { + Strict.collection.insert(doc, {w: 1}, function(err) { assert.ifError(err); Strict.findById(doc._id, function(err, doc) { assert.ifError(err); - assert.equal(true, doc._doc.bool); - assert.equal(true, doc._doc.notInSchema); + assert.equal(doc._doc.bool, true); + assert.equal(doc._doc.notInSchema, true); doc.bool = undefined; - doc.set('notInSchema', undefined, { strict: false }); + doc.set('notInSchema', undefined, {strict: false}); doc.save(function() { Strict.findById(doc._id, function(err, doc) { assert.ifError(err); - assert.equal(undefined, doc._doc.bool); - assert.equal(undefined, doc._doc.notInSchema); + assert.equal(doc._doc.bool, undefined); + assert.equal(doc._doc.notInSchema, undefined); db.close(done); }); }); @@ -280,36 +292,32 @@ describe('document: strict mode:', function() { }); var Strict = db.model('Strict', strict); - var s = new Strict({ bool: true }); + var s = new Strict({bool: true}); // insert non-schema property var doc = s.toObject(); doc.notInSchema = true; - Strict.collection.insert(doc, { w: 1 }, function(err) { + Strict.collection.insert(doc, function(err) { assert.ifError(err); Strict.findById(doc._id, function(err, doc) { assert.ifError(err); - assert.equal(true, doc._doc.bool); - assert.equal(true, doc._doc.notInSchema); - - Strict.update( - { _id: doc._id } - , { $unset: { bool: 1, notInSchema: 1 }} - , { strict: false, w: 1 } - , function(err) { + assert.equal(doc._doc.bool, true); + assert.equal(doc._doc.notInSchema, true); - assert.ifError(err); - - Strict.findById(doc._id, function(err, doc) { - db.close(); + Strict.update({_id: doc._id}, {$unset: {bool: 1, notInSchema: 1}}, {strict: false}, + function(err) { assert.ifError(err); - assert.equal(undefined, doc._doc.bool); - assert.equal(undefined, doc._doc.notInSchema); - done(); + + Strict.findById(doc._id, function(err, doc) { + db.close(); + assert.ifError(err); + assert.equal(doc._doc.bool, undefined); + assert.equal(doc._doc.notInSchema, undefined); + done(); + }); }); - }); }); }); }); @@ -322,42 +330,38 @@ describe('document: strict mode:', function() { }); var Strict = db.model('Strict', strict); - var s = new Strict({ bool: true }); + var s = new Strict({bool: true}); // insert non-schema property var doc = s.toObject(); doc.notInSchema = true; - Strict.collection.insert(doc, { w: 1 }, function(err) { + Strict.collection.insert(doc, {w: 1}, function(err) { assert.ifError(err); Strict.findById(doc._id, function(err, doc) { assert.ifError(err); - assert.equal(true, doc._doc.bool); - assert.equal(true, doc._doc.notInSchema); - - Strict.findOneAndUpdate( - { _id: doc._id } - , { $unset: { bool: 1, notInSchema: 1 }} - , { strict: false, w: 1 } - , function(err) { + assert.equal(doc._doc.bool, true); + assert.equal(doc._doc.notInSchema, true); - assert.ifError(err); - - Strict.findById(doc._id, function(err, doc) { + Strict.findOneAndUpdate({_id: doc._id}, {$unset: {bool: 1, notInSchema: 1}}, {strict: false, w: 1}, + function(err) { assert.ifError(err); - assert.equal(undefined, doc._doc.bool); - assert.equal(undefined, doc._doc.notInSchema); - db.close(done); + + Strict.findById(doc._id, function(err, doc) { + assert.ifError(err); + assert.equal(doc._doc.bool, undefined); + assert.equal(doc._doc.notInSchema, undefined); + db.close(done); + }); }); - }); }); }); }); describe('"throws" mode', function() { it('throws on set() of unknown property', function(done) { - var schema = Schema({ n: String, docs:[{x:[{y:String}]}] }); + var schema = new Schema({n: String, docs: [{x: [{y: String}]}]}); schema.set('strict', 'throw'); var M = mongoose.model('throwStrictSet', schema, 'tss_' + random()); var m = new M; @@ -406,8 +410,8 @@ describe('document: strict mode:', function() { it('fails with extra fields', function(done) { // Simple schema with throws option var FooSchema = new mongoose.Schema({ - name: { type: String } - }, {strict: "throw"}); + name: {type: String} + }, {strict: 'throw'}); // Create the model var Foo = mongoose.model('Foo1234', FooSchema); @@ -427,18 +431,33 @@ describe('document: strict mode:', function() { it('doesnt throw with refs (gh-2665)', function(done) { // Simple schema with throws option var FooSchema = new mongoose.Schema({ - name: { type: mongoose.Schema.Types.ObjectId, ref: 'test', required: false, default: null }, - father: { name: { full: String } } - }, {strict: "throw"}); + name: {type: mongoose.Schema.Types.ObjectId, ref: 'test', required: false, default: null}, + father: {name: {full: String}} + }, {strict: 'throw'}); // Create the model - var Foo = mongoose.model('Foo', FooSchema); + var Foo = mongoose.model('gh2665', FooSchema); assert.doesNotThrow(function() { - new Foo({name: mongoose.Types.ObjectId(), father: { name: { full: 'bacon' } } }); + new Foo({name: mongoose.Types.ObjectId(), father: {name: {full: 'bacon'}}}); }); done(); }); + + it('set nested to num throws ObjectExpectedError (gh-3735)', function(done) { + var schema = new Schema({ + resolved: { + by: {type: String} + } + }, {strict: 'throw'}); + + var Test = mongoose.model('gh3735', schema); + + assert.throws(function() { + new Test({resolved: 123}); + }, /ObjectExpectedError/); + done(); + }); }); }); diff --git a/test/document.test.js b/test/document.test.js index ec57dfcb77a..db3edf7fc43 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -1,22 +1,25 @@ - /** * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ObjectId = Schema.ObjectId - , Document = require('../lib/document') - , DocumentObjectId = mongoose.Types.ObjectId - , SchemaType = mongoose.SchemaType - , ValidatorError = SchemaType.ValidatorError - , ValidationError = mongoose.Document.ValidationError - , MongooseError = mongoose.Error - , EmbeddedDocument = require('../lib/types/embedded') - , Query = require('../lib/query'); +var start = require('./common'); +var mongoose = start.mongoose; +var assert = require('power-assert'); +var random = require('../lib/utils').random; +var Schema = mongoose.Schema; +var ObjectId = Schema.ObjectId; +var Document = require('../lib/document'); +var DocumentObjectId = mongoose.Types.ObjectId; +var EventEmitter = require('events').EventEmitter; +var SchemaType = mongoose.SchemaType; +var ValidatorError = SchemaType.ValidatorError; +var ValidationError = mongoose.Document.ValidationError; +var MongooseError = mongoose.Error; +var EmbeddedDocument = require('../lib/types/embedded'); +var Query = require('../lib/query'); +var validator = require('validator'); + +var _ = require('lodash'); /** * Test Document constructor. @@ -32,36 +35,41 @@ function TestDocument() { TestDocument.prototype.__proto__ = Document.prototype; +for (var i in EventEmitter.prototype) { + TestDocument[i] = EventEmitter.prototype[i]; +} + /** * Set a dummy schema to simulate compilation. */ -var em = new Schema({ title: String, body: String }); +var em = new Schema({title: String, body: String}); em.virtual('works').get(function() { return 'em virtual works'; }); var schema = new Schema({ - test : String - , oids : [ObjectId] - , numbers : [Number] - , nested : { - age : Number - , cool : ObjectId - , deep : { x: String } - , path : String - , setr : String - } - , nested2 : { - nested: String - , yup : { - nested : Boolean - , yup : String - , age : Number - } - } - , em: [em] - , date: Date + test: String, + oids: [ObjectId], + numbers: [Number], + nested: { + age: Number, + cool: ObjectId, + deep: {x: String}, + path: String, + setr: String + }, + nested2: { + nested: String, + yup: { + nested: Boolean, + yup: String, + age: Number + } + }, + em: [em], + date: Date }); + TestDocument.prototype.$__setSchema(schema); schema.virtual('nested.agePlus2').get(function() { @@ -81,7 +89,7 @@ var dateSetterCalled = false; schema.path('date').set(function(v) { // should not have been cast to a Date yet if (v !== undefined) { - assert.equal('string', typeof v); + assert.equal(typeof v, 'string'); } dateSetterCalled = true; return v; @@ -96,7 +104,7 @@ TestDocument.prototype.hooksTest = function(fn) { fn(null, arguments); }; -var childSchema = new Schema({ counter: Number }); +var childSchema = new Schema({counter: Number}); var parentSchema = new Schema({ name: String, @@ -108,11 +116,10 @@ var parentSchema = new Schema({ */ describe('document', function() { - describe('shortcut getters', function() { it('return undefined for properties with a null/undefined parent object (gh-1326)', function(done) { var doc = new TestDocument; - doc.init({ nested: null }); + doc.init({nested: null}); assert.strictEqual(undefined, doc.nested.age); done(); }); @@ -120,38 +127,38 @@ describe('document', function() { it('work', function(done) { var doc = new TestDocument(); doc.init({ - test : 'test' - , oids : [] - , nested : { - age : 5 - , cool : DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c') - , path : 'my path' + test: 'test', + oids: [], + nested: { + age: 5, + cool: DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c'), + path: 'my path' } }); - assert.equal('test', doc.test); + assert.equal(doc.test, 'test'); assert.ok(doc.oids instanceof Array); assert.equal(doc.nested.age, 5); assert.equal(String(doc.nested.cool), '4c6c2d6240ced95d0e00003c'); - assert.equal(7, doc.nested.agePlus2); - assert.equal('5my path', doc.nested.path); + assert.equal(doc.nested.agePlus2, 7); + assert.equal(doc.nested.path, '5my path'); doc.nested.setAge = 10; - assert.equal(10, doc.nested.age); + assert.equal(doc.nested.age, 10); doc.nested.setr = 'set it'; assert.equal(doc.getValue('nested.setr'), 'set it setter'); var doc2 = new TestDocument(); doc2.init({ - test : 'toop' - , oids : [] - , nested : { - age : 2 - , cool : DocumentObjectId.createFromHexString('4cf70857337498f95900001c') - , deep : { x: 'yay' } + test: 'toop', + oids: [], + nested: { + age: 2, + cool: DocumentObjectId.createFromHexString('4cf70857337498f95900001c'), + deep: {x: 'yay'} } }); - assert.equal('toop', doc2.test); + assert.equal(doc2.test, 'toop'); assert.ok(doc2.oids instanceof Array); assert.equal(doc2.nested.age, 2); @@ -169,23 +176,23 @@ describe('document', function() { assert.equal(doc2.nested2.yup.nested2, undefined); assert.equal(doc2.nested2.yup.yup, undefined); assert.equal(doc2.nested2.yup.age, undefined); - assert.equal('object', typeof doc2.nested2.yup); + assert.equal(typeof doc2.nested2.yup, 'object'); doc2.nested2.yup = { - age: 150 - , yup: "Yesiree" - , nested: true + age: 150, + yup: 'Yesiree', + nested: true }; assert.equal(doc2.nested2.nested, undefined); assert.equal(doc2.nested2.yup.nested, true); - assert.equal(doc2.nested2.yup.yup, "Yesiree"); + assert.equal(doc2.nested2.yup.yup, 'Yesiree'); assert.equal(doc2.nested2.yup.age, 150); - doc2.nested2.nested = "y"; - assert.equal(doc2.nested2.nested, "y"); + doc2.nested2.nested = 'y'; + assert.equal(doc2.nested2.nested, 'y'); assert.equal(doc2.nested2.yup.nested, true); - assert.equal(doc2.nested2.yup.yup, "Yesiree"); - assert.equal(150, doc2.nested2.yup.age); + assert.equal(doc2.nested2.yup.yup, 'Yesiree'); + assert.equal(doc2.nested2.yup.age, 150); assert.equal(String(doc2.nested.cool), '4cf70857337498f95900001c'); @@ -198,26 +205,26 @@ describe('document', function() { var doc = new TestDocument(); doc.init({ - test : 'Test' - , nested : { - age : 5 + test: 'Test', + nested: { + age: 5 } }); assert.equal(doc.isModified('test'), false); doc.test = 'Woot'; - assert.equal('Woot', doc.test); - assert.equal(true, doc.isModified('test')); + assert.equal(doc.test, 'Woot'); + assert.equal(doc.isModified('test'), true); - assert.equal(doc.isModified('nested.age'),false); + assert.equal(doc.isModified('nested.age'), false); doc.nested.age = 2; - assert.equal(2,doc.nested.age); + assert.equal(doc.nested.age, 2); assert.ok(doc.isModified('nested.age')); - doc.nested = { path: 'overwrite the entire nested object' }; - assert.equal(undefined, doc.nested.age); - assert.equal(1, Object.keys(doc._doc.nested).length); - assert.equal('overwrite the entire nested object', doc.nested.path); + doc.nested = {path: 'overwrite the entire nested object'}; + assert.equal(doc.nested.age, undefined); + assert.equal(Object.keys(doc._doc.nested).length, 1); + assert.equal(doc.nested.path, 'overwrite the entire nested object'); assert.ok(doc.isModified('nested')); done(); }); @@ -230,18 +237,18 @@ describe('document', function() { it('test shortcut of id hexString', function(done) { var doc = new TestDocument(); - assert.equal('string', typeof doc.id); + assert.equal(typeof doc.id, 'string'); done(); }); it('test toObject clone', function(done) { var doc = new TestDocument(); doc.init({ - test : 'test' - , oids : [] - , nested : { - age : 5 - , cool : new DocumentObjectId + test: 'test', + oids: [], + nested: { + age: 5, + cool: new DocumentObjectId } }); @@ -259,98 +266,98 @@ describe('document', function() { done(); }); - it('toObject options', function( done ) { + it('toObject options', function(done) { var doc = new TestDocument(); doc.init({ - test : 'test' - , oids : [] - , em: [{title:'asdf'}] - , nested : { - age : 5 - , cool : DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c') - , path : 'my path' - } - , nested2: {} - , date: new Date + test: 'test', + oids: [], + em: [{title: 'asdf'}], + nested: { + age: 5, + cool: DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c'), + path: 'my path' + }, + nested2: {}, + date: new Date }); - var clone = doc.toObject({ getters: true, virtuals: false }); + var clone = doc.toObject({getters: true, virtuals: false}); assert.equal(clone.test, 'test'); assert.ok(clone.oids instanceof Array); - assert.equal(5, clone.nested.age); + assert.equal(clone.nested.age, 5); assert.equal(clone.nested.cool.toString(), '4c6c2d6240ced95d0e00003c'); - assert.equal('5my path', clone.nested.path); - assert.equal(undefined, clone.nested.agePlus2); - assert.equal(undefined, clone.em[0].works); + assert.equal(clone.nested.path, '5my path'); + assert.equal(clone.nested.agePlus2, undefined); + assert.equal(clone.em[0].works, undefined); assert.ok(clone.date instanceof Date); - clone = doc.toObject({ virtuals: true }); + clone = doc.toObject({virtuals: true}); - assert.equal('test', clone.test); + assert.equal(clone.test, 'test'); assert.ok(clone.oids instanceof Array); - assert.equal(5, clone.nested.age); + assert.equal(clone.nested.age, 5); assert.equal(clone.nested.cool.toString(), '4c6c2d6240ced95d0e00003c'); - assert.equal('my path', clone.nested.path); - assert.equal(7, clone.nested.agePlus2); + assert.equal(clone.nested.path, 'my path'); + assert.equal(clone.nested.agePlus2, 7); assert.equal(clone.em[0].works, 'em virtual works'); - clone = doc.toObject({ getters: true }); + clone = doc.toObject({getters: true}); - assert.equal('test', clone.test); + assert.equal(clone.test, 'test'); assert.ok(clone.oids instanceof Array); - assert.equal(5, clone.nested.age); - assert.equal(clone.nested.cool.toString(),'4c6c2d6240ced95d0e00003c'); - assert.equal('5my path', clone.nested.path); - assert.equal(7, clone.nested.agePlus2); - assert.equal('em virtual works', clone.em[0].works); + assert.equal(clone.nested.age, 5); + assert.equal(clone.nested.cool.toString(), '4c6c2d6240ced95d0e00003c'); + assert.equal(clone.nested.path, '5my path'); + assert.equal(clone.nested.agePlus2, 7); + assert.equal(clone.em[0].works, 'em virtual works'); // test toObject options - doc.schema.options.toObject = { virtuals: true }; - clone = doc.toObject({ transform: false, virtuals: true }); - assert.equal('test', clone.test); + doc.schema.options.toObject = {virtuals: true}; + clone = doc.toObject({transform: false, virtuals: true}); + assert.equal(clone.test, 'test'); assert.ok(clone.oids instanceof Array); - assert.equal(5, clone.nested.age); - assert.equal(clone.nested.cool.toString(),'4c6c2d6240ced95d0e00003c'); + assert.equal(clone.nested.age, 5); + assert.equal(clone.nested.cool.toString(), '4c6c2d6240ced95d0e00003c'); - assert.equal('my path', clone.nested.path); - assert.equal(7, clone.nested.agePlus2); - assert.equal('asdf', clone.em[0].title); + assert.equal(clone.nested.path, 'my path'); + assert.equal(clone.nested.agePlus2, 7); + assert.equal(clone.em[0].title, 'asdf'); delete doc.schema.options.toObject; // minimize - clone = doc.toObject({ minimize: true }); - assert.equal(undefined, clone.nested2); - clone = doc.toObject({ minimize: true, getters: true }); - assert.equal(undefined, clone.nested2); - clone = doc.toObject({ minimize: false }); - assert.equal('Object', clone.nested2.constructor.name); - assert.equal(1, Object.keys(clone.nested2).length); + clone = doc.toObject({minimize: true}); + assert.equal(clone.nested2, undefined); + clone = doc.toObject({minimize: true, getters: true}); + assert.equal(clone.nested2, undefined); + clone = doc.toObject({minimize: false}); + assert.equal(clone.nested2.constructor.name, 'Object'); + assert.equal(Object.keys(clone.nested2).length, 1); clone = doc.toObject('2'); - assert.equal(undefined, clone.nested2); + assert.equal(clone.nested2, undefined); - doc.schema.options.toObject = { minimize: false }; - clone = doc.toObject({ transform: false, minimize: false }); - assert.equal('Object', clone.nested2.constructor.name); - assert.equal(1, Object.keys(clone.nested2).length); + doc.schema.options.toObject = {minimize: false}; + clone = doc.toObject({transform: false, minimize: false}); + assert.equal(clone.nested2.constructor.name, 'Object'); + assert.equal(Object.keys(clone.nested2).length, 1); delete doc.schema.options.toObject; doc.schema.options.minimize = false; clone = doc.toObject(); - assert.equal('Object', clone.nested2.constructor.name); - assert.equal(1, Object.keys(clone.nested2).length); + assert.equal(clone.nested2.constructor.name, 'Object'); + assert.equal(Object.keys(clone.nested2).length, 1); doc.schema.options.minimize = true; clone = doc.toObject(); - assert.equal(undefined, clone.nested2); + assert.equal(clone.nested2, undefined); // transform doc.schema.options.toObject = {}; doc.schema.options.toObject.transform = function xform(doc, ret) { - - if ('function' == typeof doc.ownerDocument) - // ignore embedded docs + // ignore embedded docs + if (typeof doc.ownerDocument === 'function') { return; + } delete ret.em; delete ret.numbers; @@ -363,34 +370,35 @@ describe('document', function() { assert.ok(undefined === clone.em); assert.ok(undefined === clone.numbers); assert.ok(undefined === clone.oids); - assert.equal('test', clone.test); - assert.equal(5, clone.nested.age); + assert.equal(clone.test, 'test'); + assert.equal(clone.nested.age, 5); // transform with return value - var out = { myid: doc._id.toString() }; + var out = {myid: doc._id.toString()}; doc.schema.options.toObject.transform = function(doc, ret) { - if ('function' == typeof doc.ownerDocument) - // ignore embedded docs + // ignore embedded docs + if (typeof doc.ownerDocument === 'function') { return; + } - return { myid: ret._id.toString() }; + return {myid: ret._id.toString()}; }; clone = doc.toObject(); assert.deepEqual(out, clone); // ignored transform with inline options - clone = doc.toObject({ x: 1, transform: false }); + clone = doc.toObject({x: 1, transform: false}); assert.ok(!('myid' in clone)); - assert.equal('test', clone.test); + assert.equal(clone.test, 'test'); assert.ok(clone.oids instanceof Array); - assert.equal(5, clone.nested.age); - assert.equal(clone.nested.cool.toString(),'4c6c2d6240ced95d0e00003c'); - assert.equal('my path', clone.nested.path); - assert.equal('Object', clone.em[0].constructor.name); + assert.equal(clone.nested.age, 5); + assert.equal(clone.nested.cool.toString(), '4c6c2d6240ced95d0e00003c'); + assert.equal(clone.nested.path, 'my path'); + assert.equal(clone.em[0].constructor.name, 'Object'); // applied transform when inline transform is true - clone = doc.toObject({ x: 1 }); + clone = doc.toObject({x: 1}); assert.deepEqual(out, clone); // transform passed inline @@ -399,11 +407,12 @@ describe('document', function() { delete doc[field]; }); } + clone = doc.toObject({ - transform: xform - , fields: '_id em numbers oids nested' + transform: xform, + fields: '_id em numbers oids nested' }); - assert.equal('test', doc.test); + assert.equal(doc.test, 'test'); assert.ok(undefined === clone.em); assert.ok(undefined === clone.numbers); assert.ok(undefined === clone.oids); @@ -415,7 +424,7 @@ describe('document', function() { done(); }); - it('toObject transform', function( done ) { + it('toObject transform', function(done) { var schema = new Schema({ name: String, places: [{type: ObjectId, ref: 'toObject-transform-places'}] @@ -427,22 +436,20 @@ describe('document', function() { schemaPlaces.set('toObject', { transform: function(doc, ret) { - // here should be only toObject-transform-places documents assert.equal(doc.constructor.modelName, 'toObject-transform-places'); - return ret; } }); - var db = start() - , Test = db.model('toObject-transform', schema) - , Places = db.model('toObject-transform-places', schemaPlaces); + var db = start(), + Test = db.model('toObject-transform', schema), + Places = db.model('toObject-transform-places', schemaPlaces); - Places.create({ identity: 'a' },{ identity: 'b' },{ identity: 'c' }, function(err, a, b, c) { - Test.create({ name: 'chetverikov', places: [a, b, c]}, function( err ) { + Places.create({identity: 'a'}, {identity: 'b'}, {identity: 'c'}, function(err, a, b, c) { + Test.create({name: 'chetverikov', places: [a, b, c]}, function(err) { assert.ifError(err); - Test.findOne({}).populate('places').exec(function( err, docs ) { + Test.findOne({}).populate('places').exec(function(err, docs) { assert.ifError(err); docs.toObject({transform: true}); @@ -457,16 +464,16 @@ describe('document', function() { var db = start(); var MyModel = db.model('gh2981', - { name: { type: String, required: true } }); + {name: {type: String, required: true}}); var doc = new MyModel(); - doc.save({ validateBeforeSave: false }, function(error) { + doc.save({validateBeforeSave: false}, function(error) { assert.ifError(error); db.close(done); }); }); - it('doesnt use custom toObject options on save', function( done ) { + it('doesnt use custom toObject options on save', function(done) { var schema = new Schema({ name: String, iWillNotBeDelete: Boolean, @@ -483,380 +490,522 @@ describe('document', function() { return ret; } }); - var db = start() - , Test = db.model('TestToObject', schema); + var db = start(), + Test = db.model('TestToObject', schema); - Test.create({ name: 'chetverikov', iWillNotBeDelete: true, 'nested.iWillNotBeDeleteToo': true}, function( err ) { + Test.create({name: 'chetverikov', iWillNotBeDelete: true, 'nested.iWillNotBeDeleteToo': true}, function(err) { assert.ifError(err); - Test.findOne({}, function( err, doc ) { + Test.findOne({}, function(err, doc) { assert.ifError(err); - assert.equal( doc._doc.iWillNotBeDelete, true ); - assert.equal( doc._doc.nested.iWillNotBeDeleteToo, true ); + assert.equal(doc._doc.iWillNotBeDelete, true); + assert.equal(doc._doc.nested.iWillNotBeDeleteToo, true); db.close(done); }); }); }); - it('does not apply toObject functions of subdocuments to root document', function( done ) { - - var subdocSchema = new Schema({ - test: String, - wow: String + describe('toObject', function() { + var db; + before(function() { + return start({ useMongoClient: true }).then(function(_db) { + db = _db; + }); }); - subdocSchema.options.toObject = {}; - subdocSchema.options.toObject.transform = function(doc, ret) { - delete ret.wow; - }; - - var docSchema = new Schema({ - foo: String, - wow: Boolean, - sub: [subdocSchema] + after(function(done) { + db.close(done); }); - var db = start() - , Doc = db.model('Doc', docSchema); + it('does not apply toObject functions of subdocuments to root document', function(done) { + var subdocSchema = new Schema({ + test: String, + wow: String + }); - Doc.create({ - foo: 'someString', - wow: true, - sub: [{ - test: 'someOtherString', - wow: 'thisIsAString' - }] - }, function( err, doc ) { + subdocSchema.options.toObject = {}; + subdocSchema.options.toObject.transform = function(doc, ret) { + delete ret.wow; + }; - var obj = doc.toObject({ - transform: function(doc, ret) { - ret.phew = 'new'; - } + var docSchema = new Schema({ + foo: String, + wow: Boolean, + sub: [subdocSchema] }); - assert.equal(obj.phew, 'new'); - assert.ok(!doc.sub.wow); + var Doc = db.model('Doc', docSchema); - db.close(done); - }); - }); + Doc.create({ + foo: 'someString', + wow: true, + sub: [{ + test: 'someOtherString', + wow: 'thisIsAString' + }] + }, function(err, doc) { + var obj = doc.toObject({ + transform: function(doc, ret) { + ret.phew = 'new'; + } + }); - it('handles child schema transforms', function(done) { - var db = start(); - var userSchema = new Schema({ - name: String, - email: String - }); - var topicSchema = new Schema({ - title: String, - email: String, - followers: [userSchema] + assert.equal(obj.phew, 'new'); + assert.ok(!doc.sub.wow); + + done(); + }); }); - userSchema.options.toObject = { - transform: function(doc, ret) { - delete ret.email; - } - }; + it('handles child schema transforms', function(done) { + var userSchema = new Schema({ + name: String, + email: String + }); + var topicSchema = new Schema({ + title: String, + email: String, + followers: [userSchema] + }); - topicSchema.options.toObject = { - transform: function(doc, ret) { - ret.title = ret.title.toLowerCase(); - } - }; + userSchema.options.toObject = { + transform: function(doc, ret) { + delete ret.email; + } + }; - var Topic = db.model('gh2691', topicSchema, 'gh2691'); + topicSchema.options.toObject = { + transform: function(doc, ret) { + ret.title = ret.title.toLowerCase(); + } + }; - var topic = new Topic({ - title: 'Favorite Foods', - email: 'a@b.co', - followers: [{ name: 'Val', email: 'val@test.co' }] - }); + var Topic = db.model('gh2691', topicSchema, 'gh2691'); - var output = topic.toObject({ transform: true }); - assert.equal('favorite foods', output.title); - assert.equal('a@b.co', output.email); - assert.equal('Val', output.followers[0].name); - assert.equal(undefined, output.followers[0].email); - db.close(done); - }); + var topic = new Topic({ + title: 'Favorite Foods', + email: 'a@b.co', + followers: [{name: 'Val', email: 'val@test.co'}] + }); - it('doesnt clobber child schema options when called with no params (gh-2035)', function(done) { - var db = start(); - var userSchema = new Schema({ - firstName: String, - lastName: String, - password: String + var output = topic.toObject({transform: true}); + assert.equal(output.title, 'favorite foods'); + assert.equal(output.email, 'a@b.co'); + assert.equal(output.followers[0].name, 'Val'); + assert.equal(output.followers[0].email, undefined); + done(); }); - userSchema.virtual('fullName').get(function() { - return this.firstName + ' ' + this.lastName; - }); + it('doesnt clobber child schema options when called with no params (gh-2035)', function(done) { + var userSchema = new Schema({ + firstName: String, + lastName: String, + password: String + }); - userSchema.set('toObject', { virtuals: false }); + userSchema.virtual('fullName').get(function() { + return this.firstName + ' ' + this.lastName; + }); - var postSchema = new Schema({ - owner: { type: Schema.Types.ObjectId, ref: 'gh-2035-user' }, - content: String - }); + userSchema.set('toObject', {virtuals: false}); - postSchema.virtual('capContent').get(function() { - return this.content.toUpperCase(); - }); + var postSchema = new Schema({ + owner: {type: Schema.Types.ObjectId, ref: 'gh-2035-user'}, + content: String + }); + + postSchema.virtual('capContent').get(function() { + return this.content.toUpperCase(); + }); - postSchema.set('toObject', { virtuals: true }); - var User = db.model('gh-2035-user', userSchema, 'gh-2035-user'); - var Post = db.model('gh-2035-post', postSchema, 'gh-2035-post'); + postSchema.set('toObject', {virtuals: true}); + var User = db.model('gh-2035-user', userSchema, 'gh-2035-user'); + var Post = db.model('gh-2035-post', postSchema, 'gh-2035-post'); - var user = new User({ firstName: 'Joe', lastName: 'Smith', password: 'password' }); + var user = new User({firstName: 'Joe', lastName: 'Smith', password: 'password'}); - user.save(function(err, savedUser) { - assert.ifError(err); - var post = new Post({ owner: savedUser._id, content: 'lorem ipsum' }); - post.save(function(err, savedPost) { + user.save(function(err, savedUser) { assert.ifError(err); - Post.findById(savedPost._id).populate('owner').exec(function(err, newPost) { + var post = new Post({owner: savedUser._id, content: 'lorem ipsum'}); + post.save(function(err, savedPost) { assert.ifError(err); - var obj = newPost.toObject(); - assert.equal(obj.owner.fullName, undefined); - db.close(done); + Post.findById(savedPost._id).populate('owner').exec(function(err, newPost) { + assert.ifError(err); + var obj = newPost.toObject(); + assert.equal(obj.owner.fullName, undefined); + done(); + }); }); }); }); }); - it('toJSON options', function(done) { - var doc = new TestDocument(); + describe('toJSON', function() { + var db; + before(function() { + db = start(); + }); - doc.init({ - test : 'test' - , oids : [] - , em: [{title:'asdf'}] - , nested : { - age : 5 - , cool : DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c') - , path : 'my path' - } - , nested2: {} + after(function(done) { + db.close(done); }); - // override to check if toJSON gets fired - var path = TestDocument.prototype.schema.path('em'); - path.casterConstructor.prototype.toJSON = function() { - return {}; - }; + it('toJSON options', function(done) { + var doc = new TestDocument(); - doc.schema.options.toJSON = { virtuals: true }; - var clone = doc.toJSON(); - assert.equal('test', clone.test); - assert.ok(clone.oids instanceof Array); - assert.equal(5, clone.nested.age); - assert.equal(clone.nested.cool.toString(),'4c6c2d6240ced95d0e00003c'); - assert.equal('my path', clone.nested.path); - assert.equal(7, clone.nested.agePlus2); - assert.equal('Object', clone.em[0].constructor.name); - assert.equal(0, Object.keys(clone.em[0]).length); - delete doc.schema.options.toJSON; - delete path.casterConstructor.prototype.toJSON; - - doc.schema.options.toJSON = { minimize: false }; - clone = doc.toJSON(); - assert.equal('Object', clone.nested2.constructor.name); - assert.equal(1, Object.keys(clone.nested2).length); - clone = doc.toJSON('8'); - assert.equal('Object', clone.nested2.constructor.name); - assert.equal(1, Object.keys(clone.nested2).length); - - // gh-852 - var arr = [doc] - , err = false - , str; - try { - str = JSON.stringify(arr); - } catch (_) { err = true; } - assert.equal(false, err); - assert.ok(/nested2/.test(str)); - assert.equal('Object', clone.nested2.constructor.name); - assert.equal(1, Object.keys(clone.nested2).length); + doc.init({ + test: 'test', + oids: [], + em: [{title: 'asdf'}], + nested: { + age: 5, + cool: DocumentObjectId.createFromHexString('4c6c2d6240ced95d0e00003c'), + path: 'my path' + }, + nested2: {} + }); - // transform - doc.schema.options.toJSON = {}; - doc.schema.options.toJSON.transform = function xform(doc, ret) { - if ('function' == typeof doc.ownerDocument) - // ignore embedded docs - return; + // override to check if toJSON gets fired + var path = TestDocument.prototype.schema.path('em'); + path.casterConstructor.prototype.toJSON = function() { + return {}; + }; - delete ret.em; - delete ret.numbers; - delete ret.oids; - ret._id = ret._id.toString(); - }; + doc.schema.options.toJSON = {virtuals: true}; + var clone = doc.toJSON(); + assert.equal(clone.test, 'test'); + assert.ok(clone.oids instanceof Array); + assert.equal(clone.nested.age, 5); + assert.equal(clone.nested.cool.toString(), '4c6c2d6240ced95d0e00003c'); + assert.equal(clone.nested.path, 'my path'); + assert.equal(clone.nested.agePlus2, 7); + assert.equal(clone.em[0].constructor.name, 'Object'); + assert.equal(Object.keys(clone.em[0]).length, 0); + delete doc.schema.options.toJSON; + delete path.casterConstructor.prototype.toJSON; + + doc.schema.options.toJSON = {minimize: false}; + clone = doc.toJSON(); + assert.equal(clone.nested2.constructor.name, 'Object'); + assert.equal(Object.keys(clone.nested2).length, 1); + clone = doc.toJSON('8'); + assert.equal(clone.nested2.constructor.name, 'Object'); + assert.equal(Object.keys(clone.nested2).length, 1); + + // gh-852 + var arr = [doc], + err = false, + str; + try { + str = JSON.stringify(arr); + } catch (_) { + err = true; + } + assert.equal(err, false); + assert.ok(/nested2/.test(str)); + assert.equal(clone.nested2.constructor.name, 'Object'); + assert.equal(Object.keys(clone.nested2).length, 1); + + // transform + doc.schema.options.toJSON = {}; + doc.schema.options.toJSON.transform = function xform(doc, ret) { + // ignore embedded docs + if (typeof doc.ownerDocument === 'function') { + return; + } - clone = doc.toJSON(); - assert.equal(doc.id, clone._id); - assert.ok(undefined === clone.em); - assert.ok(undefined === clone.numbers); - assert.ok(undefined === clone.oids); - assert.equal('test', clone.test); - assert.equal(5, clone.nested.age); + delete ret.em; + delete ret.numbers; + delete ret.oids; + ret._id = ret._id.toString(); + }; - // transform with return value - var out = { myid: doc._id.toString() }; - doc.schema.options.toJSON.transform = function(doc, ret) { - if ('function' == typeof doc.ownerDocument) + clone = doc.toJSON(); + assert.equal(clone._id, doc.id); + assert.ok(undefined === clone.em); + assert.ok(undefined === clone.numbers); + assert.ok(undefined === clone.oids); + assert.equal(clone.test, 'test'); + assert.equal(clone.nested.age, 5); + + // transform with return value + var out = {myid: doc._id.toString()}; + doc.schema.options.toJSON.transform = function(doc, ret) { // ignore embedded docs - return; + if (typeof doc.ownerDocument === 'function') { + return; + } - return { myid: ret._id.toString() }; - }; + return {myid: ret._id.toString()}; + }; - clone = doc.toJSON(); - assert.deepEqual(out, clone); + clone = doc.toJSON(); + assert.deepEqual(out, clone); + + // ignored transform with inline options + clone = doc.toJSON({x: 1, transform: false}); + assert.ok(!('myid' in clone)); + assert.equal(clone.test, 'test'); + assert.ok(clone.oids instanceof Array); + assert.equal(clone.nested.age, 5); + assert.equal(clone.nested.cool.toString(), '4c6c2d6240ced95d0e00003c'); + assert.equal(clone.nested.path, 'my path'); + assert.equal(clone.em[0].constructor.name, 'Object'); + + // applied transform when inline transform is true + clone = doc.toJSON({x: 1}); + assert.deepEqual(out, clone); + + // transform passed inline + function xform(self, doc, opts) { + opts.fields.split(' ').forEach(function(field) { + delete doc[field]; + }); + } - // ignored transform with inline options - clone = doc.toJSON({ x: 1, transform: false }); - assert.ok(!('myid' in clone)); - assert.equal('test', clone.test); - assert.ok(clone.oids instanceof Array); - assert.equal(5, clone.nested.age); - assert.equal(clone.nested.cool.toString(),'4c6c2d6240ced95d0e00003c'); - assert.equal('my path', clone.nested.path); - assert.equal('Object', clone.em[0].constructor.name); + clone = doc.toJSON({ + transform: xform, + fields: '_id em numbers oids nested' + }); + assert.equal(doc.test, 'test'); + assert.ok(undefined === clone.em); + assert.ok(undefined === clone.numbers); + assert.ok(undefined === clone.oids); + assert.ok(undefined === clone._id); + assert.ok(undefined === clone.nested); + + // all done + delete doc.schema.options.toJSON; + done(); + }); - // applied transform when inline transform is true - clone = doc.toJSON({ x: 1 }); - assert.deepEqual(out, clone); + it('jsonifying an object', function(done) { + var doc = new TestDocument({test: 'woot'}), + oidString = doc._id.toString(); + // convert to json string + var json = JSON.stringify(doc); + // parse again + var obj = JSON.parse(json); - // transform passed inline - function xform(self, doc, opts) { - opts.fields.split(' ').forEach(function(field) { - delete doc[field]; - }); - } - clone = doc.toJSON({ - transform: xform - , fields: '_id em numbers oids nested' + assert.equal(obj.test, 'woot'); + assert.equal(obj._id, oidString); + done(); }); - assert.equal('test', doc.test); - assert.ok(undefined === clone.em); - assert.ok(undefined === clone.numbers); - assert.ok(undefined === clone.oids); - assert.ok(undefined === clone._id); - assert.ok(undefined === clone.nested); - // all done - delete doc.schema.options.toJSON; - done(); - }); + it('jsonifying an object\'s populated items works (gh-1376)', function(done) { + var userSchema, User, groupSchema, Group; - it('jsonifying an object', function(done) { - var doc = new TestDocument({ test: 'woot' }) - , oidString = doc._id.toString(); - // convert to json string - var json = JSON.stringify(doc); - // parse again - var obj = JSON.parse(json); + userSchema = new Schema({name: String}); + // includes virtual path when 'toJSON' + userSchema.set('toJSON', {getters: true}); + userSchema.virtual('hello').get(function() { + return 'Hello, ' + this.name; + }); + User = db.model('User', userSchema); - assert.equal('woot', obj.test); - assert.equal(obj._id, oidString); - done(); + groupSchema = new Schema({ + name: String, + _users: [{type: Schema.ObjectId, ref: 'User'}] + }); + + Group = db.model('Group', groupSchema); + + User.create({name: 'Alice'}, {name: 'Bob'}, function(err, alice, bob) { + assert.ifError(err); + + new Group({name: 'mongoose', _users: [alice, bob]}).save(function(err, group) { + Group.findById(group).populate('_users').exec(function(err, group) { + assert.ifError(err); + assert.ok(group.toJSON()._users[0].hello); + done(); + }); + }); + }); + }); }); - it('jsonifying an object\'s populated items works (gh-1376)', function(done) { - var db = start(); - var userSchema, User, groupSchema, Group; - userSchema = Schema({name: String}); - // includes virtual path when 'toJSON' - userSchema.set('toJSON', {getters: true}); - userSchema.virtual('hello').get(function() { - return 'Hello, ' + this.name; + describe('inspect', function() { + var db; + before(function() { + db = start(); }); - User = db.model('User', userSchema); - groupSchema = Schema({ - name: String, - _users: [{type: Schema.ObjectId, ref: 'User'}] + after(function(done) { + db.close(done); }); - Group = db.model('Group', groupSchema); + it('inspect inherits schema options (gh-4001)', function(done) { + var opts = { + toObject: { virtuals: true }, + toJSON: { virtuals: true } + }; + var taskSchema = mongoose.Schema({ + name: { + type: String, + required: true + } + }, opts); + + taskSchema.virtual('title'). + get(function() { + return this.name; + }). + set(function(title) { + this.name = title; + }); - User.create({name: 'Alice'}, {name: 'Bob'}, function(err, alice, bob) { - assert.ifError(err); + var Task = db.model('gh4001', taskSchema); - new Group({name: 'mongoose', _users: [alice, bob]}).save(function(err, group) { - Group.findById(group).populate('_users').exec(function(err, group) { - assert.ifError(err); - assert.ok(group.toJSON()._users[0].hello); - db.close(done); + var doc = { name: 'task1', title: 'task999' }; + Task.collection.insert(doc, function(error) { + assert.ifError(error); + Task.findById(doc._id, function(error, doc) { + assert.ifError(error); + assert.equal(doc.inspect().title, 'task1'); + done(); + }); + }); + }); + + it('does not apply transform to populated docs (gh-4213)', function(done) { + var UserSchema = new Schema({ + name: String + }); + + var PostSchema = new Schema({ + title: String, + postedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'gh4213' + } + }, { + toObject: { + transform: function(doc, ret) { + delete ret._id; + } + }, + toJSON: { + transform: function(doc, ret) { + delete ret._id; + } + } + }); + + var User = db.model('gh4213', UserSchema); + var Post = db.model('gh4213_0', PostSchema); + + var val = new User({ name: 'Val' }); + var post = new Post({ title: 'Test', postedBy: val._id }); + + Post.create(post, function(error) { + assert.ifError(error); + User.create(val, function(error) { + assert.ifError(error); + Post.find({}). + populate('postedBy'). + exec(function(error, posts) { + assert.ifError(error); + assert.equal(posts.length, 1); + assert.ok(posts[0].postedBy._id); + done(); + }); }); }); }); + + it('populate on nested path (gh-5703)', function() { + var toySchema = new mongoose.Schema({ color: String }); + var Toy = db.model('gh5703', toySchema); + + var childSchema = new mongoose.Schema({ + name: String, + values: { + toy: { type: mongoose.Schema.Types.ObjectId, ref: 'gh5703' } + } + }); + var Child = db.model('gh5703_0', childSchema); + + return Toy.create({ color: 'blue' }). + then(function(toy) { + return Child.create({ values: { toy: toy._id } }); + }). + then(function(child) { + return Child.findById(child._id); + }). + then(function(child) { + return child.values.populate('toy').execPopulate().then(function() { + return child; + }); + }). + then(function(child) { + assert.equal(child.values.toy.color, 'blue'); + }); + }); }); describe('#update', function() { it('returns a Query', function(done) { var mg = new mongoose.Mongoose; - var M = mg.model('doc#update', { s: String }); + var M = mg.model('doc#update', {s: String}); var doc = new M; assert.ok(doc.update() instanceof Query); done(); }); it('calling update on document should relay to its model (gh-794)', function(done) { var db = start(); - var Docs = new Schema({text:String}); + var Docs = new Schema({text: String}); var docs = db.model('docRelayUpdate', Docs); - var d = new docs({text:'A doc'}); + var d = new docs({text: 'A doc'}); var called = false; d.save(function() { var oldUpdate = docs.update; docs.update = function(query, operation) { - assert.equal(1, Object.keys(query).length); - assert.equal(query._id, d._id); - assert.equal(1, Object.keys(operation).length); - assert.equal(1, Object.keys(operation.$set).length); + assert.equal(Object.keys(query).length, 1); + assert.equal(d._id, query._id); + assert.equal(Object.keys(operation).length, 1); + assert.equal(Object.keys(operation.$set).length, 1); assert.equal(operation.$set.text, 'A changed doc'); called = true; docs.update = oldUpdate; oldUpdate.apply(docs, arguments); }; - d.update({$set :{text: 'A changed doc'}}, function(err) { + d.update({$set: {text: 'A changed doc'}}, function(err) { assert.ifError(err); - assert.equal(true, called); + assert.equal(called, true); db.close(done); }); }); - }); }); it('toObject should not set undefined values to null', function(done) { - var doc = new TestDocument() - , obj = doc.toObject(); + var doc = new TestDocument(), + obj = doc.toObject(); delete obj._id; - assert.deepEqual(obj, { numbers: [], oids: [], em: [] }); + assert.deepEqual(obj, {numbers: [], oids: [], em: []}); done(); }); describe('Errors', function() { it('MongooseErrors should be instances of Error (gh-209)', function(done) { - var MongooseError = require('../lib/error') - , err = new MongooseError("Some message"); + var MongooseError = require('../lib/error'), + err = new MongooseError('Some message'); assert.ok(err instanceof Error); done(); }); it('ValidationErrors should be instances of Error', function(done) { - var ValidationError = Document.ValidationError - , err = new ValidationError(new TestDocument); + var ValidationError = Document.ValidationError, + err = new ValidationError(new TestDocument); assert.ok(err instanceof Error); done(); }); }); it('methods on embedded docs should work', function(done) { - var db = start() - , ESchema = new Schema({ name: String }); + var db = start(), + ESchema = new Schema({name: String}); ESchema.methods.test = function() { return this.name + ' butter'; @@ -866,33 +1015,33 @@ describe('document', function() { }; var E = db.model('EmbeddedMethodsAndStaticsE', ESchema); - var PSchema = new Schema({ embed: [ESchema] }); + var PSchema = new Schema({embed: [ESchema]}); var P = db.model('EmbeddedMethodsAndStaticsP', PSchema); db.close(); - var p = new P({ embed: [{name: 'peanut'}] }); - assert.equal('function', typeof p.embed[0].test); - assert.equal('function', typeof E.ten); - assert.equal('peanut butter', p.embed[0].test()); - assert.equal(10, E.ten()); + var p = new P({embed: [{name: 'peanut'}]}); + assert.equal(typeof p.embed[0].test, 'function'); + assert.equal(typeof E.ten, 'function'); + assert.equal(p.embed[0].test(), 'peanut butter'); + assert.equal(E.ten(), 10); // test push casting p = new P; p.embed.push({name: 'apple'}); - assert.equal('function', typeof p.embed[0].test); - assert.equal('function', typeof E.ten); - assert.equal('apple butter', p.embed[0].test()); + assert.equal(typeof p.embed[0].test, 'function'); + assert.equal(typeof E.ten, 'function'); + assert.equal(p.embed[0].test(), 'apple butter'); done(); }); it('setting a positional path does not cast value to array', function(done) { var doc = new TestDocument; - doc.init({ numbers: [1,3] }); - assert.equal(1, doc.numbers[0]); - assert.equal(3, doc.numbers[1]); + doc.init({numbers: [1, 3]}); + assert.equal(doc.numbers[0], 1); + assert.equal(doc.numbers[1], 3); doc.set('numbers.1', 2); - assert.equal(1, doc.numbers[0]); - assert.equal(2, doc.numbers[1]); + assert.equal(doc.numbers[0], 1); + assert.equal(doc.numbers[1], 2); done(); }); @@ -908,39 +1057,39 @@ describe('document', function() { }; var schema = new Schema({ - title: String - , embed1: [new Schema({name:String})] - , embed2: [new Schema({name:String})] - , embed3: [new Schema({name:String})] - , embed4: [new Schema({name:String})] - , embed5: [new Schema({name:String})] - , embed6: [new Schema({name:String})] - , embed7: [new Schema({name:String})] - , embed8: [new Schema({name:String})] - , embed9: [new Schema({name:String})] - , embed10: [new Schema({name:String})] - , embed11: [new Schema({name:String})] + title: String, + embed1: [new Schema({name: String})], + embed2: [new Schema({name: String})], + embed3: [new Schema({name: String})], + embed4: [new Schema({name: String})], + embed5: [new Schema({name: String})], + embed6: [new Schema({name: String})], + embed7: [new Schema({name: String})], + embed8: [new Schema({name: String})], + embed9: [new Schema({name: String})], + embed10: [new Schema({name: String})], + embed11: [new Schema({name: String})] }); var S = db.model('noMaxListeners', schema); - new S({ title: "test" }); + new S({title: 'test'}); db.close(); - assert.equal(false, traced); + assert.equal(traced, false); done(); }); it('unselected required fields should pass validation', function(done) { - var db = start() - , Tschema = new Schema({ name: String, req: { type: String, required: true }}) - , T = db.model('unselectedRequiredFieldValidation', Tschema); + var db = start(), + Tschema = new Schema({name: String, req: {type: String, required: true}}), + T = db.model('unselectedRequiredFieldValidation', Tschema); - var t = new T({ name: 'teeee', req: 'i am required' }); + var t = new T({name: 'teeee', req: 'i am required'}); t.save(function(err) { assert.ifError(err); T.findById(t).select('name').exec(function(err, t) { assert.ifError(err); - assert.equal(undefined, t.req); + assert.equal(t.req, void 0); t.name = 'wooo'; t.save(function(err) { assert.ifError(err); @@ -979,25 +1128,28 @@ describe('document', function() { var schema = null; var called = false; - var validate = [ function() { called = true; return true; }, 'BAM']; + var validate = [function() { + called = true; + return true; + }, 'BAM']; schema = new Schema({ - prop: { type: String, required: true, validate: validate }, - nick: { type: String, required: true } + prop: {type: String, required: true, validate: validate}, + nick: {type: String, required: true} }); var M = db.model('validateSchema', schema, collection); - var m = new M({ prop: 'gh891', nick: 'validation test' }); + var m = new M({prop: 'gh891', nick: 'validation test'}); m.save(function(err) { assert.ifError(err); - assert.equal(true, called); + assert.equal(called, true); called = false; M.findById(m, 'nick', function(err, m) { - assert.equal(false, called); + assert.equal(called, false); assert.ifError(err); m.nick = 'gh-891'; m.save(function(err) { - assert.equal(false, called); + assert.equal(called, false); assert.ifError(err); db.close(done); }); @@ -1009,16 +1161,18 @@ describe('document', function() { var db = start(); var schema = null; - var validate = [ function() { return true; }, 'BAM']; + var validate = [function() { + return true; + }, 'BAM']; schema = new Schema({ - prop: { type: String, required: true, validate: validate }, - nick: { type: String, required: true } + prop: {type: String, required: true, validate: validate}, + nick: {type: String, required: true} }); var M = db.model('validateSchemaPromise', schema, collection); - var m = new M({ prop: 'gh891', nick: 'validation test' }); - var mBad = new M({ prop: 'other' }); + var m = new M({prop: 'gh891', nick: 'validation test'}); + var mBad = new M({prop: 'other'}); var promise = m.validate(); promise.then(function() { @@ -1032,16 +1186,16 @@ describe('document', function() { var timeout = setTimeout(function() { db.close(); - throw new Error("Promise not fulfilled!"); + throw new Error('Promise not fulfilled!'); }, 500); }); it('doesnt have stale cast errors (gh-2766)', function(done) { var db = start(); - var testSchema = new Schema({ name: String }); + var testSchema = new Schema({name: String}); var M = db.model('gh2766', testSchema); - var m = new M({ _id: 'this is not a valid _id' }); + var m = new M({_id: 'this is not a valid _id'}); assert.ok(!m.$isValid('_id')); assert.ok(m.validateSync().errors['_id'].name, 'CastError'); @@ -1056,10 +1210,10 @@ describe('document', function() { it('cast errors persist across validate() calls (gh-2766)', function(done) { var db = start(); - var testSchema = new Schema({ name: String }); + var testSchema = new Schema({name: String}); var M = db.model('gh2766', testSchema); - var m = new M({ _id: 'this is not a valid _id' }); + var m = new M({_id: 'this is not a valid _id'}); assert.ok(!m.$isValid('_id')); m.validate(function(error) { assert.ok(error); @@ -1081,7 +1235,7 @@ describe('document', function() { var db = start(); var schema = null; - schema = new Schema({ _id : String }); + schema = new Schema({_id: String}); var M = db.model('validateSchemaPromise2', schema, collection); var m = new M(); @@ -1095,7 +1249,7 @@ describe('document', function() { var timeout = setTimeout(function() { db.close(); - throw new Error("Promise not fulfilled!"); + throw new Error('Promise not fulfilled!'); }, 500); }); @@ -1114,10 +1268,10 @@ describe('document', function() { it('with required', function(done) { var schema = new Schema({ name: String, - arr : { type: [], required: true } + arr: {type: [], required: true} }); var M = db.model('validateSchema-array1', schema, collection); - var m = new M({ name: 'gh1109-1' }); + var m = new M({name: 'gh1109-1'}); m.save(function(err) { assert.ok(/Path `arr` is required/.test(err)); m.arr = []; @@ -1143,19 +1297,19 @@ describe('document', function() { var validate = [validator, 'BAM']; var schema = new Schema({ - arr : { type: [], validate: validate } + arr: {type: [], validate: validate} }); var M = db.model('validateSchema-array2', schema, collection); - var m = new M({ name: 'gh1109-2', arr: [1] }); - assert.equal(false, called); + var m = new M({name: 'gh1109-2', arr: [1]}); + assert.equal(called, false); m.save(function(err) { - assert.equal('ValidationError: BAM', String(err)); - assert.equal(true, called); + assert.equal(String(err), 'ValidationError: arr: BAM'); + assert.equal(called, true); m.arr.push(2); called = false; m.save(function(err) { - assert.equal(true, called); + assert.equal(called, true); assert.ifError(err); done(); }); @@ -1170,16 +1324,16 @@ describe('document', function() { var validate = [validator, 'BAM']; var schema = new Schema({ - arr : { type: [], required: true, validate: validate } + arr: {type: [], required: true, validate: validate} }); var M = db.model('validateSchema-array3', schema, collection); - var m = new M({ name: 'gh1109-3' }); + var m = new M({name: 'gh1109-3'}); m.save(function(err) { assert.equal(err.errors.arr.message, 'Path `arr` is required.'); m.arr.push({nice: true}); m.save(function(err) { - assert.equal(String(err), 'ValidationError: BAM'); + assert.equal(String(err), 'ValidationError: arr: BAM'); m.arr.push(95); m.save(function(err) { assert.ifError(err); @@ -1190,7 +1344,7 @@ describe('document', function() { }); }); - it("validator should run only once gh-1743", function(done) { + it('validator should run only once gh-1743', function(done) { var count = 0; var db = start(); @@ -1211,7 +1365,7 @@ describe('document', function() { var post = new Post({ controls: [{ - test: "xx" + test: 'xx' }] }); @@ -1221,7 +1375,9 @@ describe('document', function() { }); }); - it("validator should run only once per sub-doc gh-1743", function(done) { + it('validator should run only once per sub-doc gh-1743', function(done) { + this.timeout(process.env.TRAVIS ? 8000 : 4500); + var count = 0; var db = start(); @@ -1241,14 +1397,11 @@ describe('document', function() { var Post = db.model('post', PostSchema); var post = new Post({ - controls: [ - { - test: "xx" - }, - { - test: "yy" - } - ] + controls: [{ + test: 'xx' + }, { + test: 'yy' + }] }); post.save(function() { @@ -1258,48 +1411,46 @@ describe('document', function() { }); - it("validator should run in parallel", function(done) { - // we set the time out to be double that of the validator - 1 (so that running in serial will be greater then that) - this.timeout(1000); + it('validator should run in parallel', function(done) { var db = start(); var count = 0; + var startTime, endTime; var SchemaWithValidator = new Schema({ preference: { type: String, required: true, - validate: function validator(value, done) { - count++; - setTimeout(done.bind(null, true), 500); + validate: { + validator: function validator(value, done) { + count++; + if (count === 1) startTime = Date.now(); + else if (count === 4) endTime = Date.now(); + setTimeout(done.bind(null, true), 150); + } } } }); var MWSV = db.model('mwv', new Schema({subs: [SchemaWithValidator]})); var m = new MWSV({ - subs: [ - { - preference: "xx" - }, - { - preference: "yy" - }, - { - preference: "1" - }, - { - preference: "2" - } - ] + subs: [{ + preference: 'xx' + }, { + preference: 'yy' + }, { + preference: '1' + }, { + preference: '2' + }] }); m.save(function(err) { assert.ifError(err); assert.equal(count, 4); + assert(endTime - startTime < 150 * 4); // serial >= 150 * 4, parallel < 150 * 4 db.close(done); }); }); - }); it('#invalidate', function(done) { @@ -1308,23 +1459,26 @@ describe('document', function() { var Post = null; var post = null; - InvalidateSchema = new Schema({ prop: { type: String } }, - { strict: false }); + InvalidateSchema = new Schema({prop: {type: String}}, + {strict: false}); mongoose.model('InvalidateSchema', InvalidateSchema); Post = db.model('InvalidateSchema'); post = new Post(); post.set({baz: 'val'}); - post.invalidate('baz', 'validation failed for path {PATH}'); + var _err = post.invalidate('baz', 'validation failed for path {PATH}', + 'val', 'custom error'); + assert.ok(_err instanceof ValidationError); post.save(function(err) { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); assert.ok(err.errors.baz instanceof ValidatorError); - assert.equal(err.errors.baz.message,'validation failed for path baz'); - assert.equal(err.errors.baz.kind,'user defined'); - assert.equal(err.errors.baz.path,'baz'); + assert.equal(err.errors.baz.message, 'validation failed for path baz'); + assert.equal(err.errors.baz.path, 'baz'); + assert.equal(err.errors.baz.value, 'val'); + assert.equal(err.errors.baz.kind, 'custom error'); post.save(function(err) { db.close(); @@ -1345,11 +1499,11 @@ describe('document', function() { before(function() { db = start(); - S = db.model('equals-S', new Schema({ _id: String })); - N = db.model('equals-N', new Schema({ _id: Number })); - O = db.model('equals-O', new Schema({ _id: Schema.ObjectId })); - B = db.model('equals-B', new Schema({ _id: Buffer })); - M = db.model('equals-I', new Schema({ name: String }, { _id: false })); + S = db.model('equals-S', new Schema({_id: String})); + N = db.model('equals-N', new Schema({_id: Number})); + O = db.model('equals-O', new Schema({_id: Schema.ObjectId})); + B = db.model('equals-B', new Schema({_id: Buffer})); + M = db.model('equals-I', new Schema({name: String}, {_id: false})); }); after(function(done) { @@ -1357,32 +1511,32 @@ describe('document', function() { }); it('with string _ids', function(done) { - var s1 = new S({ _id: 'one' }); - var s2 = new S({ _id: 'one' }); + var s1 = new S({_id: 'one'}); + var s2 = new S({_id: 'one'}); assert.ok(s1.equals(s2)); done(); }); it('with number _ids', function(done) { - var n1 = new N({ _id: 0 }); - var n2 = new N({ _id: 0 }); + var n1 = new N({_id: 0}); + var n2 = new N({_id: 0}); assert.ok(n1.equals(n2)); done(); }); it('with ObjectId _ids', function(done) { var id = new mongoose.Types.ObjectId; - var o1 = new O({ _id: id }); - var o2 = new O({ _id: id }); + var o1 = new O({_id: id}); + var o2 = new O({_id: id}); assert.ok(o1.equals(o2)); id = String(new mongoose.Types.ObjectId); - o1 = new O({ _id: id }); - o2 = new O({ _id: id }); + o1 = new O({_id: id}); + o2 = new O({_id: id}); assert.ok(o1.equals(o2)); done(); }); it('with Buffer _ids', function(done) { - var n1 = new B({ _id: 0 }); - var n2 = new B({ _id: 0 }); + var n1 = new B({_id: 0}); + var n2 = new B({_id: 0}); assert.ok(n1.equals(n2)); done(); }); @@ -1416,7 +1570,7 @@ describe('document', function() { it('works with undefined (gh-1892)', function(done) { var d = new TestDocument(); d.nested.setr = undefined; - assert.equal('undefined setter', d.nested.setr); + assert.equal(d.nested.setr, 'undefined setter'); dateSetterCalled = false; d.date = undefined; d.validate(function(err) { @@ -1432,89 +1586,89 @@ describe('document', function() { var doc = new TestDocument(); doc.init({ - test : 'Test' - , nested : { - age : 5 + test: 'Test', + nested: { + age: 5 } }); - doc.set('nested', { path: 'overwrite the entire nested object' }); - assert.equal(undefined, doc.nested.age); - assert.equal(1, Object.keys(doc._doc.nested).length); - assert.equal('overwrite the entire nested object', doc.nested.path); + doc.set('nested', {path: 'overwrite the entire nested object'}); + assert.equal(doc.nested.age, undefined); + assert.equal(Object.keys(doc._doc.nested).length, 1); + assert.equal(doc.nested.path, 'overwrite the entire nested object'); assert.ok(doc.isModified('nested')); // vs merging using doc.set(object) - doc.set({ test: 'Test', nested: { age: 4 }}); - assert.equal('4overwrite the entire nested object', doc.nested.path); - assert.equal(4, doc.nested.age); - assert.equal(2, Object.keys(doc._doc.nested).length); + doc.set({test: 'Test', nested: {age: 4}}); + assert.equal(doc.nested.path, '4overwrite the entire nested object'); + assert.equal(doc.nested.age, 4); + assert.equal(Object.keys(doc._doc.nested).length, 2); assert.ok(doc.isModified('nested')); doc = new TestDocument(); doc.init({ - test : 'Test' - , nested : { - age : 5 + test: 'Test', + nested: { + age: 5 } }); // vs merging using doc.set(path, object, {merge: true}) - doc.set('nested', { path: 'did not overwrite the nested object' }, {merge: true}); - assert.equal('5did not overwrite the nested object', doc.nested.path); - assert.equal(5, doc.nested.age); - assert.equal(3, Object.keys(doc._doc.nested).length); + doc.set('nested', {path: 'did not overwrite the nested object'}, {merge: true}); + assert.equal(doc.nested.path, '5did not overwrite the nested object'); + assert.equal(doc.nested.age, 5); + assert.equal(Object.keys(doc._doc.nested).length, 3); assert.ok(doc.isModified('nested')); doc = new TestDocument(); doc.init({ - test : 'Test' - , nested : { - age : 5 + test: 'Test', + nested: { + age: 5 } }); - doc.set({ test: 'Test', nested: { age: 5 }}); + doc.set({test: 'Test', nested: {age: 5}}); assert.ok(!doc.isModified()); assert.ok(!doc.isModified('test')); assert.ok(!doc.isModified('nested')); assert.ok(!doc.isModified('nested.age')); - doc.nested = { path: 'overwrite the entire nested object', age: 5 }; - assert.equal(5, doc.nested.age); - assert.equal(2, Object.keys(doc._doc.nested).length); - assert.equal('5overwrite the entire nested object', doc.nested.path); + doc.nested = {path: 'overwrite the entire nested object', age: 5}; + assert.equal(doc.nested.age, 5); + assert.equal(Object.keys(doc._doc.nested).length, 2); + assert.equal(doc.nested.path, '5overwrite the entire nested object'); assert.ok(doc.isModified('nested')); - doc.nested.deep = { x: 'Hank and Marie' }; - assert.equal(3, Object.keys(doc._doc.nested).length); - assert.equal('5overwrite the entire nested object', doc.nested.path); + doc.nested.deep = {x: 'Hank and Marie'}; + assert.equal(Object.keys(doc._doc.nested).length, 3); + assert.equal(doc.nested.path, '5overwrite the entire nested object'); assert.ok(doc.isModified('nested')); - assert.equal('Hank and Marie', doc.nested.deep.x); + assert.equal(doc.nested.deep.x, 'Hank and Marie'); doc = new TestDocument(); doc.init({ - test : 'Test' - , nested : { - age : 5 + test: 'Test', + nested: { + age: 5 } }); - doc.set('nested.deep', { x: 'Hank and Marie' }); - assert.equal(2, Object.keys(doc._doc.nested).length); - assert.equal(1, Object.keys(doc._doc.nested.deep).length); + doc.set('nested.deep', {x: 'Hank and Marie'}); + assert.equal(Object.keys(doc._doc.nested).length, 2); + assert.equal(Object.keys(doc._doc.nested.deep).length, 1); assert.ok(doc.isModified('nested')); assert.ok(!doc.isModified('nested.path')); assert.ok(!doc.isModified('nested.age')); assert.ok(doc.isModified('nested.deep')); - assert.equal('Hank and Marie', doc.nested.deep.x); + assert.equal(doc.nested.deep.x, 'Hank and Marie'); done(); }); it('gh-1954', function(done) { var schema = new Schema({ - schedule: [ new Schema({open: Number, close: Number}) ] + schedule: [new Schema({open: Number, close: Number})] }); var M = mongoose.model('Blog', schema); @@ -1531,8 +1685,8 @@ describe('document', function() { assert.ok(doc.schedule); assert.ok(doc.schedule.isMongooseDocumentArray); assert.ok(doc.schedule[0] instanceof EmbeddedDocument); - assert.equal(1100, doc.schedule[0].open); - assert.equal(1900, doc.schedule[0].close); + assert.equal(doc.schedule[0].open, 1100); + assert.equal(doc.schedule[0].close, 1900); done(); }); @@ -1540,7 +1694,7 @@ describe('document', function() { describe('when overwriting with a document instance', function() { it('does not cause StackOverflows (gh-1234)', function(done) { - var doc = new TestDocument({ nested: { age: 35 }}); + var doc = new TestDocument({nested: {age: 35}}); doc.nested = doc.nested; assert.doesNotThrow(function() { doc.nested.age; @@ -1549,7 +1703,6 @@ describe('document', function() { }); }); }); - }); describe('virtual', function() { @@ -1558,7 +1711,7 @@ describe('document', function() { var M; before(function(done) { - var schema = new mongoose.Schema({ v: Number }); + var schema = new mongoose.Schema({v: Number}); schema.virtual('thang').set(function(v) { val = v; }); @@ -1570,22 +1723,22 @@ describe('document', function() { }); it('works with objects', function(done) { - new M({ thang: {}}); + new M({thang: {}}); assert.deepEqual({}, val); done(); }); it('works with arrays', function(done) { - new M({ thang: []}); + new M({thang: []}); assert.deepEqual([], val); done(); }); it('works with numbers', function(done) { - new M({ thang: 4}); + new M({thang: 4}); assert.deepEqual(4, val); done(); }); it('works with strings', function(done) { - new M({ thang: '3'}); + new M({thang: '3'}); assert.deepEqual('3', val); done(); }); @@ -1600,7 +1753,7 @@ describe('document', function() { var parent = new Parent({name: 'Hello'}); parent.save(function(err, parent) { assert.ifError(err); - parent.children.push( {counter: 0} ); + parent.children.push({counter: 0}); parent.save(function(err, parent) { assert.ifError(err); parent.children[0].counter += 1; @@ -1611,7 +1764,7 @@ describe('document', function() { assert.ifError(err); Parent.findOne({}, function(error, parent) { assert.ifError(error); - assert.equal(2, parent.children[0].counter); + assert.equal(parent.children[0].counter, 2); db.close(done); }); }); @@ -1624,14 +1777,14 @@ describe('document', function() { describe('gh-1933', function() { it('works', function(done) { var db = start(); - var M = db.model('gh1933', new Schema({ id: String, field: Number }), 'gh1933'); + var M = db.model('gh1933', new Schema({id: String, field: Number}), 'gh1933'); M.create({}, function(error) { assert.ifError(error); M.findOne({}, function(error, doc) { assert.ifError(error); doc.__v = 123; - doc.field = 5;//.push({ _id: '123', type: '456' }); + doc.field = 5; // .push({ _id: '123', type: '456' }); doc.save(function(error) { assert.ifError(error); db.close(done); @@ -1644,7 +1797,7 @@ describe('document', function() { describe('gh-1638', function() { it('works', function(done) { var ItemChildSchema = new mongoose.Schema({ - name: { type: String, required: true, default: "hello" } + name: {type: String, required: true, default: 'hello'} }); var ItemParentSchema = new mongoose.Schema({ @@ -1655,8 +1808,8 @@ describe('document', function() { var ItemParent = db.model('gh-1638-1', ItemParentSchema, 'gh-1638-1'); var ItemChild = db.model('gh-1638-2', ItemChildSchema, 'gh-1638-2'); - var c1 = new ItemChild({ name: 'first child' }); - var c2 = new ItemChild({ name: 'second child' }); + var c1 = new ItemChild({name: 'first child'}); + var c2 = new ItemChild({name: 'second child'}); var p = new ItemParent({ children: [c1, c2] @@ -1669,7 +1822,7 @@ describe('document', function() { p.children = [c2]; p.save(function(error, doc) { assert.ifError(error); - assert.equal(1, doc.children.length); + assert.equal(doc.children.length, 1); db.close(done); }); }); @@ -1686,7 +1839,7 @@ describe('document', function() { var db = start(); var Item = db.model('gh-2434', ItemSchema, 'gh-2434'); - var item = new Item({ st: 1 }); + var item = new Item({st: 1}); item.save(function(error) { assert.ifError(error); @@ -1697,7 +1850,7 @@ describe('document', function() { // item.st is 3 but may not be saved to DB Item.findById(item._id, function(error, doc) { assert.ifError(error); - assert.equal(3, doc.st); + assert.equal(doc.st, 3); db.close(done); }); }); @@ -1718,330 +1871,3359 @@ describe('document', function() { personSchema.queue('fn'); var Person = db.model('gh2856', personSchema, 'gh2856'); - new Person({ name: 'Val' }); + new Person({name: 'Val'}); assert.equal(calledName, 'Val'); db.close(done); }); - it('applies toJSON transform correctly for populated docs (gh-2910) (gh-2990)', function(done) { - var db = start(); - var parentSchema = mongoose.Schema({ - c: { type: mongoose.Schema.Types.ObjectId, ref: 'gh-2910-1' } - }); + describe('bug fixes', function() { + var db; - var called = []; - parentSchema.options.toJSON = { - transform: function(doc, ret) { - called.push(ret); - return ret; - } - }; + before(function() { + db = start(); + }); - var childSchema = mongoose.Schema({ - name: String + after(function(done) { + db.close(done); }); - var childCalled = []; - childSchema.options.toJSON = { - transform: function(doc, ret) { - childCalled.push(ret); - return ret; - } - }; + it('applies toJSON transform correctly for populated docs (gh-2910) (gh-2990)', function(done) { + var parentSchema = mongoose.Schema({ + c: {type: mongoose.Schema.Types.ObjectId, ref: 'gh-2910-1'} + }); - var Child = db.model('gh-2910-1', childSchema); - var Parent = db.model('gh-2910-0', parentSchema); - - Child.create({ name: 'test' }, function(error, c) { - Parent.create({ c: c._id }, function(error, p) { - Parent.findOne({ _id: p._id }).populate('c').exec(function(error, p) { - var doc = p.toJSON(); - assert.equal(called.length, 1); - assert.equal(called[0]._id.toString(), p._id.toString()); - assert.equal(doc._id.toString(), p._id.toString()); - assert.equal(childCalled.length, 1); - assert.equal(childCalled[0]._id.toString(), c._id.toString()); - - called = []; - childCalled = []; - - // JSON.stringify() passes field name, so make sure we don't treat - // that as a param to toJSON (gh-2990) - doc = JSON.parse(JSON.stringify({ parent: p })).parent; - assert.equal(called.length, 1); - assert.equal(called[0]._id.toString(), p._id.toString()); - assert.equal(doc._id.toString(), p._id.toString()); - assert.equal(childCalled.length, 1); - assert.equal(childCalled[0]._id.toString(), c._id.toString()); + var called = []; + parentSchema.options.toJSON = { + transform: function(doc, ret) { + called.push(ret); + return ret; + } + }; - db.close(done); - }); + var childSchema = mongoose.Schema({ + name: String }); - }); - }); - it('setters firing with objects on real paths (gh-2943)', function(done) { - var M = mongoose.model('gh2943', { - myStr: { - type: String, set: function(v) { return v.value; } - }, - otherStr: String - }); + var childCalled = []; + childSchema.options.toJSON = { + transform: function(doc, ret) { + childCalled.push(ret); + return ret; + } + }; - var t = new M({ myStr: { value: 'test' } }); - assert.equal(t.myStr, 'test'); + var Child = db.model('gh-2910-1', childSchema); + var Parent = db.model('gh-2910-0', parentSchema); + + Child.create({name: 'test'}, function(error, c) { + Parent.create({c: c._id}, function(error, p) { + Parent.findOne({_id: p._id}).populate('c').exec(function(error, p) { + var doc = p.toJSON(); + assert.equal(called.length, 1); + assert.equal(called[0]._id.toString(), p._id.toString()); + assert.equal(doc._id.toString(), p._id.toString()); + assert.equal(childCalled.length, 1); + assert.equal(childCalled[0]._id.toString(), c._id.toString()); + + called = []; + childCalled = []; + + // JSON.stringify() passes field name, so make sure we don't treat + // that as a param to toJSON (gh-2990) + doc = JSON.parse(JSON.stringify({parent: p})).parent; + assert.equal(called.length, 1); + assert.equal(called[0]._id.toString(), p._id.toString()); + assert.equal(doc._id.toString(), p._id.toString()); + assert.equal(childCalled.length, 1); + assert.equal(childCalled[0]._id.toString(), c._id.toString()); - new M({ otherStr: { value: 'test' } }); - assert.ok(!t.otherStr); + done(); + }); + }); + }); + }); - done(); - }); + it('single nested schema transform with save() (gh-5807)', function() { + var embeddedSchema = new Schema({ + test: String + }); - describe('gh-2782', function() { - it('should set data from a sub doc', function(done) { - var schema1 = new mongoose.Schema({ + var called = false; + embeddedSchema.options.toObject = { + transform: function(doc, ret) { + called = true; + delete ret.test; + return ret; + } + }; + var topLevelSchema = new Schema({ + embedded: embeddedSchema + }); + var MyModel = db.model('gh5807', topLevelSchema); + + return MyModel.create({}). + then(function(doc) { + doc.embedded = { test: '123' }; + return doc.save(); + }). + then(function(doc) { + return MyModel.findById(doc._id); + }). + then(function(doc) { + assert.equal(doc.embedded.test, '123'); + assert.ok(!called); + }); + }); + + it('setters firing with objects on real paths (gh-2943)', function(done) { + var M = mongoose.model('gh2943', { + myStr: { + type: String, set: function(v) { + return v.value; + } + }, + otherStr: String + }); + + var t = new M({myStr: {value: 'test'}}); + assert.equal(t.myStr, 'test'); + + new M({otherStr: {value: 'test'}}); + assert.ok(!t.otherStr); + + done(); + }); + + describe('gh-2782', function() { + it('should set data from a sub doc', function(done) { + var schema1 = new mongoose.Schema({ + data: { + email: String + } + }); + var schema2 = new mongoose.Schema({ + email: String + }); + var Model1 = mongoose.model('gh-2782-1', schema1); + var Model2 = mongoose.model('gh-2782-2', schema2); + + var doc1 = new Model1({'data.email': 'some@example.com'}); + assert.equal(doc1.data.email, 'some@example.com'); + var doc2 = new Model2(); + doc2.set(doc1.data); + assert.equal(doc2.email, 'some@example.com'); + done(); + }); + }); + + it('set data from subdoc keys (gh-3346)', function(done) { + var schema1 = new mongoose.Schema({ data: { email: String } }); - var schema2 = new mongoose.Schema({ - email: String - }); - var Model1 = mongoose.model('gh-2782-1', schema1); - var Model2 = mongoose.model('gh-2782-2', schema2); + var Model1 = mongoose.model('gh3346', schema1); - var doc1 = new Model1({ 'data.email': 'some@example.com' }); + var doc1 = new Model1({'data.email': 'some@example.com'}); assert.equal(doc1.data.email, 'some@example.com'); - var doc2 = new Model2(); - doc2.set(doc1.data); - assert.equal(doc2.email, 'some@example.com'); + var doc2 = new Model1({data: doc1.data}); + assert.equal(doc2.data.email, 'some@example.com'); done(); }); - }); - it('set data from subdoc keys (gh-3346)', function(done) { - var schema1 = new mongoose.Schema({ - data: { - email: String - } + it('doesnt attempt to cast generic objects as strings (gh-3030)', function(done) { + var M = mongoose.model('gh3030', { + myStr: { + type: String + } + }); + + var t = new M({myStr: {thisIs: 'anObject'}}); + assert.ok(!t.myStr); + t.validate(function(error) { + assert.ok(error); + done(); + }); }); - var Model1 = mongoose.model('gh3346', schema1); - var doc1 = new Model1({ 'data.email': 'some@example.com' }); - assert.equal(doc1.data.email, 'some@example.com'); - var doc2 = new Model1({ data: doc1.data }); - assert.equal(doc2.data.email, 'some@example.com'); - done(); - }); + it('single embedded schemas 1 (gh-2689)', function(done) { + var userSchema = new mongoose.Schema({ + name: String, + email: String + }, {_id: false, id: false}); - it('doesnt attempt to cast generic objects as strings (gh-3030)', function(done) { - var M = mongoose.model('gh3030', { - myStr: { - type: String - } + var userHookCount = 0; + userSchema.pre('save', function(next) { + ++userHookCount; + next(); + }); + + var eventSchema = new mongoose.Schema({ + user: userSchema, + name: String + }); + + var eventHookCount = 0; + eventSchema.pre('save', function(next) { + ++eventHookCount; + next(); + }); + + var Event = db.model('gh2689', eventSchema); + + var e = new Event({name: 'test', user: {name: 123, email: 'val'}}); + e.save(function(error) { + assert.ifError(error); + assert.strictEqual(e.user.name, '123'); + assert.equal(eventHookCount, 1); + assert.equal(userHookCount, 1); + + Event.findOne( + {user: {name: '123', email: 'val'}}, + function(error, doc) { + assert.ifError(error); + assert.ok(doc); + + Event.findOne( + {user: {$in: [{name: '123', email: 'val'}]}}, + function(error, doc) { + assert.ifError(error); + assert.ok(doc); + done(); + }); + }); + }); }); - var t = new M({ myStr: { thisIs: 'anObject' } }); - assert.ok(!t.myStr); - t.validate(function(error) { + it('single embedded schemas with validation (gh-2689)', function(done) { + var userSchema = new mongoose.Schema({ + name: String, + email: {type: String, required: true, match: /.+@.+/} + }, {_id: false, id: false}); + + var eventSchema = new mongoose.Schema({ + user: userSchema, + name: String + }); + + var Event = db.model('gh2689_1', eventSchema); + + var e = new Event({name: 'test', user: {}}); + var error = e.validateSync(); + assert.ok(error); + assert.ok(error.errors['user.email']); + assert.equal(error.errors['user.email'].kind, 'required'); + + e.user.email = 'val'; + error = e.validateSync(); + assert.ok(error); + assert.ok(error.errors['user.email']); + assert.equal(error.errors['user.email'].kind, 'regexp'); + done(); }); - }); - it('single embedded schemas (gh-2689)', function(done) { - var db = start(); + it('single embedded parent() (gh-5134)', function(done) { + var userSchema = new mongoose.Schema({ + name: String, + email: {type: String, required: true, match: /.+@.+/} + }, {_id: false, id: false}); - var userSchema = new mongoose.Schema({ - name: String, - email: String - }, { _id: false, id: false }); + var eventSchema = new mongoose.Schema({ + user: userSchema, + name: String + }); - var userHookCount = 0; - userSchema.pre('save', function(next) { - ++userHookCount; - next(); - }); + var Event = db.model('gh5134', eventSchema); - var eventSchema = new mongoose.Schema({ - user: userSchema, - name: String - }); + var e = new Event({name: 'test', user: {}}); + assert.strictEqual(e.user.parent(), e.user.ownerDocument()); - var eventHookCount = 0; - eventSchema.pre('save', function(next) { - ++eventHookCount; - next(); + done(); }); - var Event = db.model('gh2689', eventSchema); + it('single embedded schemas with markmodified (gh-2689)', function(done) { + var userSchema = new mongoose.Schema({ + name: String, + email: {type: String, required: true, match: /.+@.+/} + }, {_id: false, id: false}); - var e = new Event({ name: 'test', user: { name: 123, email: 'val' } }); - e.save(function(error) { - assert.ifError(error); - assert.strictEqual(e.user.name, '123'); - assert.equal(eventHookCount, 1); - assert.equal(userHookCount, 1); + var eventSchema = new mongoose.Schema({ + user: userSchema, + name: String + }); + + var Event = db.model('gh2689_2', eventSchema); + + var e = new Event({name: 'test', user: {email: 'a@b'}}); + e.save(function(error, doc) { + assert.ifError(error); + assert.ok(doc); + assert.ok(!doc.isModified('user')); + assert.ok(!doc.isModified('user.email')); + assert.ok(!doc.isModified('user.name')); + doc.user.name = 'Val'; + assert.ok(doc.isModified('user')); + assert.ok(!doc.isModified('user.email')); + assert.ok(doc.isModified('user.name')); + + var delta = doc.$__delta()[1]; + assert.deepEqual(delta, { + $set: {'user.name': 'Val'} + }); - Event.findOne( - { user: { name: '123', email: 'val' } }, - function(error, doc) { + doc.save(function(error) { assert.ifError(error); - assert.ok(doc); + Event.findOne({_id: doc._id}, function(error, doc) { + assert.ifError(error); + assert.deepEqual(doc.user.toObject(), {email: 'a@b', name: 'Val'}); + done(); + }); + }); + }); + }); - Event.findOne( - { user: { $in: [{ name: '123', email: 'val' }] } }, - function(error, doc) { - assert.ifError(error); - assert.ok(doc); - db.close(done); - }); + it('single embedded schemas + update validators (gh-2689)', function(done) { + var userSchema = new mongoose.Schema({ + name: {type: String, default: 'Val'}, + email: {type: String, required: true, match: /.+@.+/} + }, {_id: false, id: false}); + + var eventSchema = new mongoose.Schema({ + user: userSchema, + name: String + }); + + var Event = db.model('gh2689_3', eventSchema); + + var badUpdate = {$set: {'user.email': 'a'}}; + var options = {runValidators: true}; + Event.update({}, badUpdate, options, function(error) { + assert.ok(error); + assert.equal(error.errors['user.email'].kind, 'regexp'); + + var nestedUpdate = {name: 'test'}; + var options = {upsert: true, setDefaultsOnInsert: true}; + Event.update({}, nestedUpdate, options, function(error) { + assert.ifError(error); + Event.findOne({name: 'test'}, function(error, ev) { + assert.ifError(error); + assert.equal(ev.user.name, 'Val'); + done(); + }); }); + }); }); }); - it('single embedded schemas with validation (gh-2689)', function(done) { - var db = start(); + describe('error processing (gh-2284)', function() { + var db; - var userSchema = new mongoose.Schema({ - name: String, - email: { type: String, required: true, match: /.+@.+/ } - }, { _id: false, id: false }); + before(function() { + db = start(); + }); - var eventSchema = new mongoose.Schema({ - user: userSchema, - name: String + after(function(done) { + db.close(done); }); - var Event = db.model('gh2689_1', eventSchema); + it('save errors', function(done) { + var schema = new Schema({ + name: { type: String, required: true } + }); - var e = new Event({ name: 'test', user: {} }); - var error = e.validateSync(); - assert.ok(error); - assert.ok(error.errors['user.email']); - assert.equal(error.errors['user.email'].kind, 'required'); + schema.post('save', function(error, doc, next) { + assert.ok(doc instanceof Model); + next(new Error('Catch all')); + }); - e.user.email = 'val'; - error = e.validateSync(); + schema.post('save', function(error, doc, next) { + assert.ok(doc instanceof Model); + next(new Error('Catch all #2')); + }); - assert.ok(error); - assert.ok(error.errors['user.email']); - assert.equal(error.errors['user.email'].kind, 'regexp'); + var Model = mongoose.model('gh2284', schema); - done(); + Model.create({}, function(error) { + assert.ok(error); + assert.equal(error.message, 'Catch all #2'); + done(); + }); + }); + + it('validate errors (gh-4885)', function(done) { + var testSchema = new Schema({ title: { type: String, required: true } }); + + var called = 0; + testSchema.post('validate', function(error, doc, next) { + ++called; + next(error); + }); + + var Test = db.model('gh4885', testSchema); + + Test.create({}, function(error) { + assert.ok(error); + assert.equal(called, 1); + done(); + }); + }); + + it('handles non-errors', function(done) { + var schema = new Schema({ + name: { type: String, required: true } + }); + + schema.post('save', function(error, doc, next) { + next(new Error('Catch all')); + }); + + schema.post('save', function(error, doc, next) { + next(new Error('Catch all #2')); + }); + + var Model = db.model('gh2284_1', schema); + + Model.create({ name: 'test' }, function(error) { + assert.ifError(error); + done(); + }); + }); }); - it('single embedded schemas with markmodified (gh-2689)', function(done) { - var db = start(); + describe('bug fixes', function() { + var db; - var userSchema = new mongoose.Schema({ - name: String, - email: { type: String, required: true, match: /.+@.+/ } - }, { _id: false, id: false }); + before(function() { + db = start(); + }); - var eventSchema = new mongoose.Schema({ - user: userSchema, - name: String + after(function(done) { + db.close(done); }); - var Event = db.model('gh2689_2', eventSchema); + it('single embedded schemas with populate (gh-3501)', function(done) { + var PopulateMeSchema = new Schema({}); - var e = new Event({ name: 'test', user: { email: 'a@b' } }); - e.save(function(error, doc) { - assert.ifError(error); - assert.ok(doc); - assert.ok(!doc.isModified('user')); - assert.ok(!doc.isModified('user.email')); - assert.ok(!doc.isModified('user.name')); - doc.user.name = 'Val'; - assert.ok(doc.isModified('user')); - assert.ok(!doc.isModified('user.email')); - assert.ok(doc.isModified('user.name')); + var Child = db.model('gh3501', PopulateMeSchema); - var delta = doc.$__delta()[1]; - assert.deepEqual(delta, { - $set: { 'user.name': 'Val' } + var SingleNestedSchema = new Schema({ + populateMeArray: [{ + type: Schema.Types.ObjectId, + ref: 'gh3501' + }] }); - doc.save(function(error) { + var parentSchema = new Schema({ + singleNested: SingleNestedSchema + }); + + var P = db.model('gh3501_1', parentSchema); + + Child.create([{}, {}], function(error, docs) { assert.ifError(error); - Event.findOne({ _id: doc._id }, function(error, doc) { + var obj = { + singleNested: {populateMeArray: [docs[0]._id, docs[1]._id]} + }; + P.create(obj, function(error, doc) { assert.ifError(error); - assert.deepEqual(doc.user.toObject(), { email: 'a@b', name: 'Val' }); - db.close(done); + P. + findById(doc._id). + populate('singleNested.populateMeArray'). + exec(function(error, doc) { + assert.ok(doc.singleNested.populateMeArray[0]._id); + done(); + }); }); }); }); - }); - it('single embedded schemas + update validators (gh-2689)', function(done) { - var db = start(); + it('single embedded schemas with methods (gh-3534)', function(done) { + var personSchema = new Schema({name: String}); + personSchema.methods.firstName = function() { + return this.name.substr(0, this.name.indexOf(' ')); + }; - var userSchema = new mongoose.Schema({ - name: { type: String, default: 'Val' }, - email: { type: String, required: true, match: /.+@.+/ } - }, { _id: false, id: false }); + var bandSchema = new Schema({leadSinger: personSchema}); + var Band = db.model('gh3534', bandSchema); - var eventSchema = new mongoose.Schema({ - user: userSchema, - name: String + var gnr = new Band({leadSinger: {name: 'Axl Rose'}}); + assert.equal(gnr.leadSinger.firstName(), 'Axl'); + done(); }); - var Event = db.model('gh2689_3', eventSchema); + it('single embedded schemas with models (gh-3535)', function(done) { + var db = start(); + var personSchema = new Schema({name: String}); + var Person = db.model('gh3535_0', personSchema); - var badUpdate = { $set: { 'user.email': 'a' } }; - var options = { runValidators: true }; - Event.update({}, badUpdate, options, function(error) { - assert.ok(error); - assert.equal(error.errors['user.email'].kind, 'regexp'); + var bandSchema = new Schema({leadSinger: personSchema}); + var Band = db.model('gh3535', bandSchema); + + var axl = new Person({name: 'Axl Rose'}); + var gnr = new Band({leadSinger: axl}); + + gnr.save(function(error) { + assert.ifError(error); + assert.equal(gnr.leadSinger.name, 'Axl Rose'); + done(); + }); + }); + + it('single embedded schemas with indexes (gh-3594)', function(done) { + var personSchema = new Schema({name: {type: String, unique: true}}); + + var bandSchema = new Schema({leadSinger: personSchema}); + + assert.equal(bandSchema.indexes().length, 1); + var index = bandSchema.indexes()[0]; + assert.deepEqual(index[0], {'leadSinger.name': 1}); + assert.ok(index[1].unique); + done(); + }); + + it('removing single embedded docs (gh-3596)', function(done) { + var personSchema = new Schema({name: String}); + + var bandSchema = new Schema({guitarist: personSchema, name: String}); + var Band = db.model('gh3596', bandSchema); - var nestedUpdate = { name: 'test' }; - var options = { upsert: true, setDefaultsOnInsert: true }; - Event.update({}, nestedUpdate, options, function(error) { + var gnr = new Band({ + name: 'Guns N\' Roses', + guitarist: {name: 'Slash'} + }); + gnr.save(function(error, gnr) { assert.ifError(error); - Event.findOne({ name: 'test' }, function(error, ev) { + gnr.guitarist = undefined; + gnr.save(function(error, gnr) { assert.ifError(error); - assert.equal(ev.user.name, 'Val'); - db.close(done); + assert.ok(!gnr.guitarist); + done(); }); }); }); - }); - it('single embedded schemas with populate (gh-3501)', function(done) { - var db = start(); - var PopulateMeSchema = new Schema({ + it('setting single embedded docs (gh-3601)', function(done) { + var personSchema = new Schema({name: String}); + + var bandSchema = new Schema({guitarist: personSchema, name: String}); + var Band = db.model('gh3601', bandSchema); + + var gnr = new Band({ + name: 'Guns N\' Roses', + guitarist: {name: 'Slash'} + }); + var velvetRevolver = new Band({ + name: 'Velvet Revolver' + }); + velvetRevolver.guitarist = gnr.guitarist; + velvetRevolver.save(function(error) { + assert.ifError(error); + assert.equal(velvetRevolver.guitarist, gnr.guitarist); + done(); + }); }); - var Child = db.model('gh3501', PopulateMeSchema); + it('single embedded docs init obeys strict mode (gh-3642)', function(done) { + var personSchema = new Schema({name: String}); - var SingleNestedSchema = new Schema({ - populateMeArray: [{ - type: Schema.Types.ObjectId, - ref: 'gh3501' - }] + var bandSchema = new Schema({guitarist: personSchema, name: String}); + var Band = db.model('gh3642', bandSchema); + + var velvetRevolver = new Band({ + name: 'Velvet Revolver', + guitarist: {name: 'Slash', realName: 'Saul Hudson'} + }); + + velvetRevolver.save(function(error) { + assert.ifError(error); + var query = {name: 'Velvet Revolver'}; + Band.collection.findOne(query, function(error, band) { + assert.ifError(error); + assert.ok(!band.guitarist.realName); + done(); + }); + }); }); - var parentSchema = new Schema({ - singleNested: SingleNestedSchema + it('single embedded docs post hooks (gh-3679)', function(done) { + var postHookCalls = []; + var personSchema = new Schema({name: String}); + personSchema.post('save', function() { + postHookCalls.push(this); + }); + + var bandSchema = new Schema({guitarist: personSchema, name: String}); + var Band = db.model('gh3679', bandSchema); + var obj = {name: 'Guns N\' Roses', guitarist: {name: 'Slash'}}; + + Band.create(obj, function(error) { + assert.ifError(error); + setTimeout(function() { + assert.equal(postHookCalls.length, 1); + assert.equal(postHookCalls[0].name, 'Slash'); + done(); + }); + }); }); - var P = db.model('gh3501_1', parentSchema); + it('single embedded docs .set() (gh-3686)', function(done) { + var personSchema = new Schema({name: String, realName: String}); - Child.create([{}, {}], function(error, docs) { - assert.ifError(error); + var bandSchema = new Schema({ + guitarist: personSchema, + name: String + }); + var Band = db.model('gh3686', bandSchema); var obj = { - singleNested: { populateMeArray: [docs[0]._id, docs[1]._id] } + name: 'Guns N\' Roses', + guitarist: {name: 'Slash', realName: 'Saul Hudson'} }; - P.create(obj, function(error, doc) { + + Band.create(obj, function(error, gnr) { + gnr.set('guitarist.name', 'Buckethead'); + gnr.save(function(error) { + assert.ifError(error); + assert.equal(gnr.guitarist.name, 'Buckethead'); + assert.equal(gnr.guitarist.realName, 'Saul Hudson'); + done(); + }); + }); + }); + + it('single embedded docs with arrays pre hooks (gh-3680)', function(done) { + var childSchema = new Schema({count: Number}); + + var preCalls = 0; + childSchema.pre('save', function(next) { + ++preCalls; + next(); + }); + + var SingleNestedSchema = new Schema({ + children: [childSchema] + }); + + var ParentSchema = new Schema({ + singleNested: SingleNestedSchema + }); + + var Parent = db.model('gh3680', ParentSchema); + var obj = {singleNested: {children: [{count: 0}]}}; + Parent.create(obj, function(error) { assert.ifError(error); - P. - findById(doc._id). - populate('singleNested.populateMeArray'). - exec(function(error, doc) { - assert.ok(doc.singleNested.populateMeArray[0]._id); - db.close(done); + assert.equal(preCalls, 1); + done(); + }); + }); + + it('nested single embedded doc validation (gh-3702)', function(done) { + var childChildSchema = new Schema({count: {type: Number, min: 1}}); + var childSchema = new Schema({child: childChildSchema}); + var parentSchema = new Schema({child: childSchema}); + + var Parent = db.model('gh3702', parentSchema); + var obj = {child: {child: {count: 0}}}; + Parent.create(obj, function(error) { + assert.ok(error); + assert.ok(/ValidationError/.test(error.toString())); + done(); + }); + }); + + it('handles virtuals with dots correctly (gh-3618)', function(done) { + var testSchema = new Schema({nested: {type: Object, default: {}}}); + testSchema.virtual('nested.test').get(function() { + return true; + }); + + var Test = db.model('gh3618', testSchema); + + var test = new Test(); + + var doc = test.toObject({getters: true, virtuals: true}); + delete doc._id; + delete doc.id; + assert.deepEqual(doc, {nested: {test: true}}); + + doc = test.toObject({getters: false, virtuals: true}); + delete doc._id; + delete doc.id; + assert.deepEqual(doc, {nested: {test: true}}); + done(); + }); + + it('handles pushing with numeric keys (gh-3623)', function(done) { + var schema = new Schema({ + array: [{ + 1: { + date: Date + }, + 2: { + date: Date + }, + 3: { + date: Date + } + }] + }); + + var MyModel = db.model('gh3623', schema); + + var doc = {array: [{2: {}}]}; + MyModel.collection.insertOne(doc, function(error) { + assert.ifError(error); + + MyModel.findOne({_id: doc._id}, function(error, doc) { + assert.ifError(error); + doc.array.push({2: {}}); + doc.save(function(error) { + assert.ifError(error); + done(); }); + }); + }); + }); + + it('execPopulate (gh-3753)', function(done) { + var childSchema = new Schema({ + name: String + }); + + var parentSchema = new Schema({ + name: String, + children: [{type: ObjectId, ref: 'gh3753'}] + }); + + var Child = db.model('gh3753', childSchema); + var Parent = db.model('gh3753_0', parentSchema); + + Child.create({name: 'Luke Skywalker'}, function(error, child) { + assert.ifError(error); + var doc = {name: 'Darth Vader', children: [child._id]}; + Parent.create(doc, function(error, doc) { + Parent.findOne({_id: doc._id}, function(error, doc) { + assert.ifError(error); + assert.ok(doc); + doc.populate('children').execPopulate().then(function(doc) { + assert.equal(doc.children.length, 1); + assert.equal(doc.children[0].name, 'Luke Skywalker'); + done(); + }); + }); + }); + }); + }); + + it('handles 0 for numeric subdoc ids (gh-3776)', function(done) { + var personSchema = new Schema({ + _id: Number, + name: String, + age: Number, + friends: [{type: Number, ref: 'gh3776'}] + }); + + var Person = db.model('gh3776', personSchema); + + var people = [ + {_id: 0, name: 'Alice'}, + {_id: 1, name: 'Bob'} + ]; + + Person.create(people, function(error, people) { + assert.ifError(error); + var alice = people[0]; + alice.friends.push(people[1]); + alice.save(function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('handles conflicting names (gh-3867)', function(done) { + var testSchema = new Schema({ + name: { + type: String, + required: true + }, + things: [{ + name: { + type: String, + required: true + } + }] + }); + + var M = mongoose.model('gh3867', testSchema); + + var doc = M({ + things: [{}] + }); + + var fields = Object.keys(doc.validateSync().errors).sort(); + assert.deepEqual(fields, ['name', 'things.0.name']); + done(); + }); + + it('populate with lean (gh-3873)', function(done) { + var companySchema = new mongoose.Schema({ + name: String, + description: String, + userCnt: { type: Number, default: 0, select: false } + }); + + var userSchema = new mongoose.Schema({ + name: String, + company: { type: mongoose.Schema.Types.ObjectId, ref: 'gh3873' } + }); + + var Company = db.model('gh3873', companySchema); + var User = db.model('gh3873_0', userSchema); + + var company = new Company({ name: 'IniTech', userCnt: 1 }); + var user = new User({ name: 'Peter', company: company._id }); + + company.save(function(error) { + assert.ifError(error); + user.save(function(error) { + assert.ifError(error); + next(); + }); + }); + + function next() { + var pop = { path: 'company', select: 'name', options: { lean: true } }; + User.find({}).populate(pop).exec(function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 1); + assert.strictEqual(docs[0].company.userCnt, undefined); + done(); + }); + } + }); + + it('init single nested subdoc with select (gh-3880)', function(done) { + var childSchema = new mongoose.Schema({ + name: { type: String }, + friends: [{ type: String }] + }); + + var parentSchema = new mongoose.Schema({ + name: { type: String }, + child: childSchema + }); + + var Parent = db.model('gh3880', parentSchema); + var p = new Parent({ + name: 'Mufasa', + child: { + name: 'Simba', + friends: ['Pumbaa', 'Timon', 'Nala'] + } + }); + + p.save(function(error) { + assert.ifError(error); + var fields = 'name child.name'; + Parent.findById(p._id).select(fields).exec(function(error, doc) { + assert.ifError(error); + assert.strictEqual(doc.child.friends, void 0); + done(); + }); + }); + }); + + it('single nested subdoc isModified() (gh-3910)', function(done) { + var called = 0; + + var ChildSchema = new Schema({ + name: String + }); + + ChildSchema.pre('save', function(next) { + assert.ok(this.isModified('name')); + ++called; + next(); + }); + + var ParentSchema = new Schema({ + name: String, + child: ChildSchema + }); + + var Parent = db.model('gh3910', ParentSchema); + + var p = new Parent({ + name: 'Darth Vader', + child: { + name: 'Luke Skywalker' + } + }); + + p.save(function(error) { + assert.ifError(error); + assert.strictEqual(called, 1); + done(); + }); + }); + + it('pre and post as schema keys (gh-3902)', function(done) { + var schema = new mongoose.Schema({ + pre: String, + post: String + }, { versionKey: false }); + var MyModel = db.model('gh3902', schema); + + MyModel.create({ pre: 'test', post: 'test' }, function(error, doc) { + assert.ifError(error); + assert.deepEqual(_.omit(doc.toObject(), '_id'), + { pre: 'test', post: 'test' }); + done(); + }); + }); + + it('manual population and isNew (gh-3982)', function(done) { + var NestedModelSchema = new mongoose.Schema({ + field: String + }); + + var NestedModel = db.model('gh3982', NestedModelSchema); + + var ModelSchema = new mongoose.Schema({ + field: String, + array: [{ + type: mongoose.Schema.ObjectId, + ref: 'gh3982', + required: true + }] + }); + + var Model = db.model('gh3982_0', ModelSchema); + + var nestedModel = new NestedModel({ + 'field': 'nestedModel' + }); + + nestedModel.save(function(error, nestedModel) { + assert.ifError(error); + Model.create({ array: [nestedModel._id] }, function(error, doc) { + assert.ifError(error); + Model.findById(doc._id).populate('array').exec(function(error, doc) { + assert.ifError(error); + doc.array.push(nestedModel); + assert.strictEqual(doc.isNew, false); + assert.strictEqual(doc.array[0].isNew, false); + assert.strictEqual(doc.array[1].isNew, false); + assert.strictEqual(nestedModel.isNew, false); + done(); + }); + }); + }); + }); + + it('doesnt skipId for single nested subdocs (gh-4008)', function(done) { + var childSchema = new Schema({ + name: String + }); + + var parentSchema = new Schema({ + child: childSchema + }); + + var Parent = db.model('gh4008', parentSchema); + + Parent.create({ child: { name: 'My child' } }, function(error, doc) { + assert.ifError(error); + Parent.collection.findOne({ _id: doc._id }, function(error, doc) { + assert.ifError(error); + assert.ok(doc.child._id); + done(); + }); + }); + }); + + it('single embedded docs with $near (gh-4014)', function(done) { + var schema = new mongoose.Schema({ + placeName: String + }); + + var geoSchema = new mongoose.Schema({ + type: { + type: String, + enum: 'Point', + default: 'Point' + }, + coordinates: { + type: [Number], + default: [0, 0] + } + }); + + schema.add({ geo: geoSchema }); + schema.index({ geo: '2dsphere' }); + + var MyModel = db.model('gh4014', schema); + + MyModel.on('index', function(err) { + assert.ifError(err); + + MyModel. + where('geo').near({ center: [50, 50], spherical: true }). + exec(function(err) { + assert.ifError(err); + done(); + }); + }); + }); + + it('skip validation if required returns false (gh-4094)', function(done) { + var schema = new Schema({ + div: { + type: Number, + required: function() { return false; }, + validate: function(v) { return !!v; } + } + }); + var Model = db.model('gh4094', schema); + var m = new Model(); + assert.ifError(m.validateSync()); + done(); + }); + + it('ability to overwrite array default (gh-4109)', function(done) { + var schema = new Schema({ + names: { + type: [String], + default: void 0 + } + }); + + var Model = db.model('gh4109', schema); + var m = new Model(); + assert.ok(!m.names); + m.save(function(error, m) { + assert.ifError(error); + Model.collection.findOne({ _id: m._id }, function(error, doc) { + assert.ifError(error); + assert.ok(!('names' in doc)); + done(); + }); + }); + }); + + it('validation works when setting array index (gh-3816)', function(done) { + var mySchema = new mongoose.Schema({ + items: [ + { month: Number, date: Date } + ] + }); + + var Test = db.model('test', mySchema); + + var a = [ + { month: 0, date: new Date() }, + { month: 1, date: new Date() } + ]; + Test.create({ items: a }, function(error, doc) { + assert.ifError(error); + Test.findById(doc._id).exec(function(error, doc) { + assert.ifError(error); + assert.ok(doc); + doc.items[0] = { + month: 5, + date : new Date() + }; + doc.markModified('items'); + doc.save(function(error) { + assert.ifError(error); + done(); + }); + }); + }); + }); + + it('validateSync works when setting array index nested (gh-5389)', function(done) { + var childSchema = new mongoose.Schema({ + _id: false, + name: String, + age: Number + }); + + var schema = new mongoose.Schema({ + name: String, + children: [childSchema] + }); + + var Model = db.model('gh5389', schema); + + Model. + create({ + name: 'test', + children: [ + { name: 'test-child', age: 24 } + ] + }). + then(function(doc) { + return Model.findById(doc._id); + }). + then(function(doc) { + doc.children[0] = { name: 'updated-child', age: 53 }; + var errors = doc.validateSync(); + assert.ok(!errors); + done(); + }). + catch(done); + }); + + it('single embedded with defaults have $parent (gh-4115)', function(done) { + var ChildSchema = new Schema({ + name: { + type: String, + 'default': 'child' + } + }); + + var ParentSchema = new Schema({ + child: { + type: ChildSchema, + 'default': {} + } + }); + + var Parent = db.model('gh4115', ParentSchema); + + var p = new Parent(); + assert.equal(p.child.$parent, p); + done(); + }); + + it('removing parent doc calls remove hooks on subdocs (gh-2348) (gh-4566)', function(done) { + var ChildSchema = new Schema({ + name: String + }); + + var called = {}; + ChildSchema.pre('remove', function(next) { + called[this.name] = true; + next(); + }); + + var ParentSchema = new Schema({ + children: [ChildSchema], + child: ChildSchema + }); + + var Parent = db.model('gh2348', ParentSchema); + + var doc = { + children: [{ name: 'Jacen' }, { name: 'Jaina' }], + child: { name: 'Anakin' } + }; + Parent.create(doc, function(error, doc) { + assert.ifError(error); + doc.remove(function(error, doc) { + assert.ifError(error); + assert.deepEqual(called, { + Jacen: true, + Jaina: true, + Anakin: true + }); + var arr = doc.children.toObject().map(function(v) { return v.name; }); + assert.deepEqual(arr, ['Jacen', 'Jaina']); + assert.equal(doc.child.name, 'Anakin'); + done(); + }); + }); + }); + + it('strings of length 12 are valid oids (gh-3365)', function(done) { + var schema = new Schema({ myId: mongoose.Schema.Types.ObjectId }); + var M = db.model('gh3365', schema); + var doc = new M({ myId: 'blablablabla' }); + doc.validate(function(error) { + assert.ifError(error); + done(); + }); + }); + + it('set() empty obj unmodifies subpaths (gh-4182)', function(done) { + var omeletteSchema = new Schema({ + topping: { + meat: { + type: String, + enum: ['bacon', 'sausage'] + }, + cheese: Boolean + } + }); + var Omelette = db.model('gh4182', omeletteSchema); + var doc = new Omelette({ + topping: { + meat: 'bacon', + cheese: true + } + }); + doc.topping = {}; + doc.save(function(error) { + assert.ifError(error); + assert.strictEqual(doc.topping.meat, void 0); + done(); + }); + }); + + it('emits cb errors on model for save (gh-3499)', function(done) { + var testSchema = new Schema({ name: String }); + + var Test = db.model('gh3499', testSchema); + + Test.on('error', function(error) { + assert.equal(error.message, 'fail!'); + done(); + }); + + new Test({}).save(function() { + throw new Error('fail!'); + }); + }); + + it('emits cb errors on model for save with hooks (gh-3499)', function(done) { + var testSchema = new Schema({ name: String }); + + testSchema.pre('save', function(next) { + next(); + }); + + testSchema.post('save', function(doc, next) { + next(); + }); + + var Test = db.model('gh3499_0', testSchema); + + Test.on('error', function(error) { + assert.equal(error.message, 'fail!'); + done(); + }); + + new Test({}).save(function() { + throw new Error('fail!'); + }); + }); + + it('emits cb errors on model for find() (gh-3499)', function(done) { + var testSchema = new Schema({ name: String }); + + var Test = db.model('gh3499_1', testSchema); + + Test.on('error', function(error) { + assert.equal(error.message, 'fail!'); + done(); + }); + + Test.find({}, function() { + throw new Error('fail!'); + }); + }); + + it('emits cb errors on model for find() + hooks (gh-3499)', function(done) { + var testSchema = new Schema({ name: String }); + + testSchema.post('find', function(results, next) { + assert.equal(results.length, 0); + next(); + }); + + var Test = db.model('gh3499_2', testSchema); + + Test.on('error', function(error) { + assert.equal(error.message, 'fail!'); + done(); + }); + + Test.find({}, function() { + throw new Error('fail!'); + }); + }); + + it('clears subpaths when removing single nested (gh-4216)', function(done) { + var RecurrenceSchema = new Schema({ + frequency: Number, + interval: { + type: String, + enum: ['days', 'weeks', 'months', 'years'] + } + }, { _id: false }); + + var EventSchema = new Schema({ + name: { + type: String, + trim: true + }, + recurrence: RecurrenceSchema + }); + + var Event = db.model('gh4216', EventSchema); + var ev = new Event({ + name: 'test', + recurrence: { frequency: 2, interval: 'days' } + }); + ev.recurrence = null; + ev.save(function(error) { + assert.ifError(error); + done(); + }); + }); + + it('using validator.isEmail as a validator (gh-4064) (gh-4084)', function(done) { + var schema = new Schema({ + email: { type: String, validate: validator.isEmail } + }); + + var MyModel = db.model('gh4064', schema); + + MyModel.create({ email: 'invalid' }, function(error) { + assert.ok(error); + assert.ok(error.errors['email']); + done(); + }); + }); + + it('setting path to empty object works (gh-4218)', function(done) { + var schema = new Schema({ + object: { + nested: { + field1: { type: Number, default: 1 } + } + } + }); + + var MyModel = db.model('gh4218', schema); + + MyModel.create({}, function(error, doc) { + doc.object.nested = {}; + doc.save(function(error, doc) { + assert.ifError(error); + MyModel.collection.findOne({ _id: doc._id }, function(error, doc) { + assert.ifError(error); + assert.deepEqual(doc.object.nested, {}); + done(); + }); + }); + }); + }); + + it('minimize + empty object (gh-4337)', function(done) { + var SomeModel; + var SomeModelSchema; + + SomeModelSchema = new mongoose.Schema({}, { + minimize: false + }); + + SomeModel = mongoose.model('somemodel', SomeModelSchema); + + try { + new SomeModel({}); + } catch (error) { + assert.ifError(error); + } + done(); + }); + + it('doesnt markModified child paths if parent is modified (gh-4224)', function(done) { + var childSchema = new Schema({ + name: String + }); + var parentSchema = new Schema({ + child: childSchema + }); + + var Parent = db.model('gh4224', parentSchema); + Parent.create({ child: { name: 'Jacen' } }, function(error, doc) { + assert.ifError(error); + doc.child = { name: 'Jaina' }; + doc.child.name = 'Anakin'; + assert.deepEqual(doc.modifiedPaths(), ['child']); + assert.ok(doc.isModified('child.name')); + done(); + }); + }); + + it('single nested isNew (gh-4369)', function(done) { + var childSchema = new Schema({ + name: String + }); + var parentSchema = new Schema({ + child: childSchema + }); + + var Parent = db.model('gh4369', parentSchema); + var remaining = 2; + + var doc = new Parent({ child: { name: 'Jacen' } }); + doc.child.on('isNew', function(val) { + assert.ok(!val); + assert.ok(!doc.child.isNew); + --remaining || done(); + }); + + doc.save(function(error, doc) { + assert.ifError(error); + assert.ok(!doc.child.isNew); + --remaining || done(); + }); + }); + + it('deep default array values (gh-4540)', function(done) { + var schema = new Schema({ + arr: [{ + test: { + type: Array, + default: ['test'] + } + }] + }); + assert.doesNotThrow(function() { + db.model('gh4540', schema); + }); + done(); + }); + + it('default values with subdoc array (gh-4390)', function(done) { + var childSchema = new Schema({ + name: String + }); + var parentSchema = new Schema({ + child: [childSchema] + }); + + parentSchema.path('child').default([{ name: 'test' }]); + + var Parent = db.model('gh4390', parentSchema); + + Parent.create({}, function(error, doc) { + assert.ifError(error); + var arr = doc.toObject().child.map(function(doc) { + assert.ok(doc._id); + delete doc._id; + return doc; + }); + assert.deepEqual(arr, [{ name: 'test' }]); + done(); + }); + }); + + it('handles invalid dates (gh-4404)', function(done) { + var testSchema = new Schema({ + date: Date + }); + + var Test = db.model('gh4404', testSchema); + + Test.create({ date: new Date('invalid date') }, function(error) { + assert.ok(error); + assert.equal(error.errors['date'].name, 'CastError'); + done(); + }); + }); + + it('setting array subpath (gh-4472)', function(done) { + var ChildSchema = new mongoose.Schema({ + name: String, + age: Number + }, { _id: false }); + + var ParentSchema = new mongoose.Schema({ + data: { + children: [ChildSchema] + } + }); + + var Parent = db.model('gh4472', ParentSchema); + + var p = new Parent(); + p.set('data.children.0', { + name: 'Bob', + age: 900 + }); + + assert.deepEqual(p.toObject().data.children, [{ name: 'Bob', age: 900 }]); + done(); + }); + + it('ignore paths (gh-4480)', function(done) { + var TestSchema = new Schema({ + name: { type: String, required: true } + }); + + var Test = db.model('gh4480', TestSchema); + + Test.create({ name: 'val' }, function(error) { + assert.ifError(error); + Test.findOne(function(error, doc) { + assert.ifError(error); + doc.name = null; + doc.$ignore('name'); + doc.save(function(error) { + assert.ifError(error); + Test.findById(doc._id, function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'val'); + done(); + }); + }); + }); + }); + }); + + it('composite _ids (gh-4542)', function(done) { + var schema = new Schema({ + _id: { + key1: String, + key2: String + }, + content: String + }, { retainKeyOrder: true }); + + var Model = db.model('gh4542', schema); + + var object = new Model(); + object._id = {key1: 'foo', key2: 'bar'}; + object.save(). + then(function(obj) { + obj.content = 'Hello'; + return obj.save(); + }). + then(function(obj) { + return Model.findOne({ _id: obj._id }); + }). + then(function(obj) { + assert.equal(obj.content, 'Hello'); + done(); + }). + catch(done); + }); + + it('validateSync with undefined and conditional required (gh-4607)', function(done) { + var schema = new mongoose.Schema({ + type: mongoose.SchemaTypes.Number, + conditional: { + type: mongoose.SchemaTypes.String, + required: function() { + return this.type === 1; + }, + maxlength: 128 + } + }); + + var Model = db.model('gh4607', schema); + + assert.doesNotThrow(function() { + new Model({ + type: 2, + conditional: void 0 + }).validateSync(); + }); + + done(); + }); + + it('conditional required on single nested (gh-4663)', function(done) { + var called = 0; + var childSchema = new Schema({ + name: String + }); + var schema = new Schema({ + child: { + type: childSchema, + required: function() { + assert.equal(this.child.name, 'test'); + ++called; + } + } + }); + + var M = db.model('gh4663', schema); + + new M({ child: { name: 'test' } }).validateSync(); + done(); + }); + + it('setting full path under single nested schema works (gh-4578) (gh-4528)', function(done) { + var ChildSchema = new mongoose.Schema({ + age: Number + }); + + var ParentSchema = new mongoose.Schema({ + age: Number, + family: { + child: ChildSchema + } + }); + + var M = db.model('gh4578', ParentSchema); + + M.create({ age: 45 }, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.family.child); + doc.set('family.child.age', 15); + assert.ok(doc.family.child.schema); + assert.ok(doc.isModified('family.child')); + assert.ok(doc.isModified('family.child.age')); + assert.equal(doc.family.child.toObject().age, 15); + done(); + }); + }); + + it('setting a nested path retains nested modified paths (gh-5206)', function(done) { + var testSchema = new mongoose.Schema({ + name: String, + surnames: { + docarray: [{ name: String }] + } + }); + + var Cat = db.model('gh5206', testSchema); + + var kitty = new Cat({ + name: 'Test', + surnames: { + docarray: [{ name: 'test1' }, { name: 'test2' }] + } + }); + + kitty.save(function(error) { + assert.ifError(error); + + kitty.surnames = { + docarray: [{ name: 'test1' }, { name: 'test2' }, { name: 'test3' }] + }; + + assert.deepEqual(kitty.modifiedPaths(), + ['surnames', 'surnames.docarray']); + done(); + }); + }); + + it('toObject() does not depopulate top level (gh-3057)', function(done) { + var Cat = db.model('gh3057', { name: String }); + var Human = db.model('gh3057_0', { + name: String, + petCat: { type: mongoose.Schema.Types.ObjectId, ref: 'gh3057' } + }); + + var kitty = new Cat({ name: 'Zildjian' }); + var person = new Human({ name: 'Val', petCat: kitty }); + + assert.equal(kitty.toObject({ depopulate: true }).name, 'Zildjian'); + assert.ok(!person.toObject({ depopulate: true }).petCat.name); + done(); + }); + + it('single nested doc conditional required (gh-4654)', function(done) { + var ProfileSchema = new Schema({ + firstName: String, + lastName: String + }); + + function validator() { + assert.equal(this.email, 'test'); + return true; + } + + var UserSchema = new Schema({ + email: String, + profile: { + type: ProfileSchema, + required: [validator, 'profile required'] + } + }); + + var User = db.model('gh4654', UserSchema); + User.create({ email: 'test' }, function(error) { + assert.equal(error.errors['profile'].message, 'profile required'); + done(); + }); + }); + + it('handles setting single nested schema to equal value (gh-4676)', function(done) { + var companySchema = new mongoose.Schema({ + _id: false, + name: String, + description: String + }); + + var userSchema = new mongoose.Schema({ + name: String, + company: companySchema + }); + + var User = db.model('gh4676', userSchema); + + var user = new User({ company: { name: 'Test' } }); + user.save(function(error) { + assert.ifError(error); + user.company.description = 'test'; + assert.ok(user.isModified('company')); + user.company = user.company; + assert.ok(user.isModified('company')); + done(); + }); + }); + + it('handles setting single nested doc to null after setting (gh-4766)', function(done) { + var EntitySchema = new Schema({ + company: { + type: String, + required: true + }, + name: { + type: String, + required: false + }, + email: { + type: String, + required: false + } + }, { _id: false, id: false }); + + var ShipmentSchema = new Schema({ + entity: { + shipper: { + type: EntitySchema, + required: false + }, + manufacturer: { + type: EntitySchema, + required: false + } + } + }); + + var Shipment = db.model('gh4766', ShipmentSchema); + var doc = new Shipment({ + entity: { + shipper: null, + manufacturer: { + company: 'test', + name: 'test', + email: 'test@email' + } + } + }); + + doc.save(). + then(function() { return Shipment.findById(doc._id); }). + then(function(shipment) { + shipment.entity = shipment.entity; + shipment.entity.manufacturer = null; + return shipment.save(); + }). + then(function() { + done(); + }). + catch(done); + }); + + it('buffers with subtypes as ids (gh-4506)', function(done) { + var uuid = require('uuid'); + + var UserSchema = new mongoose.Schema({ + _id: { + type: Buffer, + default: function() { + return mongoose.Types.Buffer(uuid.parse(uuid.v4())).toObject(4); + }, + unique: true, + required: true + }, + email: { + type: String, + unique: true, + lowercase: true, + required: true + }, + name: String + }); + + var User = db.model('gh4506', UserSchema); + + var user = new User({ + email: 'me@email.com', + name: 'My name' + }); + + user.save(). + then(function() { + return User.findOne({ email: 'me@email.com' }); + }). + then(function(user) { + user.name = 'other'; + return user.save(); + }). + then(function() { + return User.findOne({ email: 'me@email.com' }); + }). + then(function(doc) { + assert.equal(doc.name, 'other'); + done(); + }). + catch(done); + }); + + it('embedded docs dont mark parent as invalid (gh-4681)', function(done) { + var NestedSchema = new mongoose.Schema({ + nestedName: { type: String, required: true }, + createdAt: { type: Date, required: true } + }); + var RootSchema = new mongoose.Schema({ + rootName: String, + nested: { type: [ NestedSchema ] } + }); + + var Root = db.model('gh4681', RootSchema); + var root = new Root({ rootName: 'root', nested: [ { } ] }); + root.save(function(error) { + assert.ok(error); + assert.deepEqual(Object.keys(error.errors).sort(), + ['nested.0.createdAt', 'nested.0.nestedName']); + done(); + }); + }); + + it('should depopulate the shard key when saving (gh-4658)', function(done) { + var ChildSchema = new mongoose.Schema({ + name: String + }); + + var ChildModel = db.model('gh4658', ChildSchema); + + var ParentSchema = new mongoose.Schema({ + name: String, + child: { type: Schema.Types.ObjectId, ref: 'gh4658' } + }, {shardKey: {child: 1, _id: 1}}); + + var ParentModel = db.model('gh4658_0', ParentSchema); + + ChildModel.create({ name: 'Luke' }). + then(function(child) { + var p = new ParentModel({ name: 'Vader' }); + p.child = child; + return p.save(); + }). + then(function(p) { + p.name = 'Anakin'; + return p.save(); + }). + then(function(p) { + return ParentModel.findById(p); + }). + then(function(doc) { + assert.equal(doc.name, 'Anakin'); + done(); + }). + catch(done); + }); + + it('handles setting virtual subpaths (gh-4716)', function(done) { + var childSchema = new Schema({ + name: { type: String, default: 'John' }, + favorites: { + color: { + type: String, + default: 'Blue' + } + } + }); + + var parentSchema = new Schema({ + name: { type: String }, + children: { + type: [childSchema], + default: [{}] + } + }); + + parentSchema.virtual('favorites').set(function(v) { + return this.children[0].set('favorites', v); + }).get(function() { + return this.children[0].get('favorites'); + }); + + var Parent = db.model('gh4716', parentSchema); + var p = new Parent({ name: 'Anakin' }); + p.set('children.0.name', 'Leah'); + p.set('favorites.color', 'Red'); + assert.equal(p.children[0].favorites.color, 'Red'); + done(); + }); + + it('handles selected nested elements with defaults (gh-4739)', function(done) { + var userSchema = new Schema({ + preferences: { + sleep: { type: Boolean, default: false }, + test: { type: Boolean, default: true } + }, + name: String + }); + + var User = db.model('User', userSchema); + + var user = { name: 'test' }; + User.collection.insertOne(user, function(error) { + assert.ifError(error); + User.findById(user, { 'preferences.sleep': 1, name: 1 }, function(error, user) { + assert.ifError(error); + assert.strictEqual(user.preferences.sleep, false); + assert.ok(!user.preferences.test); + done(); + }); + }); + }); + + it('handles mark valid in subdocs correctly (gh-4778)', function(done) { + var SubSchema = new mongoose.Schema({ + field: { + nestedField: { + type: mongoose.Schema.ObjectId, + required: false + } + } + }, { _id: false, id: false }); + + var Model2Schema = new mongoose.Schema({ + sub: { + type: SubSchema, + required: false + } + }); + var Model2 = db.model('gh4778', Model2Schema); + + var doc = new Model2({ + sub: {} + }); + + doc.sub.field.nestedField = { }; + doc.sub.field.nestedField = '574b69d0d9daf106aaa62974'; + assert.ok(!doc.validateSync()); + done(); + }); + + it('timestamps with nested paths (gh-5051)', function(done) { + var schema = new Schema({ props: {} }, { + timestamps: { + createdAt: 'props.createdAt', + updatedAt: 'props.updatedAt' + } + }); + + var M = db.model('gh5051', schema); + var now = Date.now(); + M.create({}, function(error, doc) { + assert.ok(doc.props.createdAt); + assert.ok(doc.props.createdAt instanceof Date); + assert.ok(doc.props.createdAt.valueOf() >= now); + assert.ok(doc.props.updatedAt); + assert.ok(doc.props.updatedAt instanceof Date); + assert.ok(doc.props.updatedAt.valueOf() >= now); + done(); + }); + }); + + it('Declaring defaults in your schema with timestamps defined (gh-6024)', function(done) { + var schemaDefinition = { + name: String, + misc: { + hometown: String, + isAlive: { type: Boolean, default: true } + } + }; + + var schemaWithTimestamps = new Schema(schemaDefinition, {timestamps: {createdAt: 'misc.createdAt'}}); + var PersonWithTimestamps = db.model('Person_timestamps', schemaWithTimestamps); + var dude = new PersonWithTimestamps({ name: 'Keanu', misc: {hometown: 'Beirut'} }); + assert.equal(dude.misc.isAlive, true); + + done(); + }); + + it('supports $where in pre save hook (gh-4004)', function(done) { + var Promise = global.Promise; + + var schema = new Schema({ + name: String + }, { timestamps: true, versionKey: null, saveErrorIfNotFound: true }); + + schema.pre('save', function(next) { + this.$where = { updatedAt: this.updatedAt }; + next(); + }); + + schema.post('save', function(error, res, next) { + if (error instanceof MongooseError.DocumentNotFoundError) { + error = new Error('Somebody else updated the document!'); + } + next(error); + }); + + var MyModel = db.model('gh4004', schema); + + MyModel.create({ name: 'test' }). + then(function() { + return Promise.all([ + MyModel.findOne(), + MyModel.findOne() + ]); + }). + then(function(docs) { + docs[0].name = 'test2'; + return Promise.all([ + docs[0].save(), + Promise.resolve(docs[1]) + ]); + }). + then(function(docs) { + docs[1].name = 'test3'; + return docs[1].save(); + }). + then(function() { + done(new Error('Should not get here')); + }). + catch(function(error) { + assert.equal(error.message, 'Somebody else updated the document!'); + done(); + }); + }); + + it('toObject() with buffer and minimize (gh-4800)', function(done) { + var TestSchema = new mongoose.Schema({ buf: Buffer }, { + toObject: { + virtuals: true, + getters: true + } + }); + + var Test = db.model('gh4800', TestSchema); + + Test.create({ buf: new Buffer('abcd') }). + then(function(doc) { + return Test.findById(doc._id); + }). + then(function(doc) { + // Should not throw + require('util').inspect(doc); + done(); + }). + catch(done); + }); + + it('buffer subtype prop (gh-5530)', function(done) { + var TestSchema = new mongoose.Schema({ + uuid: { + type: Buffer, + subtype: 4 + } + }); + + var Test = db.model('gh5530', TestSchema); + + var doc = new Test({ uuid: 'test1' }); + assert.equal(doc.uuid._subtype, 4); + done(); + }); + + it('runs validate hooks on single nested subdocs if not directly modified (gh-3884)', function(done) { + var childSchema = new Schema({ + name: { type: String }, + friends: [{ type: String }] + }); + var count = 0; + + childSchema.pre('validate', function(next) { + ++count; + next(); + }); + + var parentSchema = new Schema({ + name: { type: String }, + child: childSchema + }); + + var Parent = db.model('gh3884', parentSchema); + + var p = new Parent({ + name: 'Mufasa', + child: { + name: 'Simba', + friends: ['Pumbaa', 'Timon', 'Nala'] + } + }); + + p.save(). + then(function(p) { + assert.equal(count, 1); + p.child.friends.push('Rafiki'); + return p.save(); + }). + then(function() { + assert.equal(count, 2); + done(); + }). + catch(done); + }); + + it('runs validate hooks on arrays subdocs if not directly modified (gh-5861)', function(done) { + var childSchema = new Schema({ + name: { type: String }, + friends: [{ type: String }] + }); + var count = 0; + + childSchema.pre('validate', function(next) { + ++count; + next(); + }); + + var parentSchema = new Schema({ + name: { type: String }, + children: [childSchema] + }); + + var Parent = db.model('gh5861', parentSchema); + + var p = new Parent({ + name: 'Mufasa', + children: [{ + name: 'Simba', + friends: ['Pumbaa', 'Timon', 'Nala'] + }] + }); + + p.save(). + then(function(p) { + assert.equal(count, 1); + p.children[0].friends.push('Rafiki'); + return p.save(); + }). + then(function() { + assert.equal(count, 2); + done(); + }). + catch(done); + }); + + it('does not overwrite when setting nested (gh-4793)', function(done) { + var grandchildSchema = new mongoose.Schema(); + grandchildSchema.method({ + foo: function() { return 'bar'; } + }); + var Grandchild = db.model('gh4793_0', grandchildSchema); + + var childSchema = new mongoose.Schema({ + grandchild: grandchildSchema + }); + var Child = mongoose.model('gh4793_1', childSchema); + + var parentSchema = new mongoose.Schema({ + children: [childSchema] + }); + var Parent = mongoose.model('gh4793_2', parentSchema); + + var grandchild = new Grandchild(); + var child = new Child({grandchild: grandchild}); + + assert.equal(child.grandchild.foo(), 'bar'); + + var p = new Parent({children: [child]}); + + assert.equal(child.grandchild.foo(), 'bar'); + assert.equal(p.children[0].grandchild.foo(), 'bar'); + done(); + }); + + it('setting to discriminator (gh-4935)', function(done) { + var Buyer = db.model('gh4935_0', new Schema({ + name: String, + vehicle: { type: Schema.Types.ObjectId, ref: 'gh4935' } + })); + var Vehicle = db.model('gh4935', new Schema({ name: String })); + var Car = Vehicle.discriminator('gh4935_1', new Schema({ + model: String + })); + + var eleanor = new Car({ name: 'Eleanor', model: 'Shelby Mustang GT' }); + var nick = new Buyer({ name: 'Nicolas', vehicle: eleanor }); + + assert.ok(!!nick.vehicle); + assert.ok(nick.vehicle === eleanor); + assert.ok(nick.vehicle instanceof Car); + assert.equal(nick.vehicle.name, 'Eleanor'); + + done(); + }); + + it('handles errors in sync validators (gh-2185)', function(done) { + var schema = new Schema({ + name: { + type: String, + validate: function() { + throw new Error('woops!'); + } + } + }); + + var M = db.model('gh2185', schema); + + var error = (new M({ name: 'test' })).validateSync(); + assert.ok(error); + assert.equal(error.errors['name'].reason.message, 'woops!'); + + new M({ name: 'test'}).validate(function(error) { + assert.ok(error); + assert.equal(error.errors['name'].reason.message, 'woops!'); + done(); + }); + }); + + it('allows hook as a schema key (gh-5047)', function(done) { + var schema = new mongoose.Schema({ + name: String, + hook: { type: String } + }); + + var Model = db.model('Model', schema); + + Model.create({ hook: 'test '}, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('save errors with callback and promise work (gh-5216)', function(done) { + var schema = new mongoose.Schema({}); + + var Model = db.model('gh5216', schema); + + var _id = new mongoose.Types.ObjectId(); + var doc1 = new Model({ _id: _id }); + var doc2 = new Model({ _id: _id }); + + Model.on('error', function(error) { + done(error); + }); + + doc1.save(). + then(function() { return doc2.save(function() {}); }). + catch(function(error) { + assert.ok(error); + done(); + }); + }); + + it('post hooks on child subdocs run after save (gh-5085)', function(done) { + var ChildModelSchema = new mongoose.Schema({ + text: { + type: String + } + }); + ChildModelSchema.post('save', function(doc) { + doc.text = 'bar'; + }); + var ParentModelSchema = new mongoose.Schema({ + children: [ChildModelSchema] + }); + + var Model = db.model('gh5085', ParentModelSchema); + + Model.create({ children: [{ text: 'test' }] }, function(error) { + assert.ifError(error); + Model.findOne({}, function(error, doc) { + assert.ifError(error); + assert.equal(doc.children.length, 1); + assert.equal(doc.children[0].text, 'test'); + done(); + }); + }); + }); + + it('nested docs toObject() clones (gh-5008)', function(done) { + var schema = new mongoose.Schema({ + sub: { + height: Number + } + }); + + var Model = db.model('gh5008', schema); + + var doc = new Model({ + sub: { + height: 3 + } + }); + + assert.equal(doc.sub.height, 3); + + var leanDoc = doc.sub.toObject(); + assert.equal(leanDoc.height, 3); + + doc.sub.height = 55; + assert.equal(doc.sub.height, 55); + assert.equal(leanDoc.height, 3); + + done(); + }); + + it('toObject() with null (gh-5143)', function(done) { + var schema = new mongoose.Schema({ + customer: { + name: { type: String, required: false } + } + }); + + var Model = db.model('gh5143', schema); + + var model = new Model(); + model.customer = null; + assert.strictEqual(model.toObject().customer, null); + assert.strictEqual(model.toObject({ getters: true }).customer, null); + + done(); + }); + + it('handles array subdocs with single nested subdoc default (gh-5162)', function(done) { + var RatingsItemSchema = new mongoose.Schema({ + value: Number + }, { versionKey: false, _id: false }); + + var RatingsSchema = new mongoose.Schema({ + ratings: { + type: RatingsItemSchema, + default: { id: 1, value: 0 } + }, + _id: false + }); + + var RestaurantSchema = new mongoose.Schema({ + menu: { + type: [RatingsSchema] + } + }); + + var Restaurant = db.model('gh5162', RestaurantSchema); + + // Should not throw + var r = new Restaurant(); + assert.deepEqual(r.toObject().menu, []); + done(); + }); + + it('iterating through nested doc keys (gh-5078)', function(done) { + var schema = new Schema({ + nested: { + test1: String, + test2: String + } + }, { retainKeyOrder: true }); + + schema.virtual('tests').get(function() { + return _.map(this.nested, function(v) { + return v; + }); + }); + + var M = db.model('gh5078', schema); + + var doc = new M({ nested: { test1: 'a', test2: 'b' } }); + + assert.deepEqual(doc.toObject({ virtuals: true }).tests, ['a', 'b']); + + // Should not throw + require('util').inspect(doc); + JSON.stringify(doc); + + done(); + }); + + it('deeply nested virtual paths (gh-5250)', function(done) { + var TestSchema = new Schema({}); + TestSchema. + virtual('a.b.c'). + get(function() { + return this.v; + }). + set(function(value) { + this.v = value; + }); + + var TestModel = db.model('gh5250', TestSchema); + var t = new TestModel({'a.b.c': 5}); + assert.equal(t.a.b.c, 5); + + done(); + }); + + it('JSON.stringify nested errors (gh-5208)', function(done) { + var AdditionalContactSchema = new Schema({ + contactName: { + type: String, + required: true + }, + contactValue: { + type: String, + required: true + } + }); + + var ContactSchema = new Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + additionalContacts: [AdditionalContactSchema] + }); + + var EmergencyContactSchema = new Schema({ + contactName: { + type: String, + required: true + }, + contact: ContactSchema + }); + + var EmergencyContact = + db.model('EmergencyContact', EmergencyContactSchema); + + var contact = new EmergencyContact({ + contactName: 'Electrical Service', + contact: { + name: 'John Smith', + email: 'john@gmail.com', + additionalContacts: [ + { + contactName: 'skype' + // Forgotten value + } + ] + } + }); + contact.validate(function(error) { + assert.ok(error); + assert.ok(error.errors['contact']); + assert.ok(error.errors['contact.additionalContacts.0.contactValue']); + + // This `JSON.stringify()` should not throw + assert.ok(JSON.stringify(error).indexOf('contactValue') !== -1); + done(); + }); + }); + + it('handles errors in subdoc pre validate (gh-5215)', function(done) { + var childSchema = new mongoose.Schema({}); + + childSchema.pre('validate', function(next) { + next(new Error('child pre validate')); + }); + + var parentSchema = new mongoose.Schema({ + child: childSchema + }); + + var Parent = db.model('gh5215', parentSchema); + + Parent.create({ child: {} }, function(error) { + assert.ok(error); + assert.ok(error.errors['child']); + assert.equal(error.errors['child'].message, 'child pre validate'); + done(); + }); + }); + + it('custom error types (gh-4009)', function(done) { + var CustomError = function() {}; + + var testSchema = new mongoose.Schema({ + num: { + type: Number, + required: { + ErrorConstructor: CustomError + }, + min: 5 + } + }); + + var Test = db.model('gh4009', testSchema); + + Test.create({}, function(error) { + assert.ok(error); + assert.ok(error.errors['num']); + assert.ok(error.errors['num'] instanceof CustomError); + Test.create({ num: 1 }, function(error) { + assert.ok(error); + assert.ok(error.errors['num']); + assert.ok(error.errors['num'].constructor.name, 'ValidatorError'); + assert.ok(!(error.errors['num'] instanceof CustomError)); + done(); + }); + }); + }); + + it('saving a doc with nested string array (gh-5282)', function(done) { + var testSchema = new mongoose.Schema({ + strs: [[String]] + }); + + var Test = db.model('gh5282', testSchema); + + var t = new Test({ + strs: [['a', 'b']] + }); + + t.save(function(error, t) { + assert.ifError(error); + assert.deepEqual(t.toObject().strs, [['a', 'b']]); + done(); + }); + }); + + it('null _id (gh-5236)', function(done) { + var childSchema = new mongoose.Schema({}); + + var M = db.model('gh5236', childSchema); + + var m = new M({ _id: null }); + m.save(function(error, doc) { + assert.equal(doc._id, null); + done(); + }); + }); + + it('setting populated path with typeKey (gh-5313)', function(done) { + var personSchema = Schema({ + name: {$type: String}, + favorite: { $type: Schema.Types.ObjectId, ref: 'gh5313' }, + books: [{ $type: Schema.Types.ObjectId, ref: 'gh5313' }] + }, { typeKey: '$type' }); + + var bookSchema = Schema({ + title: String + }); + + var Book = mongoose.model('gh5313', bookSchema); + var Person = mongoose.model('gh5313_0', personSchema); + + var book1 = new Book({ title: 'The Jungle Book' }); + var book2 = new Book({ title: '1984' }); + + var person = new Person({ + name: 'Bob', + favorite: book1, + books: [book1, book2] + }); + + assert.equal(person.books[0].title, 'The Jungle Book'); + assert.equal(person.books[1].title, '1984'); + + done(); + }); + + it('save twice with write concern (gh-5294)', function(done) { + var schema = new mongoose.Schema({ + name: String + }, { + safe: { + w: 'majority', + wtimeout: 1e4 + } + }); + + var M = db.model('gh5294', schema); + + M.create({ name: 'Test' }, function(error, doc) { + assert.ifError(error); + doc.name = 'test2'; + doc.save(function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('undefined field with conditional required (gh-5296)', function(done) { + var schema = Schema({ + name: { + type: String, + maxlength: 63, + required: function() { + return false; + } + } + }); + + var Model = db.model('gh5296', schema); + + Model.create({ name: undefined }, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('dotted virtuals in toObject (gh-5473)', function(done) { + var schema = new mongoose.Schema({}, { + toObject: { virtuals: true }, + toJSON: { virtuals: true } + }); + schema.virtual('test.a').get(function() { + return 1; + }); + schema.virtual('test.b').get(function() { + return 2; + }); + + var Model = mongoose.model('gh5473', schema); + + var m = new Model({}); + assert.deepEqual(m.toJSON().test, { + a: 1, + b: 2 + }); + assert.deepEqual(m.toObject().test, { + a: 1, + b: 2 + }); + assert.equal(m.toObject({ virtuals: false }).test, void 0); + done(); + }); + + it('dotted virtuals in toObject (gh-5506)', function(done) { + var childSchema = new Schema({ + name: String, + _id: false + }); + var parentSchema = new Schema({ + child: { + type: childSchema, + default: {} + } + }); + + var Parent = db.model('gh5506', parentSchema); + + var p = new Parent({ child: { name: 'myName' } }); + + p.save(). + then(function() { + return Parent.findOne(); + }). + then(function(doc) { + doc.child = {}; + return doc.save(); + }). + then(function() { + return Parent.findOne(); + }). + then(function(doc) { + assert.deepEqual(doc.toObject().child, {}); + done(); + }). + catch(done); + }); + + it('parent props not in child (gh-5470)', function(done) { + var employeeSchema = new mongoose.Schema({ + name: { + first: String, + last: String + }, + department: String + }); + var Employee = mongoose.model('Test', employeeSchema); + + var employee = new Employee({ + name: { + first: 'Ron', + last: 'Swanson' + }, + department: 'Parks and Recreation' + }); + var ownPropertyNames = Object.getOwnPropertyNames(employee.name); + + assert.ok(ownPropertyNames.indexOf('department') === -1, ownPropertyNames.join(',')); + assert.ok(ownPropertyNames.indexOf('first') !== -1, ownPropertyNames.join(',')); + assert.ok(ownPropertyNames.indexOf('last') !== -1, ownPropertyNames.join(',')); + done(); + }); + + it('modifying array with existing ids (gh-5523)', function(done) { + var friendSchema = new mongoose.Schema( + { + _id: String, + name: String, + age: Number, + dob: Date + }, + { _id: false }); + + var socialSchema = new mongoose.Schema( + { + friends: [friendSchema] + }, + { _id: false }); + + var userSchema = new mongoose.Schema({ + social: { + type: socialSchema, + required: true + } + }); + + var User = db.model('gh5523', userSchema); + + var user = new User({ + social: { + friends: [ + { _id: 'val', age: 28 } + ] + } + }); + + user.social.friends = [{ _id: 'val', name: 'Val' }]; + + assert.deepEqual(user.toObject().social.friends[0], { + _id: 'val', + name: 'Val' + }); + + user.save(function(error) { + assert.ifError(error); + User.findOne({ _id: user._id }, function(error, doc) { + assert.ifError(error); + assert.deepEqual(doc.toObject().social.friends[0], { + _id: 'val', + name: 'Val' + }); + done(); + }); + }); + }); + + it('consistent setter context for single nested (gh-5363)', function(done) { + var contentSchema = new Schema({ + blocks: [{ type: String }], + summary: { type: String } + }); + + // Subdocument setter + var contexts = []; + contentSchema.path('blocks').set(function(srcBlocks) { + if (!this.ownerDocument().isNew) { + contexts.push(this.toObject()); + } + + return srcBlocks; + }); + + var noteSchema = new Schema({ + title: { type: String, required: true }, + body: { type: contentSchema } + }); + + var Note = db.model('gh5363', noteSchema); + + var note = new Note({ + title: 'Lorem Ipsum Dolor', + body: { + summary: 'Summary Test', + blocks: ['html'] + } + }); + + note.save(). + then(function(note) { + assert.equal(contexts.length, 0); + note.set('body', { + summary: 'New Summary', + blocks: ['gallery', 'html'] + }); + return note.save(); + }). + then(function() { + assert.equal(contexts.length, 1); + assert.deepEqual(contexts[0].blocks, ['html']); + done(); + }). + catch(done); + }); + + it('deeply nested subdocs and markModified (gh-5406)', function(done) { + var nestedValueSchema = new mongoose.Schema({ + _id: false, + value: Number + }); + var nestedPropertySchema = new mongoose.Schema({ + _id: false, + active: Boolean, + nestedValue: nestedValueSchema + }); + var nestedSchema = new mongoose.Schema({ + _id: false, + nestedProperty: nestedPropertySchema, + nestedTwoProperty: nestedPropertySchema + }); + var optionsSchema = new mongoose.Schema({ + _id: false, + nestedField: nestedSchema + }); + var TestSchema = new mongoose.Schema({ + fieldOne: String, + options: optionsSchema + }); + + var Test = db.model('gh5406', TestSchema); + + var doc = new Test({ + fieldOne: 'Test One', + options: { + nestedField: { + nestedProperty: { + active: true, + nestedValue: { + value: 42 + } + } + } + } + }); + + doc. + save(). + then(function(doc) { + doc.options.nestedField.nestedTwoProperty = { + active: true, + nestedValue: { + value: 1337 + } + }; + + assert.ok(doc.isModified('options')); + + return doc.save(); + }). + then(function(doc) { + return Test.findById(doc._id); + }). + then(function(doc) { + assert.equal(doc.options.nestedField.nestedTwoProperty.nestedValue.value, + 1337); + done(); + }). + catch(done); + }); + + it('single nested subdoc post remove hooks (gh-5388)', function(done) { + var contentSchema = new Schema({ + blocks: [{ type: String }], + summary: { type: String } + }); + + var called = 0; + + contentSchema.post('remove', function() { + ++called; + }); + + var noteSchema = new Schema({ + body: { type: contentSchema } + }); + + var Note = db.model('gh5388', noteSchema); + + var note = new Note({ + title: 'Lorem Ipsum Dolor', + body: { + summary: 'Summary Test', + blocks: ['html'] + } + }); + + note.save(function(error) { + assert.ifError(error); + note.remove(function(error) { + assert.ifError(error); + setTimeout(function() { + assert.equal(called, 1); + done(); + }, 50); + }); + }); + }); + + it('push populated doc onto empty array triggers manual population (gh-5504)', function(done) { + var ReferringSchema = new Schema({ + reference: [{ + type: Schema.Types.ObjectId, + ref: 'gh5504' + }] + }); + + var Referrer = db.model('gh5504', ReferringSchema); + + var referenceA = new Referrer(); + var referenceB = new Referrer(); + + var referrerA = new Referrer({reference: [referenceA]}); + var referrerB = new Referrer(); + var referrerC = new Referrer(); + var referrerD = new Referrer(); + var referrerE = new Referrer(); + + referrerA.reference.push(referenceB); + assert.ok(referrerA.reference[0] instanceof Referrer); + assert.ok(referrerA.reference[1] instanceof Referrer); + + referrerB.reference.push(referenceB); + assert.ok(referrerB.reference[0] instanceof Referrer); + + referrerC.reference.unshift(referenceB); + assert.ok(referrerC.reference[0] instanceof Referrer); + + referrerD.reference.splice(0, 0, referenceB); + assert.ok(referrerD.reference[0] instanceof Referrer); + + referrerE.reference.addToSet(referenceB); + assert.ok(referrerE.reference[0] instanceof Referrer); + + done(); + }); + + it('single nested conditional required scope (gh-5569)', function(done) { + var scopes = []; + + var ThingSchema = new mongoose.Schema({ + undefinedDisallowed: { + type: String, + required: function() { + scopes.push(this); + return this.undefinedDisallowed === undefined; + }, + default: null + } + }); + + var SuperDocumentSchema = new mongoose.Schema({ + thing: { + type: ThingSchema, + default: function() { return {}; } + } + }); + + var SuperDocument = db.model('gh5569', SuperDocumentSchema); + + var doc = new SuperDocument(); + doc.thing.undefinedDisallowed = null; + + doc.save(function(error) { + assert.ifError(error); + doc = new SuperDocument(); + doc.thing.undefinedDisallowed = undefined; + doc.save(function(error) { + assert.ok(error); + assert.ok(error.errors['thing.undefinedDisallowed']); + done(); + }); + }); + }); + + it('single nested setters only get called once (gh-5601)', function(done) { + var vals = []; + var ChildSchema = new mongoose.Schema({ + number: { + type: String, + set: function(v) { + vals.push(v); + return v; + } + }, + _id: false + }); + ChildSchema.set('toObject', { getters: true, minimize: false }); + + var ParentSchema = new mongoose.Schema({ + child: { + type: ChildSchema, + default: {} + } + }); + + var Parent = db.model('gh5601', ParentSchema); + var p = new Parent(); + p.child = { number: '555.555.0123' }; + assert.equal(vals.length, 1); + assert.equal(vals[0], '555.555.0123'); + done(); + }); + + it('setting doc array to array of top-level docs works (gh-5632)', function(done) { + var MainSchema = new Schema({ + name: { type: String }, + children: [{ + name: { type: String } + }] + }); + var RelatedSchema = new Schema({ name: { type: String } }); + var Model = db.model('gh5632', MainSchema); + var RelatedModel = db.model('gh5632_0', RelatedSchema); + + RelatedModel.create({ name: 'test' }, function(error, doc) { + assert.ifError(error); + Model.create({ name: 'test1', children: [doc] }, function(error, m) { + assert.ifError(error); + m.children = [doc]; + m.save(function(error) { + assert.ifError(error); + assert.equal(m.children.length, 1); + assert.equal(m.children[0].name, 'test'); + done(); + }); + }); + }); + }); + + it('Using set as a schema path (gh-1939)', function(done) { + var testSchema = new Schema({ set: String }); + + var Test = db.model('gh1939', testSchema); + + var t = new Test({ set: 'test 1' }); + assert.equal(t.set, 'test 1'); + t.save(function(error) { + assert.ifError(error); + t.set = 'test 2'; + t.save(function(error) { + assert.ifError(error); + assert.equal(t.set, 'test 2'); + done(); + }); + }); + }); + + it('handles array defaults correctly (gh-5780)', function(done) { + var testSchema = new Schema({ + nestedArr: { + type: [[Number]], + default: [[0, 1]] + } + }); + + var Test = db.model('gh5780', testSchema); + + var t = new Test({}); + assert.deepEqual(t.toObject().nestedArr, [[0, 1]]); + + t.nestedArr.push([1, 2]); + var t2 = new Test({}); + assert.deepEqual(t2.toObject().nestedArr, [[0, 1]]); + + done(); + }); + + it('virtuals with no getters return undefined (gh-6223)', function(done) { + var personSchema = new mongoose.Schema({ + name: { type: String }, + children: [{ + name: { type: String } + }] + }, { + toObject: { getters: true, virtuals: true }, + toJSON: { getters: true, virtuals: true }, + id: false + }); + + personSchema.virtual('favoriteChild').set(function(v) { + return this.set('children.0', v); + }); + + personSchema.virtual('heir').get(function() { + return this.get('children.0'); + }); + + var Person = db.model('gh6223', personSchema); + + var person = new Person({ + name: 'Anakin' + }); + + assert.strictEqual(person.favoriteChild, void 0); + assert.ok(!('favoriteChild' in person.toJSON())); + assert.ok(!('favoriteChild' in person.toObject())); + + done(); + }); + + it('Disallows writing to __proto__ and other special properties', function(done) { + var schema = new mongoose.Schema({ + name: String + }, { strict: false }); + + var Model = db.model('prototest', schema); + var doc = new Model({ '__proto__.x': 'foo' }); + + assert.strictEqual(Model.x, void 0); + doc.set('__proto__.y', 'bar'); + + assert.strictEqual(Model.y, void 0); + + doc.set('constructor.prototype.z', 'baz'); + + assert.strictEqual(Model.z, void 0); + + done(); + }); + + it('Handles setting populated path set via `Document#populate()` (gh-7302)', function() { + var authorSchema = new Schema({ name: String }); + var bookSchema = new Schema({ + author: { type: mongoose.Schema.Types.ObjectId, ref: 'gh7302_Author' } + }); + + var Author = db.model('gh7302_Author', authorSchema); + var Book = db.model('gh7302_Book', bookSchema); + + return Author.create({ name: 'Victor Hugo' }). + then(function(author) { return Book.create({ author: author._id }); }). + then(function() { return Book.findOne(); }). + then(function(doc) { return doc.populate('author').execPopulate(); }). + then(function(doc) { + doc.author = {}; + assert.ok(!doc.author.name); + assert.ifError(doc.validateSync()); + }); + }); + + + it('Single nested subdocs using discriminator can be modified (gh-5693)', function(done) { + var eventSchema = new Schema({ message: String }, { + discriminatorKey: 'kind', + _id: false + }); + + var trackSchema = new Schema({ event: eventSchema }); + + trackSchema.path('event').discriminator('Clicked', new Schema({ + element: String + }, { _id: false })); + + trackSchema.path('event').discriminator('Purchased', new Schema({ + product: String + }, { _id: false })); + + var MyModel = db.model('gh5693', trackSchema); + + var doc = new MyModel({ + event: { + message: 'Test', + kind: 'Clicked', + element: 'Amazon Link' + } + }); + + doc.save(function(error) { + assert.ifError(error); + assert.equal(doc.event.message, 'Test'); + assert.equal(doc.event.kind, 'Clicked'); + assert.equal(doc.event.element, 'Amazon Link'); + + doc.set('event', { + kind: 'Purchased', + product: 'Professional AngularJS' + }); + + doc.save(function(error) { + assert.ifError(error); + assert.equal(doc.event.kind, 'Purchased'); + assert.equal(doc.event.product, 'Professional AngularJS'); + assert.ok(!doc.event.element); + assert.ok(!doc.event.message); + done(); + }); + }); + }); + + it('doc array: set then remove (gh-3511)', function(done) { + var ItemChildSchema = new mongoose.Schema({ + name: { + type: String, + required: true + } + }); + + var ItemParentSchema = new mongoose.Schema({ + children: [ItemChildSchema] + }); + + var ItemParent = db.model('gh3511', ItemParentSchema); + + var p = new ItemParent({ + children: [{ name: 'test1' }, { name: 'test2' }] + }); + + p.save(function(error) { + assert.ifError(error); + ItemParent.findById(p._id, function(error, doc) { + assert.ifError(error); + assert.ok(doc); + assert.equal(doc.children.length, 2); + + doc.children[1].name = 'test3'; + doc.children.remove(doc.children[0]); + + doc.save(function(error) { + assert.ifError(error); + ItemParent.findById(doc._id, function(error, doc) { + assert.ifError(error); + assert.equal(doc.children.length, 1); + assert.equal(doc.children[0].name, 'test3'); + done(); + }); + }); + }); + }); + }); + + it('modifying unselected nested object (gh-5800)', function() { + var MainSchema = new mongoose.Schema({ + a: { + b: {type: String, default: 'some default'}, + c: {type: Number, default: 0}, + d: {type: String} + }, + e: {type: String} + }); + + MainSchema.pre('save', function(next) { + if (this.isModified()) { + this.set('a.c', 100, Number); + } + next(); + }); + + var Main = db.model('gh5800', MainSchema); + + var doc = { a: { b: 'not the default', d: 'some value' }, e: 'e' }; + return Main.create(doc). + then(function(doc) { + assert.equal(doc.a.b, 'not the default'); + assert.equal(doc.a.d, 'some value'); + return Main.findOne().select('e'); + }). + then(function(doc) { + doc.e = 'e modified'; + return doc.save(); + }). + then(function() { + return Main.findOne(); + }). + then(function(doc) { + assert.equal(doc.a.b, 'not the default'); + assert.equal(doc.a.d, 'some value'); + }); + }); + + it('consistent context for nested docs (gh-5347)', function(done) { + var contexts = []; + var childSchema = new mongoose.Schema({ + phoneNumber: { + type: String, + required: function() { + contexts.push(this); + return this.notifications.isEnabled; + } + }, + notifications: { + isEnabled: { type: Boolean, required: true } + } + }); + + var parentSchema = new mongoose.Schema({ + name: String, + children: [childSchema] + }); + + var Parent = db.model('gh5347', parentSchema); + + Parent.create({ + name: 'test', + children: [ + { + phoneNumber: '123', + notifications: { + isEnabled: true + } + } + ] + }, function(error, doc) { + assert.ifError(error); + var child = doc.children.id(doc.children[0]._id); + child.phoneNumber = '345'; + assert.equal(contexts.length, 1); + doc.save(function(error) { + assert.ifError(error); + assert.equal(contexts.length, 2); + assert.ok(contexts[0].toObject().notifications.isEnabled); + assert.ok(contexts[1].toObject().notifications.isEnabled); + done(); + }); + }); + }); + + it('modify multiple subdoc paths (gh-4405)', function(done) { + var ChildObjectSchema = new Schema({ + childProperty1: String, + childProperty2: String, + childProperty3: String + }); + + var ParentObjectSchema = new Schema({ + parentProperty1: String, + parentProperty2: String, + child: ChildObjectSchema + }); + + var Parent = db.model('gh4405', ParentObjectSchema); + + var p = new Parent({ + parentProperty1: 'abc', + parentProperty2: '123', + child: { + childProperty1: 'a', + childProperty2: 'b', + childProperty3: 'c' + } + }); + p.save(function(error) { + assert.ifError(error); + Parent.findById(p._id, function(error, p) { + assert.ifError(error); + p.parentProperty1 = 'foo'; + p.parentProperty2 = 'bar'; + p.child.childProperty1 = 'ping'; + p.child.childProperty2 = 'pong'; + p.child.childProperty3 = 'weee'; + p.save(function(error) { + assert.ifError(error); + Parent.findById(p._id, function(error, p) { + assert.ifError(error); + assert.equal(p.child.childProperty1, 'ping'); + assert.equal(p.child.childProperty2, 'pong'); + assert.equal(p.child.childProperty3, 'weee'); + done(); + }); + }); + }); }); }); }); diff --git a/test/document.unit.test.js b/test/document.unit.test.js index 2e87f421ef2..3f9748c0a6f 100644 --- a/test/document.unit.test.js +++ b/test/document.unit.test.js @@ -2,15 +2,17 @@ * Module dependencies. */ +var assert = require('power-assert'); var start = require('./common'); -var assert = require('assert'); +var storeShard = require('../lib/plugins/sharding').storeShard; + var mongoose = start.mongoose; describe('sharding', function() { it('should handle shard keys properly (gh-2127)', function(done) { var mockSchema = { options: { - shardKey: { date: 1 } + shardKey: {date: 1} } }; var Stub = function() { @@ -20,10 +22,10 @@ describe('sharding', function() { Stub.prototype.__proto__ = mongoose.Document.prototype; var d = new Stub(); var currentTime = new Date(); - d._doc = { date: currentTime }; + d._doc = {date: currentTime}; - d.$__storeShard(); - assert.equal(currentTime, d.$__.shardval.date); + storeShard.call(d); + assert.equal(d.$__.shardval.date, currentTime); done(); }); }); @@ -34,10 +36,10 @@ describe('toObject()', function() { beforeEach(function() { Stub = function() { var schema = this.schema = { - options: { toObject: { minimize: false, virtuals: true } }, - virtuals: { virtual: 'test' } + options: {toObject: {minimize: false, virtuals: true}}, + virtuals: {virtual: 'test'} }; - this._doc = { empty: {} }; + this._doc = {empty: {}}; this.get = function(path) { return schema.virtuals[path]; }; this.$__ = {}; }; @@ -46,13 +48,13 @@ describe('toObject()', function() { it('should inherit options from schema', function(done) { var d = new Stub(); - assert.deepEqual(d.toObject(), { empty: {}, virtual: 'test' }); + assert.deepEqual(d.toObject(), {empty: {}, virtual: 'test'}); done(); }); it('can overwrite by passing an option', function(done) { var d = new Stub(); - assert.deepEqual(d.toObject({ minimize: true }), {}); + assert.deepEqual(d.toObject({minimize: true}), {}); done(); }); diff --git a/test/errors.validation.test.js b/test/errors.validation.test.js index 1230e529541..c446e75c2f8 100644 --- a/test/errors.validation.test.js +++ b/test/errors.validation.test.js @@ -3,24 +3,25 @@ * Module dependencies. */ -var assert = require('assert') - , start = require('./common') - , mongoose = start.mongoose - , Schema = mongoose.Schema - , SchemaType = mongoose.SchemaType - , ValidatorError = SchemaType.ValidatorError; +var assert = require('power-assert'); +var start = require('./common'); +var mongoose = start.mongoose; +var Schema = mongoose.Schema; +var SchemaType = mongoose.SchemaType; +var ValidatorError = SchemaType.ValidatorError; +var ValidationError = require('../lib/error/validation'); describe('ValidationError', function() { describe('#infiniteRecursion', function() { it('does not cause RangeError (gh-1834)', function(done) { - var SubSchema - , M - , model; + var SubSchema, + M, + model; SubSchema = new Schema({ name: {type: String, required: true}, contents: [new Schema({ - key: {type: String, required: true}, + key: {type: String, required: true}, value: {type: String, required: true} }, {_id: false})] }); @@ -30,7 +31,7 @@ describe('ValidationError', function() { model = new M({ name: 'Model', contents: [ - { key: 'foo' } + {key: 'foo'} ] }); @@ -45,12 +46,12 @@ describe('ValidationError', function() { describe('#minDate', function() { it('causes a validation error', function(done) { - var MinSchema - , M - , model; + var MinSchema, + M, + model; MinSchema = new Schema({ - appointmentDate : { type: Date, min: Date.now } + appointmentDate: {type: Date, min: Date.now} }); M = mongoose.model('MinSchema', MinSchema); @@ -59,12 +60,12 @@ describe('ValidationError', function() { appointmentDate: new Date(Date.now().valueOf() - 10000) }); - //should fail validation + // should fail validation model.validate(function(err) { assert.notEqual(err, null, 'min Date validation failed.'); model.appointmentDate = new Date(Date.now().valueOf() + 10000); - //should pass validation + // should pass validation model.validate(function(err) { assert.equal(err, null); done(); @@ -75,12 +76,12 @@ describe('ValidationError', function() { describe('#maxDate', function() { it('causes a validation error', function(done) { - var MaxSchema - , M - , model; + var MaxSchema, + M, + model; MaxSchema = new Schema({ - birthdate : { type: Date, max: Date.now } + birthdate: {type: Date, max: Date.now} }); M = mongoose.model('MaxSchema', MaxSchema); @@ -89,12 +90,12 @@ describe('ValidationError', function() { birthdate: new Date(Date.now().valueOf() + 2000) }); - //should fail validation + // should fail validation model.validate(function(err) { assert.notEqual(err, null, 'max Date validation failed'); model.birthdate = Date.now(); - //should pass validation + // should pass validation model.validate(function(err) { assert.equal(err, null, 'max Date validation failed'); done(); @@ -105,12 +106,12 @@ describe('ValidationError', function() { describe('#minlength', function() { it('causes a validation error', function(done) { - var AddressSchema - , Address - , model; + var AddressSchema, + Address, + model; AddressSchema = new Schema({ - postalCode : { type: String, minlength: 5 } + postalCode: {type: String, minlength: 5} }); Address = mongoose.model('MinLengthAddress', AddressSchema); @@ -119,28 +120,54 @@ describe('ValidationError', function() { postalCode: '9512' }); - //should fail validation + // should fail validation model.validate(function(err) { assert.notEqual(err, null, 'String minlegth validation failed.'); model.postalCode = '95125'; - //should pass validation + // should pass validation model.validate(function(err) { assert.equal(err, null); done(); }); }); }); + + it('with correct error message (gh-4207)', function(done) { + var old = mongoose.Error.messages; + mongoose.Error.messages = { + 'String': { + minlength: 'woops!' + } + }; + + var AddressSchema = new Schema({ + postalCode: { type: String, minlength: 5 } + }); + + var Address = mongoose.model('gh4207', AddressSchema); + + var model = new Address({ + postalCode: '9512' + }); + + // should fail validation + model.validate(function(err) { + assert.equal(err.errors['postalCode'].message, 'woops!'); + mongoose.Error.messages = old; + done(); + }); + }); }); describe('#maxlength', function() { it('causes a validation error', function(done) { - var AddressSchema - , Address - , model; + var AddressSchema, + Address, + model; AddressSchema = new Schema({ - postalCode : { type: String, maxlength: 10 } + postalCode: {type: String, maxlength: 10} }); Address = mongoose.model('MaxLengthAddress', AddressSchema); @@ -149,12 +176,12 @@ describe('ValidationError', function() { postalCode: '95125012345' }); - //should fail validation + // should fail validation model.validate(function(err) { assert.notEqual(err, null, 'String maxlegth validation failed.'); model.postalCode = '95125'; - //should pass validation + // should pass validation model.validate(function(err) { assert.equal(err, null); done(); @@ -166,8 +193,8 @@ describe('ValidationError', function() { describe('#toString', function() { it('does not cause RangeError (gh-1296)', function(done) { var ASchema = new Schema({ - key: {type: String, required: true} - , value: {type:String, required: true} + key: {type: String, required: true}, + value: {type: String, required: true} }); var BSchema = new Schema({ @@ -176,7 +203,7 @@ describe('ValidationError', function() { var M = mongoose.model('A', BSchema); var m = new M; - m.contents.push({ key: 'asdf' }); + m.contents.push({key: 'asdf'}); m.validate(function(err) { assert.doesNotThrow(function() { String(err); @@ -188,12 +215,29 @@ describe('ValidationError', function() { describe('formatMessage', function() { it('replaces properties in a message', function(done) { - var props = { base: 'eggs', topping: 'bacon' }; + var props = {base: 'eggs', topping: 'bacon'}; var message = 'I had {BASE} and {TOPPING} for breakfast'; var result = ValidatorError.prototype.formatMessage(message, props); - assert.equal('I had eggs and bacon for breakfast', result); + assert.equal(result, 'I had eggs and bacon for breakfast'); done(); }); }); + + it('JSON.stringify() with message (gh-5309)', function(done) { + model.modelName = 'TestClass'; + var err = new ValidationError(new model()); + + err.addError('test', { message: 'Fail' }); + + var obj = JSON.parse(JSON.stringify(err)); + assert.ok(obj.message.indexOf('TestClass validation failed') !== -1, + obj.message); + assert.ok(obj.message.indexOf('test: Fail') !== -1, + obj.message); + + done(); + + function model() {} + }); }); diff --git a/test/es6.test.js b/test/es6.test.js new file mode 100644 index 00000000000..545755b8825 --- /dev/null +++ b/test/es6.test.js @@ -0,0 +1,5 @@ +'use strict'; + +if (parseInt(process.versions.node.split('.')[0], 10) >= 4) { + require('./es6/all.test.es6.js'); +} diff --git a/test/es6/all.test.es6.js b/test/es6/all.test.es6.js new file mode 100644 index 00000000000..9ca1b22f6e9 --- /dev/null +++ b/test/es6/all.test.es6.js @@ -0,0 +1,69 @@ +'use strict'; + +/** + * Module dependencies. + */ + +const assert = require('assert'); +const start = require('../common'); + +const mongoose = start.mongoose; + +describe('bug fixes', function() { + let db; + + before(function() { + db = start(); + }); + + it('discriminators with classes modifies class in place (gh-5175)', function(done) { + class Vehicle extends mongoose.Model { } + var V = mongoose.model(Vehicle, new mongoose.Schema()); + assert.ok(V === Vehicle); + class Car extends Vehicle { } + var C = Vehicle.discriminator(Car, new mongoose.Schema()); + assert.ok(C === Car); + done(); + }); + + it('allows overwriting base class methods (gh-5227)', function(done) { + class BaseModel extends mongoose.Model { + getString() { + return 'parent'; + } + } + + class GH5227 extends BaseModel { + getString() { + return 'child'; + } + } + + const UserModel = mongoose.model(GH5227, new mongoose.Schema({})); + + const u = new UserModel({}); + + assert.equal(u.getString(), 'child'); + + done(); + }); + + it('supports adding properties (gh-5104) (gh-5635)', function(done) { + class Shape extends mongoose.Model { }; + class Circle extends Shape { }; + + const ShapeModel = mongoose.model(Shape, new mongoose.Schema({ + color: String + })); + + const CircleModel = ShapeModel.discriminator(Circle, new mongoose.Schema({ + radius: Number + })); + + const circle = new Circle({ color: 'blue', radius: 3 }); + assert.equal(circle.color, 'blue'); + assert.equal(circle.radius, 3); + + done(); + }); +}); diff --git a/test/gh-1408.test.js b/test/gh-1408.test.js index 8f4dcfdf50a..a6fda347d16 100644 --- a/test/gh-1408.test.js +++ b/test/gh-1408.test.js @@ -3,20 +3,22 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , Schema = mongoose.Schema; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + Schema = mongoose.Schema; describe('documents should not be converted to _id (gh-1408)', function() { it('if an embedded doc', function(done) { + this.timeout(process.env.TRAVIS ? 8000 : 4500); + var db = start(); var PreferenceSchema = new Schema({ _id: {type: Schema.ObjectId, auto: true}, - preference: { type: String, required: true }, - value: { type: Schema.Types.Mixed } - }, { versionKey: false }); + preference: {type: String, required: true}, + value: {type: Schema.Types.Mixed} + }, {versionKey: false}); var BrandSchema = new Schema({ settings: { @@ -29,10 +31,10 @@ describe('documents should not be converted to _id (gh-1408)', function() { var a = new A({ settings: { preferences: - [ { preference: 'group_colors', value: false }, - { preference: 'can_force_orders', value: true }, - { preference: 'hide_from_buyers', value: true }, - { preference: 'no_orders', value: '' } + [{preference: 'group_colors', value: false}, + {preference: 'can_force_orders', value: true}, + {preference: 'hide_from_buyers', value: true}, + {preference: 'no_orders', value: ''} ] } }); @@ -46,19 +48,18 @@ describe('documents should not be converted to _id (gh-1408)', function() { var newData = { settings: { preferences: - [ { preference: 'group_colors', value: true }, - { preference: 'can_force_orders', value: true }, - { preference: 'custom_csv', value: '' }, - { preference: 'hide_from_buyers', value: false }, - { preference: 'nozoom', value: false }, - { preference: 'no_orders', value: false } + [{preference: 'group_colors', value: true}, + {preference: 'can_force_orders', value: true}, + {preference: 'custom_csv', value: ''}, + {preference: 'hide_from_buyers', value: false}, + {preference: 'nozoom', value: false}, + {preference: 'no_orders', value: false} ] } }; - doc.set('settings', newData.settings, { merge: true }); + doc.set('settings', newData.settings, {merge: true}); doc.markModified('settings'); // <== this caused the bug - doc.save(function(err) { if (err) return done(err); diff --git a/test/harmony/.eslintrc b/test/harmony/.eslintrc.yml similarity index 100% rename from test/harmony/.eslintrc rename to test/harmony/.eslintrc.yml diff --git a/test/harmony/document.test_.js b/test/harmony/document.test_.js index 51df9096e4d..977037b7352 100644 --- a/test/harmony/document.test_.js +++ b/test/harmony/document.test_.js @@ -3,7 +3,7 @@ var mongoose = start.mongoose; var Schema = mongoose.Schema; var ValidationError = require('../../lib/error/validation'); var co = require('co'); -var assert = require('assert'); +var assert = require('power-assert'); /** * Asynchronous document functions return @@ -23,7 +23,7 @@ describe('Documents in ES6', function() { }; beforeEach(function() { - db = start({ noErrorListener: 1 }); + db = start({noErrorListener: 1}); }); afterEach(function(done) { @@ -46,12 +46,12 @@ describe('Documents in ES6', function() { }; schema = new Schema({ - eggs: { type: String, required: true, validate: validate }, - bacon: { type: Boolean, required: true } + eggs: {type: String, required: true, validate: validate}, + bacon: {type: Boolean, required: true} }); var M = db.model('validateSchema', schema, getCollectionName()); - var m = new M({ eggs: 'Sunny side up', bacon: false }); + var m = new M({eggs: 'Sunny side up', bacon: false}); try { yield m.validate(); @@ -60,7 +60,7 @@ describe('Documents in ES6', function() { } assert.ok(!error); - assert.equal(true, called); + assert.equal(called, true); called = false; // The validator function above should now fail @@ -82,12 +82,12 @@ describe('Documents in ES6', function() { co(function*() { var error; var schema = new Schema({ - description: { type: String, required: true } + description: {type: String, required: true} }); var Breakfast = db.model('breakfast', schema, getCollectionName()); - var goodBreakfast = new Breakfast({ description: 'eggs & bacon' }); + var goodBreakfast = new Breakfast({description: 'eggs & bacon'}); try { yield goodBreakfast.save(); @@ -103,7 +103,7 @@ describe('Documents in ES6', function() { error = e; } assert.ifError(error); - assert.equal('eggs & bacon', result.description); + assert.equal(result.description, 'eggs & bacon'); // Should cause a validation error because `description` is required var badBreakfast = new Breakfast({}); @@ -131,7 +131,7 @@ describe('Documents in ES6', function() { var breakfastCollectionName = getCollectionName(); var foodCollectionName = getCollectionName(); var breakfastSchema = new Schema({ - foods: [{ type: mongoose.Schema.ObjectId, ref: foodCollectionName }] + foods: [{type: mongoose.Schema.ObjectId, ref: foodCollectionName}] }); var foodSchema = new Schema({ @@ -141,9 +141,9 @@ describe('Documents in ES6', function() { var Food = db.model(foodCollectionName, foodSchema, foodCollectionName); var Breakfast = db.model(breakfastCollectionName, breakfastSchema, breakfastCollectionName); - var bacon = new Food({ name: 'bacon' }); - var eggs = new Food({ name: 'eggs' }); - var goodBreakfast = new Breakfast({ foods: [bacon, eggs] }); + var bacon = new Food({name: 'bacon'}); + var eggs = new Food({name: 'eggs'}); + var goodBreakfast = new Breakfast({foods: [bacon, eggs]}); try { yield [bacon.save(), eggs.save(), goodBreakfast.save()]; @@ -158,7 +158,7 @@ describe('Documents in ES6', function() { error = e; } assert.ifError(error); - assert.equal(2, result.foods.length); + assert.equal(result.foods.length, 2); try { result = yield result.populate('foods').execPopulate(); @@ -166,9 +166,9 @@ describe('Documents in ES6', function() { error = e; } assert.ifError(error); - assert.equal(2, result.foods.length); - assert.equal('bacon', result.foods[0].name); - assert.equal('eggs', result.foods[1].name); + assert.equal(result.foods.length, 2); + assert.equal(result.foods[0].name, 'bacon'); + assert.equal(result.foods[1].name, 'eggs'); done(); })(); @@ -187,7 +187,7 @@ describe('Documents in ES6', function() { var error; try { - yield breakfast.update({ steak: 'Ribeye', eggs: 'Scrambled' }, { upsert: true }).exec(); + yield breakfast.update({steak: 'Ribeye', eggs: 'Scrambled'}, {upsert: true}).exec(); } catch (e) { error = e; } @@ -201,9 +201,10 @@ describe('Documents in ES6', function() { } assert.ifError(error); assert.equal(breakfast._id.toString(), result._id.toString()); - assert.equal('Ribeye', result.steak); - assert.equal('Scrambled', result.eggs); + assert.equal(result.steak, 'Ribeye'); + assert.equal(result.eggs, 'Scrambled'); done(); })(); }); }); + diff --git a/test/harmony/model.test_.js b/test/harmony/model.test_.js index a289f0ca571..4a8464e7db8 100644 --- a/test/harmony/model.test_.js +++ b/test/harmony/model.test_.js @@ -3,7 +3,7 @@ var start = require('../common'); var mongoose = start.mongoose; var Schema = mongoose.Schema; var co = require('co'); -var assert = require('assert'); +var assert = require('power-assert'); /** * Asynchronous functions on Model return @@ -37,8 +37,8 @@ describe('Models in ES6', function() { it('`create()` integrates with co and the yield keyword', function(done) { co(function * () { var schema = new Schema({ - eggs: { type: String, required: true }, - bacon: { type: Boolean, required: true } + eggs: {type: String, required: true}, + bacon: {type: Boolean, required: true} }); var M = db.model('harmonyCreate', schema, getCollectionName()); @@ -46,15 +46,15 @@ describe('Models in ES6', function() { var results; try { results = yield M.create([ - { eggs: 'sunny-side up', bacon: false }, - { eggs: 'scrambled', bacon: true }]); + {eggs: 'sunny-side up', bacon: false}, + {eggs: 'scrambled', bacon: true}]); } catch (e) { return done(e); } - assert.equal(2, results.length); - assert.equal('sunny-side up', results[0].eggs); - assert.equal('scrambled', results[1].eggs); + assert.equal(results.length, 2); + assert.equal(results[0].eggs, 'sunny-side up'); + assert.equal(results[1].eggs, 'scrambled'); done(); })(); @@ -63,16 +63,16 @@ describe('Models in ES6', function() { it('`aggregate()` integrates with co and the yield keyword', function(done) { co(function*() { var schema = new Schema({ - eggs: { type: String, required: true }, - bacon: { type: Boolean, required: true } + eggs: {type: String, required: true}, + bacon: {type: Boolean, required: true} }); var M = db.model('harmonyAggregate', schema, getCollectionName()); try { yield M.create([ - { eggs: 'sunny-side up', bacon: false }, - { eggs: 'scrambled', bacon: true }]); + {eggs: 'sunny-side up', bacon: false}, + {eggs: 'scrambled', bacon: true}]); } catch (e) { return done(e); } @@ -80,18 +80,18 @@ describe('Models in ES6', function() { var results; try { results = yield M.aggregate([ - { $group: { _id: '$bacon', eggs: { $first: '$eggs' } } }, - { $sort: { _id: 1 } } + {$group: {_id: '$bacon', eggs: {$first: '$eggs'}}}, + {$sort: {_id: 1}} ]).exec(); } catch (e) { return done(e); } - assert.equal(2, results.length); - assert.equal(false, results[0]._id); - assert.equal('sunny-side up', results[0].eggs); - assert.equal(true, results[1]._id); - assert.equal('scrambled', results[1].eggs); + assert.equal(results.length, 2); + assert.equal(results[0]._id, false); + assert.equal(results[0].eggs, 'sunny-side up'); + assert.equal(results[1]._id, true); + assert.equal(results[1].eggs, 'scrambled'); done(); })(); @@ -100,17 +100,17 @@ describe('Models in ES6', function() { it('`mapReduce()` can also be used with co and yield', function(done) { co(function*() { var schema = new Schema({ - eggs: { type: String, required: true }, - bacon: { type: Boolean, required: true } + eggs: {type: String, required: true}, + bacon: {type: Boolean, required: true} }); var M = db.model('harmonyMapreduce', schema, getCollectionName()); try { yield M.create([ - { eggs: 'sunny-side up', bacon: false }, - { eggs: 'sunny-side up', bacon: true }, - { eggs: 'scrambled', bacon: true }]); + {eggs: 'sunny-side up', bacon: false}, + {eggs: 'sunny-side up', bacon: true}, + {eggs: 'scrambled', bacon: true}]); } catch (e) { return done(e); } @@ -125,7 +125,7 @@ describe('Models in ES6', function() { return done(e); } - assert.equal(2, results.length); + assert.equal(results.length, 2); assert.ok(results[0]._id === 'sunny-side up' || results[1]._id === 'sunny-side up'); assert.ok(results[0]._id === 'scrambled' || results[1]._id === 'scrambled'); @@ -133,3 +133,4 @@ describe('Models in ES6', function() { })(); }); }); + diff --git a/test/harmony/query.test_.js b/test/harmony/query.test_.js index d5f9899fe3b..f637cad808c 100644 --- a/test/harmony/query.test_.js +++ b/test/harmony/query.test_.js @@ -2,7 +2,7 @@ var start = require('../common'); var mongoose = start.mongoose; var Schema = mongoose.Schema; var co = require('co'); -var assert = require('assert'); +var assert = require('power-assert'); /** * Mongoose queries' .exec() function returns a @@ -32,49 +32,49 @@ describe('Queries in ES6', function() { it('`exec()` integrates with co and the yield keyword', function(done) { co(function*() { var schema = new Schema({ - eggs: { type: Number, required: true }, - bacon: { type: Number, required: true } + eggs: {type: Number, required: true}, + bacon: {type: Number, required: true} }); var Breakfast = db.model('BreakfastHarmony', schema, getCollectionName()); try { yield Breakfast.create( - { eggs: 4, bacon: 2 }, - { eggs: 3, bacon: 3 }, - { eggs: 2, bacon: 4 }); + {eggs: 4, bacon: 2}, + {eggs: 3, bacon: 3}, + {eggs: 2, bacon: 4}); } catch (e) { return done(e); } var result; try { - result = yield Breakfast.findOne({ eggs: 4 }).exec(); + result = yield Breakfast.findOne({eggs: 4}).exec(); } catch (e) { return done(e); } - assert.equal(2, result.bacon); + assert.equal(result.bacon, 2); var results; try { - results = yield Breakfast.find({ eggs: { $gt: 2 } }).sort({ bacon: 1 }).exec(); + results = yield Breakfast.find({eggs: {$gt: 2}}).sort({bacon: 1}).exec(); } catch (e) { return done(e); } - assert.equal(2, results.length); - assert.equal(2, results[0].bacon); - assert.equal(3, results[1].bacon); + assert.equal(results.length, 2); + assert.equal(results[0].bacon, 2); + assert.equal(results[1].bacon, 3); var count; try { - count = yield Breakfast.count({ eggs: { $gt: 2 } }).exec(); + count = yield Breakfast.count({eggs: {$gt: 2}}).exec(); } catch (e) { return done(e); } - assert.equal(2, count); + assert.equal(count, 2); done(); })(); @@ -83,7 +83,7 @@ describe('Queries in ES6', function() { it('can call `populate()` with `exec()`', function(done) { co(function*() { var bookSchema = new Schema({ - author: { type: mongoose.Schema.ObjectId, ref: 'AuthorHarmony' }, + author: {type: mongoose.Schema.ObjectId, ref: 'AuthorHarmony'}, title: String }); @@ -95,22 +95,23 @@ describe('Queries in ES6', function() { var Author = db.model('AuthorHarmony', authorSchema, getCollectionName()); try { - var hugo = yield Author.create({ name: 'Victor Hugo' }); - yield Book.create({ author: hugo._id, title: 'Les Miserables' }); + var hugo = yield Author.create({name: 'Victor Hugo'}); + yield Book.create({author: hugo._id, title: 'Les Miserables'}); } catch (e) { return done(e); } var result; try { - result = yield Book.findOne({ title: 'Les Miserables' }).populate('author').exec(); + result = yield Book.findOne({title: 'Les Miserables'}).populate('author').exec(); } catch (e) { return done(e); } - assert.equal('Victor Hugo', result.author.name); + assert.equal(result.author.name, 'Victor Hugo'); done(); })(); }); }); + diff --git a/test/helpers/populate.getSchemaTypes.test.js b/test/helpers/populate.getSchemaTypes.test.js new file mode 100644 index 00000000000..fc742684ca6 --- /dev/null +++ b/test/helpers/populate.getSchemaTypes.test.js @@ -0,0 +1,137 @@ +'use strict'; + +var Schema = require('../../lib/schema'); +var assert = require('assert'); +var getSchemaTypes = require('../../lib/services/populate/getSchemaTypes'); + +describe('getSchemaTypes', function() { + it('handles embedded discriminators (gh-5970)', function(done) { + var ItemSchema = new Schema({ + title: { + type: String, + required: true + } + }, {discriminatorKey: 'type'}); + + var ItemBookSchema = new Schema({ + author: { + type: String, + ref: 'Ref1' + } + }); + + var ItemEBookSchema = new Schema({ + author: { + type: String, + ref: 'Ref2' + } + }); + + var OtherItem = new Schema({ name: String }); + + var BundleSchema = new Schema({ + items: [{ + type: ItemSchema, + required: false + }] + }); + + BundleSchema.path('items').discriminator('Book', ItemBookSchema); + BundleSchema.path('items').discriminator('EBook', ItemEBookSchema); + BundleSchema.path('items').discriminator('Other', OtherItem); + + var doc = { + items: [ + { type: 'Book', author: 'test 1' }, + { type: 'EBook', author: 'test 2' }, + { type: 'Other' } + ] + }; + + var schemaTypes = getSchemaTypes(BundleSchema, doc, 'items.author'); + + assert.ok(Array.isArray(schemaTypes)); + // Make sure we only got the schema paths for Book and EBook, and none + // for the 'Other' + assert.equal(schemaTypes.length, 2); + assert.equal(schemaTypes[0].options.ref, 'Ref1'); + assert.equal(schemaTypes[1].options.ref, 'Ref2'); + + done(); + }); + + it('multiple embedded discriminators (gh-6064)', function(done) { + var ItemSchema = new Schema({ + title: { + type: String, + required: true + } + }, {discriminatorKey: 'type'}); + + var ItemBookSchema = new Schema({ + author: { + type: String, + ref: 'Ref1' + } + }); + + var ItemEBookSchema = new Schema({ + author: { + type: String, + ref: 'Ref2' + } + }); + + var OtherItem = new Schema({ name: String }); + + var BundleSchema = new Schema({ + level1: { + items: [{ + type: ItemSchema, + required: false + }] + }, + level2: { + items: [{ + type: ItemSchema, + required: false + }] + } + }); + + BundleSchema.path('level1.items').discriminator('Book', ItemBookSchema); + BundleSchema.path('level1.items').discriminator('EBook', ItemEBookSchema); + BundleSchema.path('level1.items').discriminator('Other', OtherItem); + + // HERE IS THE ADDED DISCRIMINATOR PATH + BundleSchema.path('level2.items').discriminator('Book', ItemBookSchema); + BundleSchema.path('level2.items').discriminator('EBook', ItemEBookSchema); + BundleSchema.path('level2.items').discriminator('Other', OtherItem); + + var doc = { + level1: { + items: [ + { type: 'Book', author: 'level 1 test 1' }, + { type: 'EBook', author: 'level 1 test 2' } + ] + }, + level2: { + items: [ + { type: 'EBook', author: 'level 2 test 1' }, + { type: 'Book', author: 'level 2 test 2' }, + { type: 'Other' } + ] + } + }; + var schemaTypes = getSchemaTypes(BundleSchema, doc, 'level2.items.author'); + + assert.ok(Array.isArray(schemaTypes)); + // Make sure we only got the schema paths for Book and EBook, and none + // for the 'Other' + assert.equal(schemaTypes.length, 2); + assert.equal(schemaTypes[0].options.ref, 'Ref1'); + assert.equal(schemaTypes[1].options.ref, 'Ref2'); + + done(); + }); +}); diff --git a/test/index.test.js b/test/index.test.js index 56f40831edb..bfc8d7ae6e6 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,19 +1,19 @@ -var url = require('url') - , start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , Mongoose = mongoose.Mongoose - , Schema = mongoose.Schema - , random = require('../lib/utils').random - , collection = 'blogposts_' + random(); +var url = require('url'), + start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + Mongoose = mongoose.Mongoose, + Schema = mongoose.Schema, + random = require('../lib/utils').random, + collection = 'blogposts_' + random(); describe('mongoose module:', function() { describe('default connection works', function() { it('without options', function(done) { var goose = new Mongoose; - var db = goose.connection - , uri = 'mongodb://localhost/mongoose_test'; + var db = goose.connection, + uri = 'mongodb://localhost/mongoose_test'; goose.connect(process.env.MONGOOSE_TEST_URI || uri); @@ -26,10 +26,10 @@ describe('mongoose module:', function() { it('with options', function(done) { var goose = new Mongoose; - var db = goose.connection - , uri = 'mongodb://localhost/mongoose_test'; + var db = goose.connection, + uri = 'mongodb://localhost/mongoose_test'; - goose.connect(process.env.MONGOOSE_TEST_URI || uri, {db:{safe:false}}); + goose.connect(process.env.MONGOOSE_TEST_URI || uri, {db: {safe: false}}); db.on('open', function() { db.close(function() { @@ -37,6 +37,16 @@ describe('mongoose module:', function() { }); }); }); + + it('with promise (gh-3790)', function(done) { + var goose = new Mongoose; + var db = goose.connection, + uri = 'mongodb://localhost/mongoose_test'; + + goose.connect(process.env.MONGOOSE_TEST_URI || uri).then(function() { + db.close(done); + }); + }); }); it('{g,s}etting options', function(done) { @@ -45,20 +55,31 @@ describe('mongoose module:', function() { mongoose.set('a', 'b'); mongoose.set('long option', 'c'); - assert.equal('b', mongoose.get('a')); - assert.equal('b', mongoose.set('a')); - assert.equal('c', mongoose.get('long option')); + assert.equal(mongoose.get('a'), 'b'); + assert.equal(mongoose.set('a'), 'b'); + assert.equal(mongoose.get('long option'), 'c'); done(); }); - it('declaring global plugins', function(done) { - var mong = new Mongoose() - , schema = new Schema() - , called = 0; + it('declaring global plugins (gh-5690)', function(done) { + var mong = new Mongoose(); + var subSchema = new Schema({ name: String }); + var schema = new Schema({ + test: [subSchema] + }); + var called = 0; + var calls = []; + var preSaveCalls = 0; mong.plugin(function(s) { - assert.equal(s, schema); - called++; + calls.push(s); + + s.pre('save', function(next) { + ++preSaveCalls; + next(); + }); + + s.methods.testMethod = function() { return 42; }; }); schema.plugin(function(s) { @@ -66,28 +87,43 @@ describe('mongoose module:', function() { called++; }); - mong.model('GlobalPlugins', schema); - - assert.equal(2, called); - done(); + var M = mong.model('GlobalPlugins', schema); + + assert.equal(called, 1); + assert.equal(calls.length, 2); + assert.equal(calls[0], schema); + assert.equal(calls[1], subSchema); + + assert.equal(preSaveCalls, 0); + mong.connect(start.uri, { useMongoClient: true }); + M.create({ test: [{ name: 'Val' }] }, function(error, doc) { + assert.ifError(error); + assert.equal(preSaveCalls, 2); + assert.equal(doc.testMethod(), 42); + assert.equal(doc.test[0].testMethod(), 42); + mong.disconnect(); + done(); + }); }); describe('disconnection of all connections', function() { + this.timeout(10000); + describe('no callback', function() { it('works', function(done) { - var mong = new Mongoose() - , uri = 'mongodb://localhost/mongoose_test' - , connections = 0 - , disconnections = 0 - , pending = 4; + var mong = new Mongoose(), + uri = 'mongodb://localhost/mongoose_test', + connections = 0, + disconnections = 0, + pending = 4; mong.connect(process.env.MONGOOSE_TEST_URI || uri); var db = mong.connection; function cb() { if (--pending) return; - assert.equal(2, connections); - assert.equal(2, disconnections); + assert.equal(connections, 2); + assert.equal(disconnections, 2); done(); } @@ -117,8 +153,8 @@ describe('mongoose module:', function() { }); it('properly handles errors', function(done) { - var mong = new Mongoose() - , uri = 'mongodb://localhost/mongoose_test'; + var mong = new Mongoose(), + uri = 'mongodb://localhost/mongoose_test'; mong.connect(process.env.MONGOOSE_TEST_URI || uri); var db = mong.connection; @@ -128,20 +164,18 @@ describe('mongoose module:', function() { cb(new Error('bam')); }; - var failure = {}; - try { - mong.disconnect(); - } catch (err) { - failure = err; - } - assert.equal('bam', failure.message); + mong.disconnect().connection. + on('error', function(error) { + assert.equal(error.message, 'bam'); + }); + done(); }); }); it('with callback', function(done) { - var mong = new Mongoose() - , uri = 'mongodb://localhost/mongoose_test'; + var mong = new Mongoose(), + uri = 'mongodb://localhost/mongoose_test'; mong.connect(process.env.MONGOOSE_TEST_URI || uri); @@ -151,12 +185,23 @@ describe('mongoose module:', function() { }); }); }); + + it('with promise (gh-3790)', function(done) { + var mong = new Mongoose(); + var uri = 'mongodb://localhost/mongoose_test'; + + mong.connect(process.env.MONGOOSE_TEST_URI || uri); + + mong.connection.on('open', function() { + mong.disconnect().then(function() { done(); }); + }); + }); }); describe('model()', function() { it('accessing a model that hasn\'t been defined', function(done) { - var mong = new Mongoose() - , thrown = false; + var mong = new Mongoose(), + thrown = false; try { mong.model('Test'); @@ -165,21 +210,21 @@ describe('mongoose module:', function() { thrown = true; } - assert.equal(true, thrown); + assert.equal(thrown, true); done(); }); it('returns the model at creation', function(done) { - var Named = mongoose.model('Named', new Schema({ name: String })); + var Named = mongoose.model('Named', new Schema({name: String})); var n1 = new Named(); assert.equal(n1.name, null); - var n2 = new Named({ name: 'Peter Bjorn' }); + var n2 = new Named({name: 'Peter Bjorn'}); assert.equal(n2.name, 'Peter Bjorn'); - var schema = new Schema({ number: Number }); + var schema = new Schema({number: Number}); var Numbered = mongoose.model('Numbered', schema, collection); - var n3 = new Numbered({ number: 1234 }); - assert.equal(1234, n3.number.valueOf()); + var n3 = new Numbered({number: 1234}); + assert.equal(n3.number.valueOf(), 1234); done(); }); @@ -218,15 +263,15 @@ describe('mongoose module:', function() { describe('when model name already exists', function() { it('returns a new uncached model', function(done) { var m = new Mongoose; - var s1 = new Schema({ a: [] }); + var s1 = new Schema({a: []}); var name = 'non-cached-collection-name'; var A = m.model(name, s1); var B = m.model(name); var C = m.model(name, 'alternate'); - assert.ok(A.collection.name == B.collection.name); - assert.ok(A.collection.name != C.collection.name); - assert.ok(m.models[name].collection.name != C.collection.name); - assert.ok(m.models[name].collection.name == A.collection.name); + assert.ok(A.collection.name === B.collection.name); + assert.ok(A.collection.name !== C.collection.name); + assert.ok(m.models[name].collection.name !== C.collection.name); + assert.ok(m.models[name].collection.name === A.collection.name); done(); }); }); @@ -235,8 +280,8 @@ describe('mongoose module:', function() { describe('passing object literal schemas', function() { it('works', function(done) { var m = new Mongoose; - var A = m.model('A', { n: [{ age: 'number' }]}); - var a = new A({ n: [{ age: '47' }] }); + var A = m.model('A', {n: [{age: 'number'}]}); + var a = new A({n: [{age: '47'}]}); assert.strictEqual(47, a.n[0].age); done(); }); @@ -244,8 +289,8 @@ describe('mongoose module:', function() { }); it('connecting with a signature of host, database, function', function(done) { - var mong = new Mongoose() - , uri = process.env.MONGOOSE_TEST_URI || 'mongodb://localhost/mongoose_test'; + var mong = new Mongoose(), + uri = process.env.MONGOOSE_TEST_URI || 'mongodb://localhost/mongoose_test'; uri = url.parse(uri); @@ -258,10 +303,10 @@ describe('mongoose module:', function() { describe('connecting with a signature of uri, options, function', function() { it('with single mongod', function(done) { - var mong = new Mongoose() - , uri = process.env.MONGOOSE_TEST_URI || 'mongodb://localhost/mongoose_test'; + var mong = new Mongoose(), + uri = process.env.MONGOOSE_TEST_URI || 'mongodb://localhost/mongoose_test'; - mong.connect(uri, { db: { safe: false }}, function(err) { + mong.connect(uri, {db: {safe: false}}, function(err) { assert.ifError(err); mong.connection.close(); done(); @@ -269,12 +314,12 @@ describe('mongoose module:', function() { }); it('with replica set', function(done) { - var mong = new Mongoose() - , uri = process.env.MONGOOSE_SET_TEST_URI; + var mong = new Mongoose(), + uri = process.env.MONGOOSE_SET_TEST_URI; if (!uri) return done(); - mong.connect(uri, { db: { safe: false }}, function(err) { + mong.connect(uri, {db: {safe: false}}, function(err) { assert.ifError(err); mong.connection.close(); done(); @@ -302,8 +347,8 @@ describe('mongoose module:', function() { test: String })); - var Test = mong.model('Test') - , test = new Test(); + var Test = mong.model('Test'), + test = new Test(); test.test = 'aa'; test.save(function(err) { @@ -311,7 +356,7 @@ describe('mongoose module:', function() { Test.findById(test._id, function(err, doc) { assert.ifError(err); - assert.equal('aa', doc.test); + assert.equal(doc.test, 'aa'); mong.connection.close(); complete(); }); @@ -341,8 +386,8 @@ describe('mongoose module:', function() { test: String })); - var Test = conn.model('ReplSetTwo') - , test = new Test(); + var Test = conn.model('ReplSetTwo'), + test = new Test(); test.test = 'aa'; test.save(function(err) { @@ -350,7 +395,7 @@ describe('mongoose module:', function() { Test.findById(test._id, function(err, doc) { assert.ifError(err); - assert.equal('aa', doc.test); + assert.equal(doc.test, 'aa'); conn.close(); complete(); }); @@ -368,31 +413,39 @@ describe('mongoose module:', function() { describe('exports', function() { function test(mongoose) { - assert.equal('string', typeof mongoose.version); - assert.equal('function', typeof mongoose.Mongoose); - assert.equal('function', typeof mongoose.Collection); - assert.equal('function', typeof mongoose.Connection); - assert.equal('function', typeof mongoose.Schema); - assert.equal('function', typeof mongoose.SchemaType); - assert.equal('function', typeof mongoose.Query); - assert.equal('function', typeof mongoose.Promise); - assert.equal('function', typeof mongoose.Model); - assert.equal('function', typeof mongoose.Document); - assert.equal('function', typeof mongoose.Error); - assert.equal('function', typeof mongoose.Error.CastError); - assert.equal('function', typeof mongoose.Error.ValidationError); - assert.equal('function', typeof mongoose.Error.ValidatorError); - assert.equal('function', typeof mongoose.Error.VersionError); + assert.equal(typeof mongoose.version, 'string'); + assert.equal(typeof mongoose.Mongoose, 'function'); + assert.equal(typeof mongoose.Collection, 'function'); + assert.equal(typeof mongoose.Connection, 'function'); + assert.equal(typeof mongoose.Schema, 'function'); + assert.ok(mongoose.Schema.Types); + assert.equal(typeof mongoose.SchemaType, 'function'); + assert.equal(typeof mongoose.Query, 'function'); + assert.equal(typeof mongoose.Promise, 'function'); + assert.equal(typeof mongoose.Model, 'function'); + assert.equal(typeof mongoose.Document, 'function'); + assert.equal(typeof mongoose.Error, 'function'); + assert.equal(typeof mongoose.Error.CastError, 'function'); + assert.equal(typeof mongoose.Error.ValidationError, 'function'); + assert.equal(typeof mongoose.Error.ValidatorError, 'function'); + assert.equal(typeof mongoose.Error.VersionError, 'function'); } it('of module', function(done) { test(mongoose); done(); }); + it('of new Mongoose instances', function(done) { test(new mongoose.Mongoose); done(); }); - }); + it('of result from .connect() (gh-3940)', function(done) { + var m = new mongoose.Mongoose; + test(m.connect('mongodb://localhost:27017')); + m.disconnect(); + done(); + }); + }); }); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 00000000000..a35a923698e --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,2 @@ +--reporter dot +--ui bdd \ No newline at end of file diff --git a/test/model.aggregate.test.js b/test/model.aggregate.test.js index d77f5709013..496773f322f 100644 --- a/test/model.aggregate.test.js +++ b/test/model.aggregate.test.js @@ -3,28 +3,30 @@ * Test dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , random = require('../lib/utils').random - , Aggregate = require('../lib/aggregate') - , Schema = mongoose.Schema; +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + random = require('../lib/utils').random, + Aggregate = require('../lib/aggregate'), + Schema = mongoose.Schema; /** * Setup. */ var userSchema = new Schema({ - name: String - , age: Number + name: String, + age: Number }); var collection = 'aggregate_' + random(); mongoose.model('Aggregate', userSchema); describe('model aggregate', function() { - var group = { $group: { _id: null, maxAge: { $max: '$age' } }}; - var project = { $project: { maxAge: 1, _id: 0 }}; + this.timeout(process.env.TRAVIS ? 8000 : 4500); + + var group = {$group: {_id: null, maxAge: {$max: '$age'}}}; + var project = {$project: {maxAge: 1, _id: 0}}; var db, A, maxAge; var mongo26_or_greater = false; @@ -41,14 +43,14 @@ describe('model aggregate', function() { for (var i = 0; i < num; ++i) { var age = Math.random() * 100 | 0; maxAge = Math.max(maxAge, age); - docs.push({ author: authors[i % authors.length], age: age }); + docs.push({author: authors[i % authors.length], age: age}); } A.create(docs, function(err) { assert.ifError(err); start.mongodVersion(function(err, version) { if (err) throw err; - mongo26_or_greater = 2 < version[0] || (2 == version[0] && 6 <= version[1]); + mongo26_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 6); if (!mongo26_or_greater) console.log('not testing mongodb 2.6 features'); done(); }); @@ -65,6 +67,16 @@ describe('model aggregate', function() { A.aggregate(group, project, function(err, res) { assert.ifError(err); + assert.ok(res); + assert.equal(res.length, 1); + assert.ok('maxAge' in res[0]); + assert.equal(res[0].maxAge, maxAge); + done(); + }); + }); + + it('when return promise', function(done) { + A.aggregate(group, project).then( function(res) { assert.ok(res); assert.equal(1, res.length); assert.ok('maxAge' in res[0]); @@ -74,21 +86,17 @@ describe('model aggregate', function() { }); it('with arrays', function(done) { - this.timeout(4000); - A.aggregate([group, project], function(err, res) { assert.ifError(err); assert.ok(res); - assert.equal(1, res.length); + assert.equal(res.length, 1); assert.ok('maxAge' in res[0]); - assert.equal(maxAge, res[0].maxAge); + assert.equal(res[0].maxAge, maxAge); done(); }); }); it('with Aggregate syntax', function(done) { - this.timeout(4000); - var promise = A.aggregate() .group(group.$group) .project(project.$project) @@ -96,16 +104,14 @@ describe('model aggregate', function() { assert.ifError(err); assert.ok(promise instanceof mongoose.Promise); assert.ok(res); - assert.equal(1, res.length); + assert.equal(res.length, 1); assert.ok('maxAge' in res[0]); - assert.equal(maxAge, res[0].maxAge); + assert.equal(res[0].maxAge, maxAge); done(); }); }); it('with Aggregate syntax if callback not provided', function(done) { - this.timeout(4000); - var promise = A.aggregate() .group(group.$group) .project(project.$project) @@ -114,7 +120,7 @@ describe('model aggregate', function() { promise.then(function(res) { assert.ok(promise instanceof mongoose.Promise); assert.ok(res); - assert.equal(1, res.length); + assert.equal(res.length, 1); assert.ok('maxAge' in res[0]); assert.equal(maxAge, res[0].maxAge); done(); @@ -131,8 +137,6 @@ describe('model aggregate', function() { return done(); } - this.timeout(4000); - var outputCollection = 'aggregate_output_' + random(); A.aggregate() .group(group.$group) @@ -142,7 +146,7 @@ describe('model aggregate', function() { assert.ifError(error); A.db.collection(outputCollection).find().toArray(function(error, documents) { assert.ifError(error); - assert.equal(1, documents.length); + assert.equal(documents.length, 1); assert.ok('maxAge' in documents[0]); assert.equal(maxAge, documents[0].maxAge); done(); diff --git a/test/model.create.test.js b/test/model.create.test.js index 3a96825bd7b..67b6c819b89 100644 --- a/test/model.create.test.js +++ b/test/model.create.test.js @@ -2,19 +2,20 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , DocumentObjectId = mongoose.Types.ObjectId; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema, + DocumentObjectId = mongoose.Types.ObjectId, + PromiseProvider = require('../lib/promise_provider'); /** * Setup */ -var schema = Schema({ - title: String +var schema = new Schema({ + title: { type: String, required: true } }); @@ -33,7 +34,7 @@ describe('model', function() { }); it('accepts an array and returns an array', function(done) { - B.create([{ title: 'hi'}, { title: 'bye'}], function(err, posts) { + B.create([{title: 'hi'}, {title: 'bye'}], function(err, posts) { assert.ifError(err); assert.ok(posts instanceof Array); @@ -41,10 +42,10 @@ describe('model', function() { var post1 = posts[0]; var post2 = posts[1]; assert.ok(post1.get('_id') instanceof DocumentObjectId); - assert.equal(post1.title,'hi'); + assert.equal(post1.title, 'hi'); assert.ok(post2.get('_id') instanceof DocumentObjectId); - assert.equal(post2.title,'bye'); + assert.equal(post2.title, 'bye'); done(); }); @@ -66,42 +67,65 @@ describe('model', function() { }); }); + it('should not cause unhandled reject promise', function(done) { + mongoose.Promise = global.Promise; + mongoose.Promise = require('bluebird'); + + B.create({title: 'reject promise'}, function(err, b) { + assert.ifError(err); + + var perr = null; + var p = B.create({_id: b._id}, function(err) { + assert(err); + setTimeout(function() { + PromiseProvider.reset(); + // perr should be null + done(perr); + }, 100); + }); + + p.catch(function(err) { + // should not go here + perr = err; + }); + }); + }); + it('returns a promise', function(done) { - var p = B.create({ title: 'returns promise' }, function() { + var p = B.create({title: 'returns promise'}, function() { assert.ok(p instanceof mongoose.Promise); done(); }); }); it('creates in parallel', function(done) { - // we set the time out to be double that of the validator - 1 (so that running in serial will be greater then that) - this.timeout(1000); - var db = start() - , countPre = 0 - , countPost = 0; + var db = start(), + countPre = 0, + countPost = 0; var SchemaWithPreSaveHook = new Schema({ preference: String }); + + var startTime, endTime; SchemaWithPreSaveHook.pre('save', true, function hook(next, done) { setTimeout(function() { countPre++; + if (countPre === 1) startTime = Date.now(); + else if (countPre === 4) endTime = Date.now(); next(); done(); - }, 500); + }, 100); }); SchemaWithPreSaveHook.post('save', function() { countPost++; }); var MWPSH = db.model('mwpsh', SchemaWithPreSaveHook); MWPSH.create([ - {preference: "xx"} - , - {preference: "yy"} - , - {preference: "1"} - , - {preference: "2"} + {preference: 'xx'}, + {preference: 'yy'}, + {preference: '1'}, + {preference: '2'} ], function(err, docs) { assert.ifError(err); @@ -117,46 +141,46 @@ describe('model', function() { assert.ok(doc4); assert.equal(countPre, 4); assert.equal(countPost, 4); - done(); + assert.ok(endTime - startTime < 4 * 100); // serial: >= 4 * 100 parallel: < 4 * 100 + db.close(done); }); }); - describe('callback is optional', function() { it('with one doc', function(done) { - var p = B.create({ title: 'optional callback' }); + var p = B.create({title: 'optional callback'}); p.then(function(doc) { - assert.equal('optional callback', doc.title); + assert.equal(doc.title, 'optional callback'); done(); }, done).end(); }); it('with more than one doc', function(done) { - var p = B.create({ title: 'optional callback 2' }, { title: 'orient expressions' }); + var p = B.create({title: 'optional callback 2'}, {title: 'orient expressions'}); p.then(function(doc1, doc2) { - assert.equal('optional callback 2', doc1.title); - assert.equal('orient expressions', doc2.title); + assert.equal(doc1.title, 'optional callback 2'); + assert.equal(doc2.title, 'orient expressions'); done(); }, done).end(); }); it('with array of docs', function(done) { - var p = B.create([{ title: 'optional callback3' }, { title: '3' }]); + var p = B.create([{title: 'optional callback3'}, {title: '3'}]); p.then(function(docs) { assert.ok(docs instanceof Array); assert.equal(docs.length, 2); var doc1 = docs[0]; var doc2 = docs[1]; - assert.equal('optional callback3', doc1.title); - assert.equal('3', doc2.title); + assert.equal(doc1.title, 'optional callback3'); + assert.equal(doc2.title, '3'); done(); }, done).end(); }); it('and should reject promise on error', function(done) { - var p = B.create({ title: 'optional callback 4' }); + var p = B.create({title: 'optional callback 4'}); p.then(function(doc) { - var p2 = B.create({ _id: doc._id }); + var p2 = B.create({_id: doc._id}); p2.then(function() { assert(false); }, function(err) { @@ -165,6 +189,15 @@ describe('model', function() { }).end(); }, done).end(); }); + + it('if callback is falsy, will ignore it (gh-5061)', function(done) { + B.create({ title: 'test' }, null). + then(function(doc) { + assert.equal(doc.title, 'test'); + done(); + }). + catch(done); + }); }); }); }); diff --git a/test/model.discriminator.querying.test.js b/test/model.discriminator.querying.test.js index 190d11aebcb..69e4732a563 100644 --- a/test/model.discriminator.querying.test.js +++ b/test/model.discriminator.querying.test.js @@ -2,13 +2,13 @@ * Test dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , Schema = mongoose.Schema - , assert = require('assert') - , random = require('../lib/utils').random - , util = require('util') - , async = require('async'); +var start = require('./common'), + mongoose = start.mongoose, + Schema = mongoose.Schema, + assert = require('power-assert'), + random = require('../lib/utils').random, + util = require('util'), + async = require('async'); /** @@ -19,24 +19,30 @@ function BaseSchema() { this.add({ name: String, - createdAt: { type: Date, default: Date.now } + createdAt: {type: Date, default: Date.now} }); } util.inherits(BaseSchema, Schema); var EventSchema = new BaseSchema(); var ImpressionEventSchema = new BaseSchema(); -var ConversionEventSchema = new BaseSchema({ revenue: Number }); +var ConversionEventSchema = new BaseSchema({revenue: Number}); +var SecretEventSchema = new BaseSchema({ secret: { type: String, select: false } }); describe('model', function() { describe('discriminator()', function() { - var db, BaseEvent, ImpressionEvent, ConversionEvent; + var db; + var BaseEvent; + var ImpressionEvent; + var ConversionEvent; + var SecretEvent; before(function() { db = start(); BaseEvent = db.model('model-discriminator-querying-event', EventSchema, 'model-discriminator-querying-' + random()); ImpressionEvent = BaseEvent.discriminator('model-discriminator-querying-impression', ImpressionEventSchema); ConversionEvent = BaseEvent.discriminator('model-discriminator-querying-conversion', ConversionEventSchema); + SecretEvent = BaseEvent.discriminator('model-discriminator-querying-secret', SecretEventSchema); }); afterEach(function(done) { @@ -71,7 +77,7 @@ describe('model', function() { BaseCustomEventSchema); DiscCustomEvent = BaseCustomEvent.discriminator('disc-custom-event', DiscCustomEventSchema); - var ContainerSchema = Schema({ + var ContainerSchema = new Schema({ title: String, events: [{type: Schema.Types.ObjectId, ref: 'base-custom-event'}] }); @@ -80,7 +86,7 @@ describe('model', function() { it('into non-discriminated arrays works', function(done) { var c = new ContainerModel({ - title: "events-group-1" + title: 'events-group-1' }); var d1 = new BaseCustomEvent(); var d2 = new BaseCustomEvent(); @@ -118,9 +124,9 @@ describe('model', function() { describe('find', function() { it('hydrates correct models', function(done) { - var baseEvent = new BaseEvent({ name: 'Base event' }); - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 }); + var baseEvent = new BaseEvent({name: 'Base event'}); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 1.337}); baseEvent.save(function(err) { assert.ifError(err); @@ -149,9 +155,9 @@ describe('model', function() { }); var checkHydratesCorrectModels = function(fields, done) { - var baseEvent = new BaseEvent({ name: 'Base event' }); - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 }); + var baseEvent = new BaseEvent({name: 'Base event'}); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 1.337}); baseEvent.save(function(err) { assert.ifError(err); @@ -191,9 +197,9 @@ describe('model', function() { var impressionEvent, conversionEvent1, conversionEvent2; before(function() { - impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - conversionEvent1 = new ConversionEvent({ name: 'Conversion event 1', revenue: 1 }); - conversionEvent2 = new ConversionEvent({ name: 'Conversion event 2', revenue: 2 }); + impressionEvent = new ImpressionEvent({name: 'Impression event'}); + conversionEvent1 = new ConversionEvent({name: 'Conversion event 1', revenue: 1}); + conversionEvent2 = new ConversionEvent({name: 'Conversion event 2', revenue: 2}); }); describe('using "ModelDiscriminator#findById"', function() { @@ -235,16 +241,16 @@ describe('model', function() { conversionEvent2.save(function(err) { assert.ifError(err); // doesn't find anything since we're querying for an impression id - var query = ConversionEvent.find({ _id: impressionEvent._id }); + var query = ConversionEvent.find({_id: impressionEvent._id}); assert.equal(query.op, 'find'); - assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'model-discriminator-querying-conversion' }); + assert.deepEqual(query._conditions, {_id: impressionEvent._id, __t: 'model-discriminator-querying-conversion'}); query.exec(function(err, documents) { assert.ifError(err); assert.equal(documents.length, 0); // now find one with no criteria given and ensure it gets added to _conditions var query = ConversionEvent.find(); - assert.deepEqual(query._conditions, { __t: 'model-discriminator-querying-conversion' }); + assert.deepEqual(query._conditions, {__t: 'model-discriminator-querying-conversion'}); assert.equal(query.op, 'find'); query.exec(function(err, documents) { assert.ifError(err); @@ -267,9 +273,9 @@ describe('model', function() { }); var checkDiscriminatorModelsFindDocumentsOfItsType = function(fields, done) { - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent1 = new ConversionEvent({ name: 'Conversion event 1', revenue: 1 }); - var conversionEvent2 = new ConversionEvent({ name: 'Conversion event 2', revenue: 2 }); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent1 = new ConversionEvent({name: 'Conversion event 1', revenue: 1}); + var conversionEvent2 = new ConversionEvent({name: 'Conversion event 2', revenue: 2}); impressionEvent.save(function(err) { assert.ifError(err); @@ -278,16 +284,16 @@ describe('model', function() { conversionEvent2.save(function(err) { assert.ifError(err); // doesn't find anything since we're querying for an impression id - var query = ConversionEvent.find({ _id: impressionEvent._id }, fields); + var query = ConversionEvent.find({_id: impressionEvent._id}, fields); assert.equal(query.op, 'find'); - assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'model-discriminator-querying-conversion' }); + assert.deepEqual(query._conditions, {_id: impressionEvent._id, __t: 'model-discriminator-querying-conversion'}); query.exec(function(err, documents) { assert.ifError(err); assert.equal(documents.length, 0); // now find one with no criteria given and ensure it gets added to _conditions var query = ConversionEvent.find({}, fields); - assert.deepEqual(query._conditions, { __t: 'model-discriminator-querying-conversion' }); + assert.deepEqual(query._conditions, {__t: 'model-discriminator-querying-conversion'}); assert.equal(query.op, 'find'); query.exec(function(err, documents) { assert.ifError(err); @@ -331,9 +337,9 @@ describe('model', function() { }); it('hydrates streams', function(done) { - var baseEvent = new BaseEvent({ name: 'Base event' }); - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 }); + var baseEvent = new BaseEvent({name: 'Base event'}); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 1.337}); baseEvent.save(function(err) { assert.ifError(err); @@ -375,10 +381,54 @@ describe('model', function() { }); describe('findOne', function() { + it('when selecting `select: false` field (gh-4629)', function(done) { + var s = new SecretEvent({ name: 'test', secret: 'test2' }); + s.save(function(error) { + assert.ifError(error); + SecretEvent.findById(s._id, '+secret', function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'test'); + assert.equal(doc.secret, 'test2'); + done(); + }); + }); + }); + + it('select: false in base schema (gh-5448)', function(done) { + var schema = new mongoose.Schema({ + foo: String, + hiddenColumn: { + type: String, + select: false + } + }); + + var Foo = db.model('Foo', schema); + var Bar = Foo.discriminator('Bar', new mongoose.Schema({ + bar: String + })); + + var obj = { + foo: 'test', + hiddenColumn: 'Wanna see me?', + bar: 'test2' + }; + Bar.create(obj). + then(function() { return Foo.find().select('+hiddenColumn'); }). + then(function(docs) { + assert.equal(docs.length, 1); + assert.equal(docs[0].hiddenColumn, 'Wanna see me?'); + assert.equal(docs[0].foo, 'test'); + assert.equal(docs[0].bar, 'test2'); + done(); + }). + catch(done); + }); + it('hydrates correct model', function(done) { - var baseEvent = new BaseEvent({ name: 'Base event' }); - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 }); + var baseEvent = new BaseEvent({name: 'Base event'}); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 1.337}); baseEvent.save(function(err) { assert.ifError(err); @@ -387,20 +437,20 @@ describe('model', function() { conversionEvent.save(function(err) { assert.ifError(err); // finds & hydrates BaseEvent - BaseEvent.findOne({ _id: baseEvent._id }, function(err, event) { + BaseEvent.findOne({_id: baseEvent._id}, function(err, event) { assert.ifError(err); assert.ok(event instanceof BaseEvent); assert.equal(event.name, 'Base event'); // finds & hydrates ImpressionEvent - BaseEvent.findOne({ _id: impressionEvent._id }, function(err, event) { + BaseEvent.findOne({_id: impressionEvent._id}, function(err, event) { assert.ifError(err); assert.ok(event instanceof ImpressionEvent); assert.equal(event.schema, ImpressionEventSchema); assert.equal(event.name, 'Impression event'); // finds & hydrates ConversionEvent - BaseEvent.findOne({ _id: conversionEvent._id }, function(err, event) { + BaseEvent.findOne({_id: conversionEvent._id}, function(err, event) { assert.ifError(err); assert.ok(event instanceof ConversionEvent); assert.equal(event.schema, ConversionEventSchema); @@ -415,9 +465,9 @@ describe('model', function() { }); var checkHydratesCorrectModels = function(fields, done, checkUndefinedRevenue) { - var baseEvent = new BaseEvent({ name: 'Base event' }); - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 }); + var baseEvent = new BaseEvent({name: 'Base event'}); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 1.337}); baseEvent.save(function(err) { assert.ifError(err); @@ -426,20 +476,20 @@ describe('model', function() { conversionEvent.save(function(err) { assert.ifError(err); // finds & hydrates BaseEvent - BaseEvent.findOne({ _id: baseEvent._id }, fields, function(err, event) { + BaseEvent.findOne({_id: baseEvent._id}, fields, function(err, event) { assert.ifError(err); assert.ok(event instanceof BaseEvent); assert.equal(event.name, 'Base event'); // finds & hydrates ImpressionEvent - BaseEvent.findOne({ _id: impressionEvent._id }, fields, function(err, event) { + BaseEvent.findOne({_id: impressionEvent._id}, fields, function(err, event) { assert.ifError(err); assert.ok(event instanceof ImpressionEvent); assert.equal(event.schema, ImpressionEventSchema); assert.equal(event.name, 'Impression event'); // finds & hydrates ConversionEvent - BaseEvent.findOne({ _id: conversionEvent._id }, fields, function(err, event) { + BaseEvent.findOne({_id: conversionEvent._id}, fields, function(err, event) { assert.ifError(err); assert.ok(event instanceof ConversionEvent); assert.equal(event.schema, ConversionEventSchema); @@ -481,17 +531,17 @@ describe('model', function() { }); it('discriminator model only finds a document of its type', function(done) { - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 2 }); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 2}); impressionEvent.save(function(err) { assert.ifError(err); conversionEvent.save(function(err) { assert.ifError(err); // doesn't find anything since we're querying for an impression id - var query = ConversionEvent.findOne({ _id: impressionEvent._id }); + var query = ConversionEvent.findOne({_id: impressionEvent._id}); assert.equal(query.op, 'findOne'); - assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'model-discriminator-querying-conversion' }); + assert.deepEqual(query._conditions, {_id: impressionEvent._id, __t: 'model-discriminator-querying-conversion'}); query.exec(function(err, document) { assert.ifError(err); @@ -500,7 +550,7 @@ describe('model', function() { // now find one with no criteria given and ensure it gets added to _conditions var query = ConversionEvent.findOne(); assert.equal(query.op, 'findOne'); - assert.deepEqual(query._conditions, { __t: 'model-discriminator-querying-conversion' }); + assert.deepEqual(query._conditions, {__t: 'model-discriminator-querying-conversion'}); query.exec(function(err, document) { assert.ifError(err); @@ -514,17 +564,17 @@ describe('model', function() { }); var checkDiscriminatorModelsFindOneDocumentOfItsType = function(fields, done) { - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 2 }); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 2}); impressionEvent.save(function(err) { assert.ifError(err); conversionEvent.save(function(err) { assert.ifError(err); // doesn't find anything since we're querying for an impression id - var query = ConversionEvent.findOne({ _id: impressionEvent._id }, fields); + var query = ConversionEvent.findOne({_id: impressionEvent._id}, fields); assert.equal(query.op, 'findOne'); - assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'model-discriminator-querying-conversion' }); + assert.deepEqual(query._conditions, {_id: impressionEvent._id, __t: 'model-discriminator-querying-conversion'}); query.exec(function(err, document) { assert.ifError(err); @@ -533,7 +583,7 @@ describe('model', function() { // now find one with no criteria given and ensure it gets added to _conditions var query = ConversionEvent.findOne({}, fields); assert.equal(query.op, 'findOne'); - assert.deepEqual(query._conditions, { __t: 'model-discriminator-querying-conversion' }); + assert.deepEqual(query._conditions, {__t: 'model-discriminator-querying-conversion'}); query.exec(function(err, document) { assert.ifError(err); @@ -573,9 +623,9 @@ describe('model', function() { describe('findOneAndUpdate', function() { it('does not update models of other types', function(done) { - var baseEvent = new BaseEvent({ name: 'Base event' }); - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 }); + var baseEvent = new BaseEvent({name: 'Base event'}); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 1.337}); baseEvent.save(function(err) { assert.ifError(err); @@ -583,8 +633,8 @@ describe('model', function() { assert.ifError(err); conversionEvent.save(function(err) { assert.ifError(err); - var query = ConversionEvent.findOneAndUpdate({ name: 'Impression event' }, { $set: { name: 'Impression event - updated'}}); - assert.deepEqual(query._conditions, { name: 'Impression event', __t: 'model-discriminator-querying-conversion' }); + var query = ConversionEvent.findOneAndUpdate({name: 'Impression event'}, {$set: {name: 'Impression event - updated'}}); + assert.deepEqual(query._conditions, {name: 'Impression event', __t: 'model-discriminator-querying-conversion'}); query.exec(function(err, document) { assert.ifError(err); assert.equal(document, null); @@ -596,9 +646,9 @@ describe('model', function() { }); it('updates models of its own type', function(done) { - var baseEvent = new BaseEvent({ name: 'Base event' }); - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 }); + var baseEvent = new BaseEvent({name: 'Base event'}); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 1.337}); baseEvent.save(function(err) { assert.ifError(err); @@ -606,8 +656,8 @@ describe('model', function() { assert.ifError(err); conversionEvent.save(function(err) { assert.ifError(err); - var query = ConversionEvent.findOneAndUpdate({ name: 'Conversion event' }, { $set: { name: 'Conversion event - updated'}}, { 'new': true }); - assert.deepEqual(query._conditions, { name: 'Conversion event', __t: 'model-discriminator-querying-conversion' }); + var query = ConversionEvent.findOneAndUpdate({name: 'Conversion event'}, {$set: {name: 'Conversion event - updated'}}, {new: true}); + assert.deepEqual(query._conditions, {name: 'Conversion event', __t: 'model-discriminator-querying-conversion'}); query.exec(function(err, document) { assert.ifError(err); var expected = conversionEvent.toJSON(); @@ -621,9 +671,9 @@ describe('model', function() { }); it('base model modifies any event type', function(done) { - var baseEvent = new BaseEvent({ name: 'Base event' }); - var impressionEvent = new ImpressionEvent({ name: 'Impression event' }); - var conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 }); + var baseEvent = new BaseEvent({name: 'Base event'}); + var impressionEvent = new ImpressionEvent({name: 'Impression event'}); + var conversionEvent = new ConversionEvent({name: 'Conversion event', revenue: 1.337}); baseEvent.save(function(err) { assert.ifError(err); @@ -631,8 +681,8 @@ describe('model', function() { assert.ifError(err); conversionEvent.save(function(err) { assert.ifError(err); - var query = BaseEvent.findOneAndUpdate({ name: 'Conversion event' }, { $set: { name: 'Conversion event - updated'}}, { 'new': true }); - assert.deepEqual(query._conditions, { name: 'Conversion event' }); + var query = BaseEvent.findOneAndUpdate({name: 'Conversion event'}, {$set: {name: 'Conversion event - updated'}}, {new: true}); + assert.deepEqual(query._conditions, {name: 'Conversion event'}); query.exec(function(err, document) { assert.ifError(err); var expected = conversionEvent.toJSON(); @@ -649,40 +699,40 @@ describe('model', function() { describe('population/reference mapping', function() { it('populates and hydrates correct models', function(done) { var vehicleSchema = new Schema(); - var carSchema = new Schema({ speed: Number }); - var busSchema = new Schema({ speed: Number }); + var carSchema = new Schema({speed: Number}); + var busSchema = new Schema({speed: Number}); var userSchema = new Schema({ - vehicles: [{ type: Schema.Types.ObjectId, ref: 'ModelDiscriminatorPopulationVehicle' }] - , favoriteVehicle: { type: Schema.Types.ObjectId, ref: 'ModelDiscriminatorPopulationVehicle' } - , favoriteBus: { type: Schema.Types.ObjectId, ref: 'ModelDiscriminatorPopulationBus' } + vehicles: [{type: Schema.Types.ObjectId, ref: 'ModelDiscriminatorPopulationVehicle'}], + favoriteVehicle: {type: Schema.Types.ObjectId, ref: 'ModelDiscriminatorPopulationVehicle'}, + favoriteBus: {type: Schema.Types.ObjectId, ref: 'ModelDiscriminatorPopulationBus'} }); - var Vehicle = db.model('ModelDiscriminatorPopulationVehicle', vehicleSchema) - , Car = Vehicle.discriminator('ModelDiscriminatorPopulationCar', carSchema) - , Bus = Vehicle.discriminator('ModelDiscriminatorPopulationBus', busSchema) - , User = db.model('ModelDiscriminatorPopulationUser', userSchema); + var Vehicle = db.model('ModelDiscriminatorPopulationVehicle', vehicleSchema), + Car = Vehicle.discriminator('ModelDiscriminatorPopulationCar', carSchema), + Bus = Vehicle.discriminator('ModelDiscriminatorPopulationBus', busSchema), + User = db.model('ModelDiscriminatorPopulationUser', userSchema); Vehicle.create({}, function(err, vehicle) { assert.ifError(err); - Car.create({ speed: 160 }, function(err, car) { - Bus.create({ speed: 80 }, function(err, bus) { + Car.create({speed: 160}, function(err, car) { + Bus.create({speed: 80}, function(err, bus) { assert.ifError(err); - User.create({ vehicles: [vehicle._id, car._id, bus._id], favoriteVehicle: car._id, favoriteBus: bus._id }, function(err) { + User.create({vehicles: [vehicle._id, car._id, bus._id], favoriteVehicle: car._id, favoriteBus: bus._id}, function(err) { assert.ifError(err); User.findOne({}).populate('vehicles favoriteVehicle favoriteBus').exec(function(err, user) { assert.ifError(err); var expected = { - __v: 0 - , _id: user._id - , vehicles: [ - { _id: vehicle._id, __v: 0 } - , { _id: car._id, speed: 160, __v: 0, __t: 'ModelDiscriminatorPopulationCar' } - , { _id: bus._id, speed: 80, __v: 0, __t: 'ModelDiscriminatorPopulationBus' } - ] - , favoriteVehicle: { _id: car._id, speed: 160, __v: 0, __t: 'ModelDiscriminatorPopulationCar' } - , favoriteBus: { _id: bus._id, speed: 80, __v: 0, __t: 'ModelDiscriminatorPopulationBus' } + __v: 0, + _id: user._id, + vehicles: [ + {_id: vehicle._id, __v: 0}, + {_id: car._id, speed: 160, __v: 0, __t: 'ModelDiscriminatorPopulationCar'}, + {_id: bus._id, speed: 80, __v: 0, __t: 'ModelDiscriminatorPopulationBus'} + ], + favoriteVehicle: {_id: car._id, speed: 160, __v: 0, __t: 'ModelDiscriminatorPopulationCar'}, + favoriteBus: {_id: bus._id, speed: 80, __v: 0, __t: 'ModelDiscriminatorPopulationBus'} }; assert.deepEqual(user.toJSON(), expected); @@ -722,16 +772,16 @@ describe('model', function() { num_of_places: Number }); - var Vehicle = db.model('gh2719PopulationVehicle', vehicleSchema) - , Car = Vehicle.discriminator('gh2719PopulationCar', carSchema) - , Bus = Vehicle.discriminator('gh2719PopulationBus', busSchema) - , Garage = db.model('gh2719PopulationGarage', garageSchema); + var Vehicle = db.model('gh2719PopulationVehicle', vehicleSchema), + Car = Vehicle.discriminator('gh2719PopulationCar', carSchema), + Bus = Vehicle.discriminator('gh2719PopulationBus', busSchema), + Garage = db.model('gh2719PopulationGarage', garageSchema); Garage.create({name: 'My', num_of_places: 3}, function(err, garage) { assert.ifError(err); - Car.create({ speed: 160, garage: garage }, function(err) { + Car.create({speed: 160, garage: garage}, function(err) { assert.ifError(err); - Bus.create({ speed: 80, garage: garage }, function(err) { + Bus.create({speed: 80, garage: garage}, function(err) { assert.ifError(err); Vehicle.find({}).populate('garage').exec(function(err, vehicles) { assert.ifError(err); @@ -747,6 +797,66 @@ describe('model', function() { }); }); + it('populates parent array reference (gh-4643)', function(done) { + var vehicleSchema = new Schema({ + wheels: [{ + type: Schema.Types.ObjectId, + ref: 'gh4643' + }] + }); + var wheelSchema = new Schema({ brand: String }); + var busSchema = new Schema({ speed: Number }); + + var Vehicle = db.model('gh4643_0', vehicleSchema); + var Bus = Vehicle.discriminator('gh4643_00', busSchema); + var Wheel = db.model('gh4643', wheelSchema); + + Wheel.create({ brand: 'Rotiform' }, function(err, wheel) { + assert.ifError(err); + Bus.create({ speed: 80, wheels: [wheel] }, function(err) { + assert.ifError(err); + Bus.findOne({}).populate('wheels').exec(function(err, bus) { + assert.ifError(err); + + assert.ok(bus instanceof Vehicle); + assert.ok(bus instanceof Bus); + assert.equal(bus.wheels.length, 1); + assert.ok(bus.wheels[0] instanceof Wheel); + assert.equal(bus.wheels[0].brand, 'Rotiform'); + done(); + }); + }); + }); + }); + + it('updating type key (gh-5613)', function(done) { + function BaseSchema() { + Schema.apply(this, arguments); + + this.add({ + name: {type: String, required: true} + }); + } + + util.inherits(BaseSchema, Schema); + + var orgSchema = new BaseSchema({}); + var schoolSchema = new BaseSchema({ principal: String }); + + var Org = db.model('gh5613', orgSchema); + Org.discriminator('gh5613_0', schoolSchema); + + Org.create({ name: 'test' }, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.__t); + Org.findByIdAndUpdate(doc._id, { __t: 'gh5613_0' }, { new: true }, function(error, doc) { + assert.ifError(error); + assert.equal(doc.__t, 'gh5613_0'); + done(); + }); + }); + }); + it('reference in child schemas (gh-2719-2)', function(done) { var EventSchema, Event, TalkSchema, Talk, Survey; @@ -754,10 +864,10 @@ describe('model', function() { Schema.apply(this, arguments); this.add({ - name : { type: String, required: true }, - date : { type: Date, required: true }, - period : { start : { type: String, required: true }, - end : { type: String, required: true } + name: {type: String, required: true}, + date: {type: Date, required: true}, + period: {start: {type: String, required: true}, + end: {type: String, required: true} } }); } @@ -768,11 +878,11 @@ describe('model', function() { Event = db.model('Event', EventSchema); TalkSchema = new BaseSchema({ - pin : { type: String, required: true, index: { unique: true } }, - totalAttendees : { type: Number }, - speakers : [{ type: Schema.Types.ObjectId, ref: 'Speaker' }], - surveys : [{ type: Schema.Types.ObjectId, ref: 'Survey' }], - questions : [{ type: Schema.Types.ObjectId, ref: 'Question' }] + pin: {type: String, required: true, index: {unique: true}}, + totalAttendees: {type: Number}, + speakers: [{type: Schema.Types.ObjectId, ref: 'Speaker'}], + surveys: [{type: Schema.Types.ObjectId, ref: 'Survey'}], + questions: [{type: Schema.Types.ObjectId, ref: 'Question'}] }); Talk = Event.discriminator('Talk', TalkSchema); @@ -783,17 +893,17 @@ describe('model', function() { })); Survey.create({ - name: "That you see?", + name: 'That you see?', date: Date.now() }, function(err, survey) { assert.ifError(err); Talk.create({ - name: "Meetup rails", - date: new Date("2015-04-01T00:00:00Z"), - pin: "0004", - period: { start: "11:00", end: "12:00" }, - surveys: [ survey ] + name: 'Meetup rails', + date: new Date('2015-04-01T00:00:00Z'), + pin: '0004', + period: {start: '11:00', end: '12:00'}, + surveys: [survey] }, function(err) { assert.ifError(err); @@ -813,9 +923,9 @@ describe('model', function() { var impressionEvent, conversionEvent, ignoredImpressionEvent; beforeEach(function(done) { - impressionEvent = new ImpressionEvent({ name: 'Test Event' }); - conversionEvent = new ConversionEvent({ name: 'Test Event', revenue: 10 }); - ignoredImpressionEvent = new ImpressionEvent({ name: 'Ignored Event' }); + impressionEvent = new ImpressionEvent({name: 'Test Event'}); + conversionEvent = new ConversionEvent({name: 'Test Event', revenue: 10}); + ignoredImpressionEvent = new ImpressionEvent({name: 'Ignored Event'}); async.forEach( [impressionEvent, conversionEvent, ignoredImpressionEvent], @@ -829,13 +939,13 @@ describe('model', function() { describe('using "RootModel#aggregate"', function() { it('to aggregate documents of all discriminators', function(done) { var aggregate = BaseEvent.aggregate([ - { $match: { name: 'Test Event' } } + {$match: {name: 'Test Event'}} ]); aggregate.exec(function(err, docs) { assert.ifError(err); assert.deepEqual(aggregate._pipeline, [ - { $match: { name: 'Test Event' } } + {$match: {name: 'Test Event'}} ]); assert.equal(docs.length, 2); done(); @@ -846,7 +956,7 @@ describe('model', function() { describe('using "ModelDiscriminator#aggregate"', function() { it('only aggregates documents of the appropriate discriminator', function(done) { var aggregate = ImpressionEvent.aggregate([ - { $group: { _id: '$__t', count: { $sum: 1 } } } + {$group: {_id: '$__t', count: {$sum: 1}}} ]); aggregate.exec(function(err, result) { @@ -857,21 +967,75 @@ describe('model', function() { // aggregations with empty pipelines, but that are over // discriminators be executed assert.deepEqual(aggregate._pipeline, [ - { $match: { __t: 'model-discriminator-querying-impression' } }, - { $group: { _id: '$__t', count: { $sum: 1 } } } + {$match: {__t: 'model-discriminator-querying-impression'}}, + {$group: {_id: '$__t', count: {$sum: 1}}} ]); assert.equal(result.length, 1); assert.deepEqual(result, [ - { _id: 'model-discriminator-querying-impression', count: 2 } + {_id: 'model-discriminator-querying-impression', count: 2} ]); done(); }); }); + it('hides fields when discriminated model has select (gh-4991)', function(done) { + var baseSchema = new mongoose.Schema({ + internal: { + test: [{ type: String }] + } + }); + + var Base = db.model('gh4991', baseSchema); + var discriminatorSchema = new mongoose.Schema({ + internal: { + password: { type: String, select: false } + } + }); + var Discriminator = Base.discriminator('gh4991_0', + discriminatorSchema); + + var obj = { + internal: { + test: ['abc'], + password: 'password' + } + }; + Discriminator.create(obj). + then(function(doc) { return Base.findById(doc._id); }). + then(function(doc) { + assert.ok(!doc.internal.password); + done(); + }). + catch(done); + }); + + it('doesnt exclude field if slice (gh-4991)', function(done) { + var baseSchema = new mongoose.Schema({ + propA: { type: String, default: 'default value' }, + array: [{type: String}] + }); + + var Base = db.model('gh4991_A', baseSchema); + var discriminatorSchema = new mongoose.Schema({ + propB: { type: String} + }); + var Discriminator = Base.discriminator('gh4991_A1', discriminatorSchema); + + var obj = { propA: 'Hi', propB: 'test', array: ['a', 'b'] }; + Discriminator.create(obj, function(error) { + assert.ifError(error); + Base.find().slice('array', 1).exec(function(error, docs) { + assert.equal(docs.length, 1); + assert.equal(docs[0].propA, 'Hi'); + done(); + }); + }); + }); + it('merges the first pipeline stages if applicable', function(done) { var aggregate = ImpressionEvent.aggregate([ - { $match: { name: 'Test Event' } } + {$match: {name: 'Test Event'}} ]); aggregate.exec(function(err, result) { @@ -882,7 +1046,7 @@ describe('model', function() { // aggregations with empty pipelines, but that are over // discriminators be executed assert.deepEqual(aggregate._pipeline, [ - { $match: { __t: 'model-discriminator-querying-impression', name: 'Test Event' } } + {$match: {__t: 'model-discriminator-querying-impression', name: 'Test Event'}} ]); assert.equal(result.length, 1); diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index e6930235cba..6bf45f7a85b 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -2,22 +2,22 @@ * Test dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , Schema = mongoose.Schema - , assert = require('assert') - , util = require('util') - , clone = require('../lib/utils').clone - , random = require('../lib/utils').random; +var start = require('./common'); +var mongoose = start.mongoose; +var Schema = mongoose.Schema; +var assert = require('power-assert'); +var util = require('util'); +var clone = require('../lib/utils').clone; +var random = require('../lib/utils').random; /** * Setup */ var PersonSchema = new Schema({ - name: { first: String, last: String } - , gender: String -}, { collection: 'model-discriminator-' + random() }); -PersonSchema.index({ name: 1 }); + name: {first: String, last: String}, + gender: String +}, {collection: 'model-discriminator-' + random()}); +PersonSchema.index({name: 1}); PersonSchema.methods.getFullName = function() { return this.name.first + ' ' + this.name.last; }; @@ -25,7 +25,8 @@ PersonSchema.methods.toJSonConfig = { include: ['prop1', 'prop2'], exclude: ['prop3', 'prop4'] }; -PersonSchema.statics.findByGender = function() {}; +PersonSchema.statics.findByGender = function() { +}; PersonSchema.virtual('name.full').get(function() { return this.name.first + ' ' + this.name.last; }); @@ -37,18 +38,16 @@ PersonSchema.virtual('name.full').set(function(name) { PersonSchema.path('gender').validate(function(value) { return /[A-Z]/.test(value); }, 'Invalid name'); -PersonSchema.post('save', function(next) { - next(); -}); -PersonSchema.set('toObject', { getters: true, virtuals: true }); -PersonSchema.set('toJSON', { getters: true, virtuals: true }); +PersonSchema.set('toObject', {getters: true, virtuals: true}); +PersonSchema.set('toJSON', {getters: true, virtuals: true}); -var EmployeeSchema = new Schema({ department: String }); -EmployeeSchema.index({ department: 1 }); +var EmployeeSchema = new Schema({department: String}); +EmployeeSchema.index({department: 1}); EmployeeSchema.methods.getDepartment = function() { return this.department; }; -EmployeeSchema.statics.findByDepartment = function() {}; +EmployeeSchema.statics.findByDepartment = function() { +}; EmployeeSchema.path('department').validate(function(value) { return /[a-zA-Z]/.test(value); }, 'Invalid name'); @@ -56,8 +55,8 @@ var employeeSchemaPreSaveFn = function(next) { next(); }; EmployeeSchema.pre('save', employeeSchemaPreSaveFn); -EmployeeSchema.set('toObject', { getters: true, virtuals: false }); -EmployeeSchema.set('toJSON', { getters: false, virtuals: true }); +EmployeeSchema.set('toObject', {getters: true, virtuals: false}); +EmployeeSchema.set('toJSON', {getters: false, virtuals: true}); describe('model', function() { describe('discriminator()', function() { @@ -98,10 +97,11 @@ describe('model', function() { createdAt: Date }); } + util.inherits(BossBaseSchema, Schema); var PersonSchema = new BossBaseSchema(); - var BossSchema = new BossBaseSchema({ department: String }); + var BossSchema = new BossBaseSchema({department: String}); BossSchema.methods.myName = function() { return this.name; }; @@ -111,7 +111,7 @@ describe('model', function() { var Person = db.model('Person', PersonSchema); var Boss = Person.discriminator('Boss', BossSchema); - var boss = new Boss({name:'Bernenke'}); + var boss = new Boss({name: 'Bernenke'}); assert.equal(boss.myName(), 'Bernenke'); assert.equal(boss.notInstanceMethod, undefined); assert.equal(Boss.currentPresident(), 'obama'); @@ -120,12 +120,12 @@ describe('model', function() { }); it('sets schema root discriminator mapping', function(done) { - assert.deepEqual(PersonSchema.discriminatorMapping, { key: '__t', value: null, isRoot: true }); + assert.deepEqual(PersonSchema.discriminatorMapping, {key: '__t', value: null, isRoot: true}); done(); }); it('sets schema discriminator type mapping', function(done) { - assert.deepEqual(EmployeeSchema.discriminatorMapping, { key: '__t', value: 'model-discriminator-employee', isRoot: false }); + assert.deepEqual(EmployeeSchema.discriminatorMapping, {key: '__t', value: 'model-discriminator-employee', isRoot: false}); done(); }); @@ -148,41 +148,41 @@ describe('model', function() { it('throws error on invalid schema', function(done) { assert.throws( - function() { - Person.discriminator('Foo'); - }, - /You must pass a valid discriminator Schema/ + function() { + Person.discriminator('Foo'); + }, + /You must pass a valid discriminator Schema/ ); done(); }); it('throws error when attempting to nest discriminators', function(done) { assert.throws( - function() { - Employee.discriminator('model-discriminator-foo', new Schema()); - }, - /Discriminator "model-discriminator-foo" can only be a discriminator of the root model/ + function() { + Employee.discriminator('model-discriminator-foo', new Schema()); + }, + /Discriminator "model-discriminator-foo" can only be a discriminator of the root model/ ); done(); }); it('throws error when discriminator has mapped discriminator key in schema', function(done) { assert.throws( - function() { - Person.discriminator('model-discriminator-foo', new Schema({ __t: String })); - }, - /Discriminator "model-discriminator-foo" cannot have field with name "__t"/ + function() { + Person.discriminator('model-discriminator-foo', new Schema({__t: String})); + }, + /Discriminator "model-discriminator-foo" cannot have field with name "__t"/ ); done(); }); it('throws error when discriminator has mapped discriminator key in schema with discriminatorKey option set', function(done) { assert.throws( - function() { - var Foo = db.model('model-discriminator-foo', new Schema({}, { discriminatorKey: '_type' }), 'model-discriminator-' + random()); - Foo.discriminator('model-discriminator-bar', new Schema({ _type: String })); - }, - /Discriminator "model-discriminator-bar" cannot have field with name "_type"/ + function() { + var Foo = db.model('model-discriminator-foo', new Schema({}, {discriminatorKey: '_type'}), 'model-discriminator-' + random()); + Foo.discriminator('model-discriminator-bar', new Schema({_type: String})); + }, + /Discriminator "model-discriminator-bar" cannot have field with name "_type"/ ); done(); }); @@ -190,12 +190,23 @@ describe('model', function() { it('throws error when discriminator with taken name is added', function(done) { var Foo = db.model('model-discriminator-foo', new Schema({}), 'model-discriminator-' + random()); Foo.discriminator('model-discriminator-taken', new Schema()); + assert.throws( + function() { + Foo.discriminator('model-discriminator-taken', new Schema()); + }, + /Discriminator with name "model-discriminator-taken" already exists/ + ); + done(); + }); + + it('throws error if model name is taken (gh-4148)', function(done) { + var Foo = db.model('model-discriminator-4148', new Schema({})); + db.model('model-discriminator-4148-bar', new Schema({})); assert.throws( function() { - Foo.discriminator('model-discriminator-taken', new Schema()); + Foo.discriminator('model-discriminator-4148-bar', new Schema()); }, - /Discriminator with name "model-discriminator-taken" already exists/ - ); + /Cannot overwrite `model-discriminator-4148-bar`/); done(); }); @@ -210,12 +221,12 @@ describe('model', function() { util.inherits(MinionSchema, mongoose.Schema); var BaseSchema = function() { - mongoose.Schema.apply( this, arguments ); + mongoose.Schema.apply(this, arguments); this.add({ name: String, created_at: Date, - minions: [ new MinionSchema() ] + minions: [new MinionSchema()] }); }; util.inherits(BaseSchema, mongoose.Schema); @@ -223,38 +234,34 @@ describe('model', function() { var PersonSchema = new BaseSchema(); var BossSchema = new BaseSchema({ department: String - }); + }, { id: false }); - assert.doesNotThrow(function() { - var Person = db.model('gh2821', PersonSchema); - Person.discriminator('Boss', BossSchema); - }); + // Should not throw + var Person = db.model('gh2821', PersonSchema); + Person.discriminator('gh2821-Boss', BossSchema); done(); }); describe('options', function() { it('allows toObject to be overridden', function(done) { assert.notDeepEqual(Employee.schema.get('toObject'), Person.schema.get('toObject')); - assert.deepEqual(Employee.schema.get('toObject'), { getters: true, virtuals: false }); + assert.deepEqual(Employee.schema.get('toObject'), {getters: true, virtuals: false}); done(); }); it('allows toJSON to be overridden', function(done) { assert.notDeepEqual(Employee.schema.get('toJSON'), Person.schema.get('toJSON')); - assert.deepEqual(Employee.schema.get('toJSON'), { getters: false, virtuals: true }); + assert.deepEqual(Employee.schema.get('toJSON'), {getters: false, virtuals: true}); done(); }); it('is not customizable', function(done) { - var errorMessage - , CustomizedSchema = new Schema({}, { capped: true }); - try { + var CustomizedSchema = new Schema({}, {capped: true}); + + assert.throws(function() { Person.discriminator('model-discriminator-custom', CustomizedSchema); - } catch (e) { - errorMessage = e.message; - } + }, /Can't customize discriminator option capped/); - assert.equal(errorMessage, 'Discriminator options are not customizable (except toJSON & toObject)'); done(); }); }); @@ -274,8 +281,8 @@ describe('model', function() { }); it('does not inherit and override fields that exist', function(done) { - var FemaleSchema = new Schema({ gender: { type: String, default: 'F' }}) - , Female = Person.discriminator('model-discriminator-female', FemaleSchema); + var FemaleSchema = new Schema({gender: {type: String, default: 'F'}}), + Female = Person.discriminator('model-discriminator-female', FemaleSchema); var gender = Female.schema.paths.gender; @@ -308,9 +315,7 @@ describe('model', function() { }); it('merges callQueue with base queue defined before discriminator types callQueue', function(done) { - assert.equal(Employee.schema.callQueue.length, 4); - // PersonSchema.post('save') - assert.strictEqual(Employee.schema.callQueue[0], Person.schema.callQueue[0]); + assert.equal(Employee.schema.callQueue.length, 7); // EmployeeSchema.pre('save') var queueIndex = Employee.schema.callQueue.length - 1; @@ -321,14 +326,14 @@ describe('model', function() { }); it('does not inherit indexes', function(done) { - assert.deepEqual(Person.schema.indexes(), [[{ name: 1 }, { background: true }]]); - assert.deepEqual(Employee.schema.indexes(), [[{ department: 1 }, { background: true }]]); + assert.deepEqual(Person.schema.indexes(), [[{name: 1}, {background: true}]]); + assert.deepEqual(Employee.schema.indexes(), [[{department: 1}, {background: true}]]); done(); }); it('gets options overridden by root options except toJSON and toObject', function(done) { - var personOptions = clone(Person.schema.options) - , employeeOptions = clone(Employee.schema.options); + var personOptions = clone(Person.schema.options), + employeeOptions = clone(Employee.schema.options); delete personOptions.toJSON; delete personOptions.toObject; @@ -338,6 +343,685 @@ describe('model', function() { assert.deepEqual(personOptions, employeeOptions); done(); }); + + it('does not allow setting discriminator key (gh-2041)', function(done) { + var doc = new Employee({ __t: 'fake' }); + assert.equal(doc.__t, 'model-discriminator-employee'); + doc.save(function(error) { + assert.ok(error); + assert.equal(error.errors['__t'].reason.message, + 'Can\'t set discriminator key "__t"'); + done(); + }); + }); + + it('with typeKey (gh-4339)', function(done) { + var options = { typeKey: '$type', discriminatorKey: '_t' }; + var schema = new Schema({ test: { $type: String } }, options); + var Model = mongoose.model('gh4339', schema); + Model.discriminator('gh4339_0', new Schema({ + test2: String + }, { typeKey: '$type' })); + done(); + }); + + it('applyPluginsToDiscriminators (gh-4965)', function(done) { + var schema = new Schema({ test: String }); + mongoose.set('applyPluginsToDiscriminators', true); + var called = 0; + mongoose.plugin(function() { + ++called; + }); + var Model = mongoose.model('gh4965', schema); + var childSchema = new Schema({ + test2: String + }); + Model.discriminator('gh4965_0', childSchema); + assert.equal(called, 2); + + mongoose.plugins = []; + mongoose.set('applyPluginsToDiscriminators', false); + done(); + }); + + it('cloning with discriminator key (gh-4387)', function(done) { + var employee = new Employee({ name: { first: 'Val', last: 'Karpov' } }); + var clone = new employee.constructor(employee); + + // Should not error because we have the same discriminator key + clone.save(function(error) { + assert.ifError(error); + done(); + }); + }); + + it('embedded discriminators with create() (gh-5001)', function(done) { + var eventSchema = new Schema({ message: String }, + { discriminatorKey: 'kind', _id: false }); + var batchSchema = new Schema({ events: [eventSchema] }); + var docArray = batchSchema.path('events'); + + var Clicked = docArray.discriminator('Clicked', new Schema({ + element: { + type: String, + required: true + } + }, { _id: false })); + + var Purchased = docArray.discriminator('Purchased', new Schema({ + product: { + type: String, + required: true + } + }, { _id: false })); + + var Batch = db.model('EventBatch', batchSchema); + + var batch = { + events: [ + { kind: 'Clicked', element: '#hero' } + ] + }; + + Batch.create(batch). + then(function(doc) { + assert.equal(doc.events.length, 1); + var newDoc = doc.events.create({ + kind: 'Purchased', + product: 'action-figure-1' + }); + assert.equal(newDoc.kind, 'Purchased'); + assert.equal(newDoc.product, 'action-figure-1'); + assert.ok(newDoc instanceof Purchased); + + doc.events.push(newDoc); + assert.equal(doc.events.length, 2); + assert.equal(doc.events[1].kind, 'Purchased'); + assert.equal(doc.events[1].product, 'action-figure-1'); + assert.ok(newDoc instanceof Purchased); + assert.ok(newDoc === doc.events[1]); + + done(); + }). + catch(done); + }); + + it('supports clone() (gh-4983)', function(done) { + var childSchema = new Schema({ + name: String + }); + var childCalls = 0; + var childValidateCalls = 0; + childSchema.pre('validate', function(next) { + ++childValidateCalls; + next(); + }); + childSchema.pre('save', function(next) { + ++childCalls; + next(); + }); + + var personSchema = new Schema({ + name: String + }, { discriminatorKey: 'kind' }); + + var parentSchema = new Schema({ + children: [childSchema], + heir: childSchema + }); + var parentCalls = 0; + parentSchema.pre('save', function(next) { + ++parentCalls; + next(); + }); + + var Person = db.model('gh4983', personSchema); + var Parent = Person.discriminator('gh4983_0', parentSchema.clone()); + + var obj = { + name: 'Ned Stark', + heir: { name: 'Robb Stark' }, + children: [{ name: 'Jon Snow' }] + }; + Parent.create(obj, function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Ned Stark'); + assert.equal(doc.heir.name, 'Robb Stark'); + assert.equal(doc.children.length, 1); + assert.equal(doc.children[0].name, 'Jon Snow'); + assert.equal(childValidateCalls, 2); + assert.equal(childCalls, 2); + assert.equal(parentCalls, 1); + done(); + }); + }); + + it('clone() allows reusing schemas (gh-5098)', function(done) { + var personSchema = new Schema({ + name: String + }, { discriminatorKey: 'kind' }); + + var parentSchema = new Schema({ + child: String + }); + + var Person = db.model('gh5098', personSchema); + var Parent = Person.discriminator('gh5098_0', parentSchema.clone()); + // Should not throw + var Parent2 = Person.discriminator('gh5098_1', parentSchema.clone()); + done(); + }); + + it('clone() allows reusing with different models (gh-5721)', function(done) { + var schema = new mongoose.Schema({ + name: String + }); + + var schemaExt = new mongoose.Schema({ + nameExt: String + }); + + var ModelA = db.model('gh5721_a0', schema); + ModelA.discriminator('gh5721_a1', schemaExt); + + ModelA.findOneAndUpdate({}, { $set: { name: 'test' } }, function(error) { + assert.ifError(error); + + var ModelB = db.model('gh5721_b0', schema.clone()); + ModelB.discriminator('gh5721_b1', schemaExt.clone()); + + done(); + }); + }); + + it('copies query hooks (gh-5147)', function(done) { + var options = { discriminatorKey: 'kind' }; + + var eventSchema = new mongoose.Schema({ time: Date }, options); + var eventSchemaCalls = 0; + eventSchema.pre('findOneAndUpdate', function() { + ++eventSchemaCalls; + }); + + var Event = db.model('gh5147', eventSchema); + + var clickedEventSchema = new mongoose.Schema({ url: String }, options); + var clickedEventSchemaCalls = 0; + clickedEventSchema.pre('findOneAndUpdate', function() { + ++clickedEventSchemaCalls; + }); + var ClickedLinkEvent = Event.discriminator('gh5147_0', clickedEventSchema); + + ClickedLinkEvent.findOneAndUpdate({}, { time: new Date() }, {}). + exec(function(error) { + assert.ifError(error); + assert.equal(eventSchemaCalls, 1); + assert.equal(clickedEventSchemaCalls, 1); + done(); + }); + }); + + it('reusing schema for discriminators (gh-5684)', function(done) { + var ParentSchema = new Schema({}); + var ChildSchema = new Schema({ name: String }); + + var FirstContainerSchema = new Schema({ + stuff: [ParentSchema] + }); + + FirstContainerSchema.path('stuff').discriminator('Child', ChildSchema); + + var SecondContainerSchema = new Schema({ + things: [ParentSchema] + }); + + SecondContainerSchema.path('things').discriminator('Child', ChildSchema); + + var M1 = db.model('gh5684_0', FirstContainerSchema); + var M2 = db.model('gh5684_1', SecondContainerSchema); + + var doc1 = new M1({ stuff: [{ __t: 'Child', name: 'test' }] }); + var doc2 = new M2({ things: [{ __t: 'Child', name: 'test' }] }); + + assert.equal(doc1.stuff.length, 1); + assert.equal(doc1.stuff[0].name, 'test'); + assert.equal(doc2.things.length, 1); + assert.equal(doc2.things[0].name, 'test'); + + done(); + }); + + it('nested discriminator key with projecting in parent (gh-5775)', function(done) { + var itemSchema = new Schema({ + type: { type: String }, + active: { type: Boolean, default: true } + }, { discriminatorKey: 'type' }); + + var collectionSchema = new Schema({ + items: [itemSchema] + }); + + var s = new Schema({ count: Number }); + collectionSchema.path('items').discriminator('type1', s); + + var MyModel = db.model('Collection', collectionSchema); + var doc = { + items: [{ type: 'type1', active: false, count: 3 }] + }; + MyModel.create(doc, function(error) { + assert.ifError(error); + MyModel.findOne({}).select('items').exec(function(error, doc) { + assert.ifError(error); + assert.equal(doc.items.length, 1); + assert.equal(doc.items[0].type, 'type1'); + assert.strictEqual(doc.items[0].active, false); + assert.strictEqual(doc.items[0].count, 3); + done(); + }); + }); + }); + + it('with $meta projection (gh-5859)', function() { + var eventSchema = new Schema({ eventField: String }, { id: false }); + var Event = db.model('gh5859', eventSchema); + + var trackSchema = new Schema({ trackField: String }); + var Track = Event.discriminator('gh5859_0', trackSchema); + + var trackedItem = new Track({ + trackField: 'trackField', + eventField: 'eventField', + }); + + return trackedItem.save(). + then(function() { + return Event.find({}).select({ score: { $meta: 'textScore' } }); + }). + then(function(docs) { + assert.equal(docs.length, 1); + assert.equal(docs[0].trackField, 'trackField'); + }). + then(function() { + return Track.find({}).select({ score: { $meta: 'textScore' } }); + }). + then(function(docs) { + assert.equal(docs.length, 1); + assert.equal(docs[0].trackField, 'trackField'); + assert.equal(docs[0].eventField, 'eventField'); + }); + }); + + it('embedded discriminators with $push (gh-5009)', function(done) { + var eventSchema = new Schema({ message: String }, + { discriminatorKey: 'kind', _id: false }); + var batchSchema = new Schema({ events: [eventSchema] }); + var docArray = batchSchema.path('events'); + + var Clicked = docArray.discriminator('Clicked', new Schema({ + element: { + type: String, + required: true + } + }, { _id: false })); + + var Purchased = docArray.discriminator('Purchased', new Schema({ + product: { + type: String, + required: true + } + }, { _id: false })); + + var Batch = db.model('gh5009', batchSchema); + + var batch = { + events: [ + { kind: 'Clicked', element: '#hero' } + ] + }; + + Batch.create(batch). + then(function(doc) { + assert.equal(doc.events.length, 1); + return Batch.updateOne({ _id: doc._id }, { + $push: { + events: { kind: 'Clicked', element: '#button' } + } + }).then(function() { + return doc; + }); + }). + then(function(doc) { + return Batch.findOne({ _id: doc._id }); + }). + then(function(doc) { + assert.equal(doc.events.length, 2); + assert.equal(doc.events[1].element, '#button'); + assert.equal(doc.events[1].kind, 'Clicked'); + done(); + }). + catch(done); + }); + + it('embedded discriminators with $push + $each (gh-5070)', function(done) { + var eventSchema = new Schema({ message: String }, + { discriminatorKey: 'kind', _id: false }); + var batchSchema = new Schema({ events: [eventSchema] }); + var docArray = batchSchema.path('events'); + + var Clicked = docArray.discriminator('Clicked', new Schema({ + element: { + type: String, + required: true + } + }, { _id: false })); + + var Purchased = docArray.discriminator('Purchased', new Schema({ + product: { + type: String, + required: true + } + }, { _id: false })); + + var Batch = db.model('gh5070', batchSchema); + + var batch = { + events: [ + { kind: 'Clicked', element: '#hero' } + ] + }; + + Batch.create(batch). + then(function(doc) { + assert.equal(doc.events.length, 1); + return Batch.updateOne({ _id: doc._id }, { + $push: { + events: { $each: [{ kind: 'Clicked', element: '#button' }] } + } + }).then(function() { + return doc; + }); + }). + then(function(doc) { + return Batch.findOne({ _id: doc._id }); + }). + then(function(doc) { + assert.equal(doc.events.length, 2); + assert.equal(doc.events[1].element, '#button'); + assert.equal(doc.events[1].kind, 'Clicked'); + done(); + }). + catch(done); + }); + + it('embedded discriminators with $set (gh-5130)', function(done) { + var eventSchema = new Schema({ message: String }, + { discriminatorKey: 'kind' }); + var batchSchema = new Schema({ events: [eventSchema] }); + var docArray = batchSchema.path('events'); + + var Clicked = docArray.discriminator('Clicked', new Schema({ + element: { + type: String, + required: true + } + })); + + var Purchased = docArray.discriminator('Purchased', new Schema({ + product: { + type: String, + required: true + } + })); + + var Batch = db.model('gh5130', batchSchema); + + var batch = { + events: [ + { kind: 'Clicked', element: '#hero' } + ] + }; + + Batch.create(batch). + then(function(doc) { + assert.equal(doc.events.length, 1); + return Batch.updateOne({ _id: doc._id, 'events._id': doc.events[0]._id }, { + $set: { + 'events.$': { + message: 'updated', + kind: 'Clicked', + element: '#hero2' + } + } + }).then(function() { return doc; }); + }). + then(function(doc) { + return Batch.findOne({ _id: doc._id }); + }). + then(function(doc) { + assert.equal(doc.events.length, 1); + assert.equal(doc.events[0].message, 'updated'); + assert.equal(doc.events[0].element, '#hero2'); // <-- test failed + assert.equal(doc.events[0].kind, 'Clicked'); // <-- test failed + done(); + }). + catch(done); + }); + + it('embedded in document arrays (gh-2723)', function(done) { + var eventSchema = new Schema({ message: String }, + { discriminatorKey: 'kind', _id: false }); + + var batchSchema = new Schema({ events: [eventSchema] }); + batchSchema.path('events').discriminator('Clicked', new Schema({ + element: String + }, { _id: false })); + batchSchema.path('events').discriminator('Purchased', new Schema({ + product: String + }, { _id: false })); + + var MyModel = db.model('gh2723', batchSchema); + var doc = { + events: [ + { kind: 'Clicked', element: 'Test' }, + { kind: 'Purchased', product: 'Test2' } + ] + }; + MyModel.create(doc). + then(function(doc) { + assert.equal(doc.events.length, 2); + assert.equal(doc.events[0].element, 'Test'); + assert.equal(doc.events[1].product, 'Test2'); + var obj = doc.toObject({ virtuals: false }); + delete obj._id; + assert.deepEqual(obj, { + __v: 0, + events: [ + { kind: 'Clicked', element: 'Test' }, + { kind: 'Purchased', product: 'Test2' } + ] + }); + done(); + }). + then(function() { + return MyModel.findOne({ + events: { + $elemMatch: { + kind: 'Clicked', + element: 'Test' + } + } + }, { 'events.$': 1 }); + }). + then(function(doc) { + assert.ok(doc); + assert.equal(doc.events.length, 1); + assert.equal(doc.events[0].element, 'Test'); + }). + catch(done); + }); }); + + it('embedded with single nested subdocs (gh-5244)', function(done) { + var eventSchema = new Schema({ message: String }, + { discriminatorKey: 'kind', _id: false }); + + var trackSchema = new Schema({ event: eventSchema }); + trackSchema.path('event').discriminator('Clicked', new Schema({ + element: String + }, { _id: false })); + trackSchema.path('event').discriminator('Purchased', new Schema({ + product: String + }, { _id: false })); + + var MyModel = db.model('gh5244', trackSchema); + var doc1 = { + event: { + kind: 'Clicked', + element: 'Amazon Link' + } + }; + var doc2 = { + event: { + kind: 'Purchased', + product: 'Professional AngularJS' + } + }; + MyModel.create([doc1, doc2]). + then(function(docs) { + var doc1 = docs[0]; + var doc2 = docs[1]; + + assert.equal(doc1.event.kind, 'Clicked'); + assert.equal(doc1.event.element, 'Amazon Link'); + assert.ok(!doc1.event.product); + + assert.equal(doc2.event.kind, 'Purchased'); + assert.equal(doc2.event.product, 'Professional AngularJS'); + assert.ok(!doc2.event.element); + done(); + }). + catch(done); + }); + describe('embedded discriminators + hooks (gh-5706)', function(){ + var counters = { + eventPreSave: 0, + eventPostSave: 0, + purchasePreSave: 0, + purchasePostSave: 0, + eventPreValidate: 0, + eventPostValidate: 0, + purchasePreValidate: 0, + purchasePostValidate: 0, + }; + var eventSchema = new Schema( + { message: String }, + { discriminatorKey: 'kind', _id: false } + ); + eventSchema.pre('validate', function(next) { + counters.eventPreValidate++; + next(); + }); + + eventSchema.post('validate', function(doc) { + counters.eventPostValidate++; + }); + + eventSchema.pre('save', function(next) { + counters.eventPreSave++; + next(); + }); + + eventSchema.post('save', function(doc) { + counters.eventPostSave++; + }); + + var purchasedSchema = new Schema({ + product: String, + }, { _id: false }); + + purchasedSchema.pre('validate', function(next) { + counters.purchasePreValidate++; + next(); + }); + + purchasedSchema.post('validate', function(doc) { + counters.purchasePostValidate++; + }); + + purchasedSchema.pre('save', function(next) { + counters.purchasePreSave++; + next(); + }); + + purchasedSchema.post('save', function(doc) { + counters.purchasePostSave++; + }); + + beforeEach(function() { + Object.keys(counters).forEach(function(i) { + counters[i] = 0; + }); + }); + + it('should call the hooks on the embedded document defined by both the parent and discriminated schemas', function(done){ + var trackSchema = new Schema({ + event: eventSchema, + }); + + var embeddedEventSchema = trackSchema.path('event'); + embeddedEventSchema.discriminator('Purchased', purchasedSchema.clone()); + + var TrackModel = db.model('Track', trackSchema); + var doc = new TrackModel({ + event: { + message: 'Test', + kind: 'Purchased' + } + }); + doc.save(function(err){ + assert.ok(!err); + assert.equal(doc.event.message, 'Test') + assert.equal(doc.event.kind, 'Purchased') + Object.keys(counters).forEach(function(i) { + assert.equal(counters[i], 1); + }); + done(); + }) + }) + + it('should call the hooks on the embedded document in an embedded array defined by both the parent and discriminated schemas', function(done){ + var trackSchema = new Schema({ + events: [eventSchema], + }); + + var embeddedEventSchema = trackSchema.path('events'); + embeddedEventSchema.discriminator('Purchased', purchasedSchema.clone()); + + var TrackModel = db.model('Track2', trackSchema); + var doc = new TrackModel({ + events: [ + { + message: 'Test', + kind: 'Purchased' + }, + { + message: 'TestAgain', + kind: 'Purchased' + } + ] + }); + doc.save(function(err){ + assert.ok(!err); + assert.equal(doc.events[0].kind, 'Purchased'); + assert.equal(doc.events[0].message, 'Test'); + assert.equal(doc.events[1].kind, 'Purchased'); + assert.equal(doc.events[1].message, 'TestAgain'); + Object.keys(counters).forEach(function(i) { + assert.equal(counters[i], 2); + }); + done(); + }) + }) + }) }); }); diff --git a/test/model.field.selection.test.js b/test/model.field.selection.test.js index eaad22e7bfb..903f7988f9c 100644 --- a/test/model.field.selection.test.js +++ b/test/model.field.selection.test.js @@ -2,61 +2,64 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ObjectId = Schema.Types.ObjectId - , DocumentObjectId = mongoose.Types.ObjectId; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema, + ObjectId = Schema.Types.ObjectId, + DocumentObjectId = mongoose.Types.ObjectId; -/** - * Setup. - */ - -var Comments = new Schema; - -Comments.add({ - title : String - , date : Date - , body : String - , comments : [Comments] -}); +describe('model field selection', function() { + var Comments; + var BlogPostB; + var modelName; + var collection; + + before(function() { + Comments = new Schema; + + Comments.add({ + title: String, + date: Date, + body: String, + comments: [Comments] + }); -var BlogPostB = new Schema({ - title : String - , author : String - , slug : String - , date : Date - , meta : { - date : Date - , visitors : Number - } - , published : Boolean - , mixed : {} - , numbers : [Number] - , tags : [String] - , sigs : [Buffer] - , owners : [ObjectId] - , comments : [Comments] - , def : { type: String, default: 'kandinsky' } -}); + BlogPostB = new Schema({ + title: String, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number + }, + published: Boolean, + mixed: {}, + numbers: [Number], + tags: [String], + sigs: [Buffer], + owners: [ObjectId], + comments: [Comments], + def: {type: String, default: 'kandinsky'} + }); -var modelName = 'model.select.blogpost'; -mongoose.model(modelName, BlogPostB); -var collection = 'blogposts_' + random(); + modelName = 'model.select.blogpost'; + mongoose.model(modelName, BlogPostB); + collection = 'blogposts_' + random(); + }); -describe('model field selection', function() { it('excluded fields should be undefined', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection) - , date = new Date; + var db = start(), + BlogPostB = db.model(modelName, collection), + date = new Date; var doc = { - title: 'subset 1' - , author: 'me' - , comments: [{ title: 'first comment', date: new Date }, { title: '2nd', date: new Date }] - , meta: { date: date } + title: 'subset 1', + author: 'me', + comments: [{title: 'first comment', date: new Date}, {title: '2nd', date: new Date}], + meta: {date: date} }; BlogPostB.create(doc, function(err, created) { @@ -71,13 +74,13 @@ describe('model field selection', function() { assert.strictEqual('kandinsky', found.def); assert.strictEqual('me', found.author); assert.strictEqual(true, Array.isArray(found.numbers)); - assert.equal(undefined, found.meta.date); + assert.equal(found.meta.date, undefined); assert.equal(found.numbers.length, 0); - assert.equal(undefined, found.owners); + assert.equal(found.owners, undefined); assert.strictEqual(true, Array.isArray(found.comments)); assert.equal(found.comments.length, 2); found.comments.forEach(function(comment) { - assert.equal(undefined, comment.user); + assert.equal(comment.user, undefined); }); done(); }); @@ -85,12 +88,12 @@ describe('model field selection', function() { }); it('excluded fields should be undefined and defaults applied to other fields', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection) - , id = new DocumentObjectId - , date = new Date; + var db = start(), + BlogPostB = db.model(modelName, collection), + id = new DocumentObjectId, + date = new Date; - BlogPostB.collection.insert({ _id: id, title: 'hahaha1', meta: { date: date }}, function(err) { + BlogPostB.collection.insert({_id: id, title: 'hahaha1', meta: {date: date}}, function(err) { assert.ifError(err); BlogPostB.findById(id, {title: 0}, function(err, found) { @@ -109,30 +112,30 @@ describe('model field selection', function() { }); it('where subset of fields excludes _id', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); BlogPostB.create({title: 'subset 1'}, function(err) { assert.ifError(err); BlogPostB.findOne({title: 'subset 1'}, {title: 1, _id: 0}, function(err, found) { db.close(); assert.ifError(err); assert.strictEqual(undefined, found._id); - assert.equal(found.title,'subset 1'); + assert.equal(found.title, 'subset 1'); done(); }); }); }); it('works with subset of fields, excluding _id', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); BlogPostB.create({title: 'subset 1', author: 'me'}, function(err) { assert.ifError(err); BlogPostB.find({title: 'subset 1'}, {title: 1, _id: 0}, function(err, found) { db.close(); assert.ifError(err); assert.strictEqual(undefined, found[0]._id); - assert.equal(found[0].title,'subset 1'); + assert.equal(found[0].title, 'subset 1'); assert.strictEqual(undefined, found[0].def); assert.strictEqual(undefined, found[0].author); assert.strictEqual(false, Array.isArray(found[0].comments)); @@ -144,49 +147,49 @@ describe('model field selection', function() { it('works with just _id and findOneAndUpdate (gh-3407)', function(done) { var db = start(); - var MyModel = db.model('gh3407', { test: { type: Number, default: 1 } }); + var MyModel = db.model('gh3407', {test: {type: Number, default: 1}}); MyModel.collection.insert({}, function(error) { assert.ifError(error); - MyModel.findOne({}, { _id: 1 }, function(error, doc) { + MyModel.findOne({}, {_id: 1}, function(error, doc) { assert.ifError(error); assert.ok(!doc.test); - done(); + db.close(done); }); }); }); it('works with subset of fields excluding emebedded doc _id (gh-541)', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); - BlogPostB.create({title: 'LOTR', comments: [{ title: ':)' }]}, function(err, created) { + BlogPostB.create({title: 'LOTR', comments: [{title: ':)'}]}, function(err, created) { assert.ifError(err); - BlogPostB.find({_id: created}, { _id: 0, 'comments._id': 0 }, function(err, found) { + BlogPostB.find({_id: created}, {_id: 0, 'comments._id': 0}, function(err, found) { db.close(); assert.ifError(err); assert.strictEqual(undefined, found[0]._id); - assert.equal(found[0].title,'LOTR'); + assert.equal(found[0].title, 'LOTR'); assert.strictEqual('kandinsky', found[0].def); assert.strictEqual(undefined, found[0].author); assert.strictEqual(true, Array.isArray(found[0].comments)); - assert.equal(found[0].comments.length,1); + assert.equal(found[0].comments.length, 1); assert.equal(found[0].comments[0].title, ':)'); assert.strictEqual(undefined, found[0].comments[0]._id); // gh-590 - assert.strictEqual(null, found[0].comments[0].id); + assert.ok(!found[0].comments[0].id); done(); }); }); }); it('included fields should have defaults applied when no value exists in db (gh-870)', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection) - , id = new DocumentObjectId; + var db = start(), + BlogPostB = db.model(modelName, collection), + id = new DocumentObjectId; BlogPostB.collection.insert( - { _id: id, title: 'issue 870'}, { safe: true }, function(err) { + {_id: id, title: 'issue 870'}, {safe: true}, function(err) { assert.ifError(err); BlogPostB.findById(id, 'def comments', function(err, found) { @@ -198,30 +201,30 @@ describe('model field selection', function() { assert.strictEqual('kandinsky', found.def); assert.strictEqual(undefined, found.author); assert.strictEqual(true, Array.isArray(found.comments)); - assert.equal(0, found.comments.length); + assert.equal(found.comments.length, 0); done(); }); }); }); it('including subdoc field excludes other subdoc fields (gh-1027)', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); - BlogPostB.create({ comments: [{title: 'a'}, {title:'b'}] }, function(err, doc) { + BlogPostB.create({comments: [{title: 'a'}, {title: 'b'}]}, function(err, doc) { assert.ifError(err); BlogPostB.findById(doc._id).select('_id comments.title').exec(function(err, found) { db.close(); assert.ifError(err); assert.ok(found); - assert.equal(found._id.toString(), doc._id.toString()); + assert.equal(doc._id.toString(), found._id.toString()); assert.strictEqual(undefined, found.title); assert.strictEqual(true, Array.isArray(found.comments)); found.comments.forEach(function(comment) { - assert.equal(undefined, comment.body); - assert.equal(undefined, comment.comments); - assert.equal(undefined, comment._id); + assert.equal(comment.body, undefined); + assert.equal(comment.comments, undefined); + assert.equal(comment._id, undefined); assert.ok(!!comment.title); }); done(); @@ -230,29 +233,29 @@ describe('model field selection', function() { }); it('excluding nested subdoc fields (gh-1027)', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); - BlogPostB.create({ title: 'top', comments: [{title: 'a',body:'body'}, {title:'b', body:'body',comments: [{title:'c'}]}] }, function(err, doc) { + BlogPostB.create({title: 'top', comments: [{title: 'a', body: 'body'}, {title: 'b', body: 'body', comments: [{title: 'c'}]}]}, function(err, doc) { assert.ifError(err); BlogPostB.findById(doc._id).select('-_id -comments.title -comments.comments.comments -numbers').exec(function(err, found) { db.close(); assert.ifError(err); assert.ok(found); - assert.equal(undefined, found._id); - assert.strictEqual('top', found.title); - assert.equal(undefined, found.numbers); + assert.equal(found._id, undefined); + assert.strictEqual(found.title, 'top'); + assert.equal(found.numbers, undefined); assert.strictEqual(true, Array.isArray(found.comments)); found.comments.forEach(function(comment) { - assert.equal(undefined, comment.title); - assert.equal('body', comment.body); - assert.strictEqual(true, Array.isArray(comment.comments)); + assert.equal(comment.title, undefined); + assert.equal(comment.body, 'body'); + assert.strictEqual(Array.isArray(comment.comments), true); assert.ok(comment._id); comment.comments.forEach(function(comment) { - assert.equal('c', comment.title); - assert.equal(undefined, comment.body); - assert.equal(undefined, comment.comments); + assert.equal(comment.title, 'c'); + assert.equal(comment.body, undefined); + assert.equal(comment.comments, undefined); assert.ok(comment._id); }); }); @@ -275,66 +278,78 @@ describe('model field selection', function() { var _id1 = new mongoose.Types.ObjectId; var _id2 = new mongoose.Types.ObjectId; - B.create({ ids: [_id1, _id2] }, function(err, doc) { + B.create({ids: [_id1, _id2]}, function(err, doc) { assert.ifError(err); B .findById(doc._id) - .select({ ids: { $elemMatch: { $in: [_id2.toString()] }}}) + .select({ids: {$elemMatch: {$in: [_id2.toString()]}}}) .exec(function(err, found) { assert.ifError(err); assert.ok(found); assert.equal(found.id, doc.id); - assert.equal(1, found.ids.length); - assert.equal(_id2.toString(), found.ids[0].toString()); + assert.equal(found.ids.length, 1); + assert.equal(found.ids[0].toString(), _id2.toString()); B - .find({ _id: doc._id }) - .select({ ids: { $elemMatch: { $in: [_id2.toString()] }}}) + .find({_id: doc._id}) + .select({ids: {$elemMatch: {$in: [_id2.toString()]}}}) .exec(function(err, found) { assert.ifError(err); assert.ok(found.length); found = found[0]; assert.equal(found.id, doc.id); - assert.equal(1, found.ids.length); - assert.equal(_id2.toString(), found.ids[0].toString()); - done(); + assert.equal(found.ids.length, 1); + assert.equal(found.ids[0].toString(), _id2.toString()); + db.close(done); }); }); }); }); - it('disallows saving modified elemMatch paths (gh-1334)', function(done) { + it('saves modified elemMatch paths (gh-1334)', function(done) { var db = start(); var postSchema = new Schema({ - ids: [{type: Schema.ObjectId}] - , ids2: [{type: Schema.ObjectId}] + ids: [{type: Schema.ObjectId}], + ids2: [{type: Schema.ObjectId}] }); var B = db.model('gh-1334', postSchema); var _id1 = new mongoose.Types.ObjectId; var _id2 = new mongoose.Types.ObjectId; - B.create({ ids: [_id1, _id2], ids2: [_id2, _id1] }, function(err, doc) { + B.create({ids: [_id1, _id2], ids2: [_id2, _id1]}, function(err, doc) { assert.ifError(err); B .findById(doc._id) - .select({ ids: { $elemMatch: { $in: [_id2.toString()] }}}) - .select({ ids2: { $elemMatch: { $in: [_id1.toString()] }}}) + .select({ids2: {$elemMatch: {$in: [_id1.toString()]}}}) .exec(function(err, found) { assert.ifError(err); - assert.equal(1, found.ids.length); - assert.equal(1, found.ids2.length); - found.ids = []; + assert.equal(found.ids2.length, 1); found.ids2.set(0, _id2); + found.save(function(err) { - db.close(); - assert.ok(/\$elemMatch projection/.test(err)); - assert.ok(/ ids/.test(err)); - assert.ok(/ ids2/.test(err)); - done(); + assert.ifError(err); + + B + .findById(doc._id) + .select({ids: {$elemMatch: {$in: [_id2.toString()]}}}) + .select('ids2') + .exec(function(err, found) { + assert.equal(2, found.ids2.length); + assert.equal(_id2.toHexString(), found.ids2[0].toHexString()); + assert.equal(_id2.toHexString(), found.ids2[1].toHexString()); + + found.ids.pull(_id2); + + found.save(function(err) { + assert.ok(err); + + db.close(done); + }); + }); }); }); }); @@ -344,13 +359,13 @@ describe('model field selection', function() { var db = start(); var postSchema = new Schema({ - tags: [{ tag: String, count: 0 }] + tags: [{tag: String, count: 0}] }); var Post = db.model('gh-2031', postSchema, 'gh-2031'); - Post.create({ tags: [{ tag: 'bacon', count: 2 }, { tag: 'eggs', count: 3 }] }, function(error) { + Post.create({tags: [{tag: 'bacon', count: 2}, {tag: 'eggs', count: 3}]}, function(error) { assert.ifError(error); - Post.findOne({ 'tags.tag': 'eggs' }, { 'tags.$': 1 }, function(error, post) { + Post.findOne({'tags.tag': 'eggs'}, {'tags.$': 1}, function(error, post) { assert.ifError(error); post.tags[0].count = 1; post.save(function(error) { @@ -363,40 +378,60 @@ describe('model field selection', function() { }); it('selecting an array of docs applies defaults properly (gh-1108)', function(done) { - var db = start() - , M = db.model(modelName, collection); + var db = start(), + M = db.model(modelName, collection); - var m = new M({ title: '1108', comments: [{body:'yay'}] }); + var m = new M({title: '1108', comments: [{body: 'yay'}]}); m.comments[0].comments = undefined; m.save(function(err, doc) { assert.ifError(err); M.findById(doc._id).select('comments').exec(function(err, found) { assert.ifError(err); assert.ok(Array.isArray(found.comments)); - assert.equal(1, found.comments.length); + assert.equal(found.comments.length, 1); assert.ok(Array.isArray(found.comments[0].comments)); db.close(done); }); }); }); + it('select properties named length (gh-3903)', function(done) { + var db = start(); + + var schema = new mongoose.Schema({ + length: Number, + name: String + }); + + var MyModel = db.model('gh3903', schema); + + MyModel.create({ name: 'val', length: 3 }, function(error) { + assert.ifError(error); + MyModel.findOne({}).select({ length: 1 }).exec(function(error, doc) { + assert.ifError(error); + assert.ok(!doc.name); + db.close(done); + }); + }); + }); + it('appropriately filters subdocuments based on properties (gh-1280)', function(done) { var db = start(); var RouteSchema = new Schema({ - stations: { + stations: { start: { - name: { type: String }, - loc: { type: [Number], index: '2d' } + name: {type: String}, + loc: {type: [Number], index: '2d'} }, end: { - name: { type: String }, - loc: { type: [Number], index: '2d' } + name: {type: String}, + loc: {type: [Number], index: '2d'} }, points: [ - { - name: { type: String }, - loc: { type: [Number], index: '2d' } - } + { + name: {type: String}, + loc: {type: [Number], index: '2d'} + } ] } }); @@ -404,16 +439,16 @@ describe('model field selection', function() { var Route = db.model('Route' + random(), RouteSchema); var item = { - stations : { - start : { - name : "thing", - loc : [1,2] + stations: { + start: { + name: 'thing', + loc: [1, 2] }, - end : { - name : "thingend", - loc : [2,3] + end: { + name: 'thingend', + loc: [2, 3] }, - points : [ { name : "rawr" }] + points: [{name: 'rawr'}] } }; @@ -422,12 +457,12 @@ describe('model field selection', function() { Route.findById(i.id).select('-stations').exec(function(err, res) { assert.ifError(err); - assert.ok(res.stations.toString() === "undefined"); + assert.ok(res.stations.toString() === 'undefined'); Route.findById(i.id).select('-stations.start -stations.end').exec(function(err, res) { assert.ifError(err); - assert.equal(res.stations.start.toString(), "undefined"); - assert.equal(res.stations.end.toString(), "undefined"); + assert.equal(res.stations.start.toString(), 'undefined'); + assert.equal(res.stations.end.toString(), 'undefined'); assert.ok(Array.isArray(res.stations.points)); db.close(done); }); diff --git a/test/model.findAndRemoveOne.test.js b/test/model.findAndRemoveOne.test.js index 4a882ff612e..58008c5b281 100644 --- a/test/model.findAndRemoveOne.test.js +++ b/test/model.findAndRemoveOne.test.js @@ -3,85 +3,89 @@ * Test dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ObjectId = Schema.Types.ObjectId - , DocumentObjectId = mongoose.Types.ObjectId; +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + random = require('../lib/utils').random, + Schema = mongoose.Schema, + ObjectId = Schema.Types.ObjectId, + DocumentObjectId = mongoose.Types.ObjectId; -/** - * Setup. - */ - -var Comments = new Schema; - -Comments.add({ - title : String - , date : Date - , body : String - , comments : [Comments] -}); +describe('model: findOneAndRemove:', function() { + var Comments; + var BlogPost; + var modelname; + var collection; + var strictSchema; + + before(function() { + Comments = new Schema; + + Comments.add({ + title: String, + date: Date, + body: String, + comments: [Comments] + }); -var BlogPost = new Schema({ - title : String - , author : String - , slug : String - , date : Date - , meta : { - date : Date - , visitors : Number - } - , published : Boolean - , mixed : {} - , numbers : [Number] - , owners : [ObjectId] - , comments : [Comments] -}); + BlogPost = new Schema({ + title: String, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number + }, + published: Boolean, + mixed: {}, + numbers: [Number], + owners: [ObjectId], + comments: [Comments] + }); -BlogPost.virtual('titleWithAuthor') - .get(function() { - return this.get('title') + ' by ' + this.get('author'); - }) - .set(function(val) { - var split = val.split(' by '); - this.set('title', split[0]); - this.set('author', split[1]); - }); + BlogPost.virtual('titleWithAuthor') + .get(function() { + return this.get('title') + ' by ' + this.get('author'); + }) + .set(function(val) { + var split = val.split(' by '); + this.set('title', split[0]); + this.set('author', split[1]); + }); -BlogPost.method('cool', function() { - return this; -}); + BlogPost.method('cool', function() { + return this; + }); -BlogPost.static('woot', function() { - return this; -}); + BlogPost.static('woot', function() { + return this; + }); -var modelname = 'RemoveOneBlogPost'; -mongoose.model(modelname, BlogPost); + modelname = 'RemoveOneBlogPost'; + mongoose.model(modelname, BlogPost); -var collection = 'removeoneblogposts_' + random(); + collection = 'removeoneblogposts_' + random(); -var strictSchema = new Schema({ name: String }, { strict: true }); -mongoose.model('RemoveOneStrictSchema', strictSchema); + strictSchema = new Schema({name: String}, {strict: true}); + mongoose.model('RemoveOneStrictSchema', strictSchema); + }); -describe('model: findOneAndRemove:', function() { it('returns the original document', function(done) { - var db = start() - , M = db.model(modelname, collection) - , title = 'remove muah'; + var db = start(), + M = db.model(modelname, collection), + title = 'remove muah'; - var post = new M({ title: title }); + var post = new M({title: title}); post.save(function(err) { assert.ifError(err); - M.findOneAndRemove({ title: title }, function(err, doc) { + M.findOneAndRemove({title: title}, function(err, doc) { assert.ifError(err); - assert.equal(doc.id, post.id); + assert.equal(post.id, doc.id); M.findById(post.id, function(err, gone) { db.close(); assert.ifError(err); - assert.equal(null, gone); + assert.equal(gone, null); done(); }); }); @@ -89,58 +93,58 @@ describe('model: findOneAndRemove:', function() { }); it('options/conditions/doc are merged when no callback is passed', function(done) { - var db = start() - , M = db.model(modelname, collection); + var db = start(), + M = db.model(modelname, collection); db.close(); - var now = new Date - , query; + var now = new Date, + query; // Model.findOneAndRemove - query = M.findOneAndRemove({ author: 'aaron' }, { select: 'author' }); - assert.equal(1, query._fields.author); - assert.equal('aaron', query._conditions.author); + query = M.findOneAndRemove({author: 'aaron'}, {select: 'author'}); + assert.equal(query._fields.author, 1); + assert.equal(query._conditions.author, 'aaron'); - query = M.findOneAndRemove({ author: 'aaron' }); - assert.equal(undefined, query._fields); - assert.equal('aaron', query._conditions.author); + query = M.findOneAndRemove({author: 'aaron'}); + assert.equal(query._fields, undefined); + assert.equal(query._conditions.author, 'aaron'); query = M.findOneAndRemove(); - assert.equal(undefined, query.options.new); - assert.equal(undefined, query._fields); - assert.equal(undefined, query._conditions.author); + assert.equal(query.options.new, undefined); + assert.equal(query._fields, undefined); + assert.equal(query._conditions.author, undefined); // Query.findOneAndRemove - query = M.where('author', 'aaron').findOneAndRemove({ date: now }); - assert.equal(undefined, query._fields); - assert.equal(now, query._conditions.date); - assert.equal('aaron', query._conditions.author); + query = M.where('author', 'aaron').findOneAndRemove({date: now}); + assert.equal(query._fields, undefined); + assert.equal(query._conditions.date, now); + assert.equal(query._conditions.author, 'aaron'); - query = M.find().findOneAndRemove({ author: 'aaron' }, { select: 'author' }); - assert.equal(1, query._fields.author); - assert.equal('aaron', query._conditions.author); + query = M.find().findOneAndRemove({author: 'aaron'}, {select: 'author'}); + assert.equal(query._fields.author, 1); + assert.equal(query._conditions.author, 'aaron'); query = M.find().findOneAndRemove(); - assert.equal(undefined, query._fields); - assert.equal(undefined, query._conditions.author); + assert.equal(query._fields, undefined); + assert.equal(query._conditions.author, undefined); done(); }); it('executes when a callback is passed', function(done) { - var db = start() - , M = db.model(modelname, collection + random()) - , pending = 5; - - M.findOneAndRemove({ name: 'aaron1' }, { select: 'name' }, cb); - M.findOneAndRemove({ name: 'aaron1' }, cb); - M.where().findOneAndRemove({ name: 'aaron1' }, { select: 'name' }, cb); - M.where().findOneAndRemove({ name: 'aaron1' }, cb); + var db = start(), + M = db.model(modelname, collection + random()), + pending = 5; + + M.findOneAndRemove({name: 'aaron1'}, {select: 'name'}, cb); + M.findOneAndRemove({name: 'aaron1'}, cb); + M.where().findOneAndRemove({name: 'aaron1'}, {select: 'name'}, cb); + M.where().findOneAndRemove({name: 'aaron1'}, cb); M.where('name', 'aaron1').findOneAndRemove(cb); function cb(err, doc) { assert.ifError(err); - assert.equal(null, doc); // no previously existing doc + assert.equal(doc, null); // no previously existing doc if (--pending) return; db.close(); done(); @@ -148,9 +152,9 @@ describe('model: findOneAndRemove:', function() { }); it('executed with only a callback throws', function(done) { - var db = start() - , M = db.model(modelname, collection) - , err; + var db = start(), + M = db.model(modelname, collection), + err; try { M.findOneAndRemove(function() {}); @@ -163,13 +167,10 @@ describe('model: findOneAndRemove:', function() { done(); }); -}); - -describe('model: findByIdAndRemove:', function() { it('executed with only a callback throws', function(done) { - var db = start() - , M = db.model(modelname, collection) - , err; + var db = start(), + M = db.model(modelname, collection), + err; try { M.findByIdAndRemove(function() {}); @@ -183,17 +184,17 @@ describe('model: findByIdAndRemove:', function() { }); it('executes when a callback is passed', function(done) { - var db = start() - , M = db.model(modelname, collection + random()) - , _id = new DocumentObjectId - , pending = 2; + var db = start(), + M = db.model(modelname, collection + random()), + _id = new DocumentObjectId, + pending = 2; - M.findByIdAndRemove(_id, { select: 'name' }, cb); + M.findByIdAndRemove(_id, {select: 'name'}, cb); M.findByIdAndRemove(_id, cb); function cb(err, doc) { assert.ifError(err); - assert.equal(null, doc); // no previously existing doc + assert.equal(doc, null); // no previously existing doc if (--pending) return; db.close(); done(); @@ -201,20 +202,20 @@ describe('model: findByIdAndRemove:', function() { }); it('returns the original document', function(done) { - var db = start() - , M = db.model(modelname, collection) - , title = 'remove muah pleez'; + var db = start(), + M = db.model(modelname, collection), + title = 'remove muah pleez'; - var post = new M({ title: title }); + var post = new M({title: title}); post.save(function(err) { assert.ifError(err); M.findByIdAndRemove(post.id, function(err, doc) { assert.ifError(err); - assert.equal(doc.id, post.id); + assert.equal(post.id, doc.id); M.findById(post.id, function(err, gone) { db.close(); assert.ifError(err); - assert.equal(null, gone); + assert.equal(gone, null); done(); }); }); @@ -222,126 +223,126 @@ describe('model: findByIdAndRemove:', function() { }); it('options/conditions/doc are merged when no callback is passed', function(done) { - var db = start() - , M = db.model(modelname, collection) - , _id = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + _id = new DocumentObjectId; db.close(); var query; // Model.findByIdAndRemove - query = M.findByIdAndRemove(_id, { select: 'author' }); - assert.equal(1, query._fields.author); - assert.equal(_id.toString(), query._conditions._id.toString()); + query = M.findByIdAndRemove(_id, {select: 'author'}); + assert.equal(query._fields.author, 1); + assert.equal(query._conditions._id.toString(), _id.toString()); query = M.findByIdAndRemove(_id.toString()); - assert.equal(undefined, query._fields); - assert.equal(_id.toString(), query._conditions._id); + assert.equal(query._fields, undefined); + assert.equal(query._conditions._id, _id.toString()); query = M.findByIdAndRemove(); - assert.equal(undefined, query.options.new); - assert.equal(undefined, query._fields); - assert.equal(undefined, query._conditions._id); + assert.equal(query.options.new, undefined); + assert.equal(query._fields, undefined); + assert.equal(query._conditions._id, undefined); done(); }); it('supports v3 select string syntax', function(done) { - var db = start() - , M = db.model(modelname, collection) - , _id = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + _id = new DocumentObjectId; db.close(); var query; - query = M.findByIdAndRemove(_id, { select: 'author -title' }); + query = M.findByIdAndRemove(_id, {select: 'author -title'}); assert.strictEqual(1, query._fields.author); assert.strictEqual(0, query._fields.title); - query = M.findOneAndRemove({}, { select: 'author -title' }); + query = M.findOneAndRemove({}, {select: 'author -title'}); assert.strictEqual(1, query._fields.author); assert.strictEqual(0, query._fields.title); done(); }); it('supports v3 select object syntax', function(done) { - var db = start() - , M = db.model(modelname, collection) - , _id = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + _id = new DocumentObjectId; db.close(); var query; - query = M.findByIdAndRemove(_id, { select: { author: 1, title: 0 }}); + query = M.findByIdAndRemove(_id, {select: {author: 1, title: 0}}); assert.strictEqual(1, query._fields.author); assert.strictEqual(0, query._fields.title); - query = M.findOneAndRemove({}, { select: { author: 1, title: 0 }}); + query = M.findOneAndRemove({}, {select: {author: 1, title: 0}}); assert.strictEqual(1, query._fields.author); assert.strictEqual(0, query._fields.title); done(); }); it('supports v3 sort string syntax', function(done) { - var db = start() - , M = db.model(modelname, collection) - , _id = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + _id = new DocumentObjectId; db.close(); var query; - query = M.findByIdAndRemove(_id, { sort: 'author -title' }); - assert.equal(2, Object.keys(query.options.sort).length); - assert.equal(1, query.options.sort.author); - assert.equal(-1, query.options.sort.title); + query = M.findByIdAndRemove(_id, {sort: 'author -title'}); + assert.equal(Object.keys(query.options.sort).length, 2); + assert.equal(query.options.sort.author, 1); + assert.equal(query.options.sort.title, -1); - query = M.findOneAndRemove({}, { sort: 'author -title' }); - assert.equal(2, Object.keys(query.options.sort).length); - assert.equal(1, query.options.sort.author); - assert.equal(-1, query.options.sort.title); + query = M.findOneAndRemove({}, {sort: 'author -title'}); + assert.equal(Object.keys(query.options.sort).length, 2); + assert.equal(query.options.sort.author, 1); + assert.equal(query.options.sort.title, -1); done(); }); it('supports v3 sort object syntax', function(done) { - var db = start() - , M = db.model(modelname, collection) - , _id = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + _id = new DocumentObjectId; var query; - query = M.findByIdAndRemove(_id, { sort: { author: 1, title: -1 }}); - assert.equal(2, Object.keys(query.options.sort).length); - assert.equal(1, query.options.sort.author); - assert.equal(-1, query.options.sort.title); + query = M.findByIdAndRemove(_id, {sort: {author: 1, title: -1}}); + assert.equal(Object.keys(query.options.sort).length, 2); + assert.equal(query.options.sort.author, 1); + assert.equal(query.options.sort.title, -1); - query = M.findOneAndRemove(_id, { sort: { author: 1, title: -1 }}); - assert.equal(2, Object.keys(query.options.sort).length); - assert.equal(1, query.options.sort.author); - assert.equal(-1, query.options.sort.title); + query = M.findOneAndRemove(_id, {sort: {author: 1, title: -1}}); + assert.equal(Object.keys(query.options.sort).length, 2); + assert.equal(query.options.sort.author, 1); + assert.equal(query.options.sort.title, -1); db.close(done); }); it('supports population (gh-1395)', function(done) { var db = start(); - var M = db.model('A', { name: String }); - var N = db.model('B', { a: { type: Schema.ObjectId, ref: 'A' }, i: Number}); + var M = db.model('A', {name: String}); + var N = db.model('B', {a: {type: Schema.ObjectId, ref: 'A'}, i: Number}); - M.create({ name: 'i am an A' }, function(err, a) { + M.create({name: 'i am an A'}, function(err, a) { if (err) return done(err); - N.create({ a: a._id, i: 10 }, function(err, b) { + N.create({a: a._id, i: 10}, function(err, b) { if (err) return done(err); - N.findOneAndRemove({ _id: b._id }, { select: 'a -_id' }) + N.findOneAndRemove({_id: b._id}, {select: 'a -_id'}) .populate('a') .exec(function(err, doc) { if (err) return done(err); assert.ok(doc); - assert.equal(undefined, doc._id); + assert.equal(doc._id, undefined); assert.ok(doc.a); - assert.equal(doc.a.name, 'i am an A'); + assert.equal('i am an A', doc.a.name); db.close(done); }); }); @@ -361,7 +362,7 @@ describe('model: findByIdAndRemove:', function() { it('works', function(done) { var s = new Schema({ - topping: { type: String, default: 'bacon' }, + topping: {type: String, default: 'bacon'}, base: String }); @@ -384,13 +385,13 @@ describe('model: findByIdAndRemove:', function() { assert.ifError(error); Breakfast.findOneAndRemove( - { base: 'eggs' }, + {base: 'eggs'}, {}, function(error, breakfast) { assert.ifError(error); - assert.equal('eggs', breakfast.base); - assert.equal(1, preCount); - assert.equal(1, postCount); + assert.equal(breakfast.base, 'eggs'); + assert.equal(preCount, 1); + assert.equal(postCount, 1); done(); }); }); @@ -398,7 +399,7 @@ describe('model: findByIdAndRemove:', function() { it('works with exec()', function(done) { var s = new Schema({ - topping: { type: String, default: 'bacon' }, + topping: {type: String, default: 'bacon'}, base: String }); @@ -421,12 +422,12 @@ describe('model: findByIdAndRemove:', function() { assert.ifError(error); Breakfast. - findOneAndRemove({ base: 'eggs' }, {}). + findOneAndRemove({base: 'eggs'}, {}). exec(function(error, breakfast) { assert.ifError(error); - assert.equal('eggs', breakfast.base); - assert.equal(1, preCount); - assert.equal(1, postCount); + assert.equal(breakfast.base, 'eggs'); + assert.equal(preCount, 1); + assert.equal(postCount, 1); done(); }); }); diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index fdc25295b23..a11dd73361b 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -1,86 +1,91 @@ - /** * Test dependencies. */ var CastError = require('../lib/error/cast'); var start = require('./common'); -var assert = require('assert'); +var assert = require('power-assert'); var mongoose = start.mongoose; var random = require('../lib/utils').random; var Utils = require('../lib/utils'); var Schema = mongoose.Schema; var ObjectId = Schema.Types.ObjectId; var DocumentObjectId = mongoose.Types.ObjectId; -var _ = require('underscore'); - -/** - * Setup. - */ +var _ = require('lodash'); +var uuid = require('uuid'); -var Comments = new Schema(); - -Comments.add({ - title : String - , date : Date - , body : String - , comments : [Comments] -}); +describe('model: findOneAndUpdate:', function() { + var Comments; + var BlogPost; + var modelname; + var collection; + var strictSchema; + var strictThrowSchema; + + before(function() { + Comments = new Schema(); + + Comments.add({ + title: String, + date: Date, + body: String, + comments: [Comments] + }); -var BlogPost = new Schema({ - title : String - , author : String - , slug : String - , date : Date - , meta : { - date : Date - , visitors : Number - } - , published : Boolean - , mixed : {} - , numbers : [Number] - , owners : [ObjectId] - , comments : [Comments] -}); + BlogPost = new Schema({ + title: String, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number + }, + published: Boolean, + mixed: {}, + numbers: [Number], + owners: [ObjectId], + comments: [Comments] + }); -BlogPost.virtual('titleWithAuthor') - .get(function() { - return this.get('title') + ' by ' + this.get('author'); - }) - .set(function(val) { - var split = val.split(' by '); - this.set('title', split[0]); - this.set('author', split[1]); - }); + BlogPost.virtual('titleWithAuthor') + .get(function() { + return this.get('title') + ' by ' + this.get('author'); + }) + .set(function(val) { + var split = val.split(' by '); + this.set('title', split[0]); + this.set('author', split[1]); + }); -BlogPost.method('cool', function() { - return this; -}); + BlogPost.method('cool', function() { + return this; + }); -BlogPost.static('woot', function() { - return this; -}); + BlogPost.static('woot', function() { + return this; + }); -var modelname = 'UpdateOneBlogPost'; -mongoose.model(modelname, BlogPost); + modelname = 'UpdateOneBlogPost'; + mongoose.model(modelname, BlogPost); -var collection = 'updateoneblogposts_' + random(); + collection = 'updateoneblogposts_' + random(); -var strictSchema = new Schema({ name: String }, { strict: true }); -mongoose.model('UpdateOneStrictSchema', strictSchema); + strictSchema = new Schema({name: String}, {strict: true}); + mongoose.model('UpdateOneStrictSchema', strictSchema); -var strictThrowSchema = new Schema({ name: String }, { strict: 'throw'}); -mongoose.model('UpdateOneStrictThrowSchema', strictThrowSchema); + strictThrowSchema = new Schema({name: String}, {strict: 'throw'}); + mongoose.model('UpdateOneStrictThrowSchema', strictThrowSchema); + }); -describe('model: findOneAndUpdate:', function() { it('WWW returns the edited document', function(done) { - var db = start() - , M = db.model(modelname, collection) - , title = 'Tobi ' + random() - , author = 'Brian ' + random() - , newTitle = 'Woot ' + random() - , id0 = new DocumentObjectId - , id1 = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + title = 'Tobi ' + random(), + author = 'Brian ' + random(), + newTitle = 'Woot ' + random(), + id0 = new DocumentObjectId, + id1 = new DocumentObjectId; var post = new M; post.set('title', title); @@ -88,60 +93,60 @@ describe('model: findOneAndUpdate:', function() { post.meta.visitors = 0; post.date = new Date; post.published = true; - post.mixed = { x: 'ex' }; - post.numbers = [4,5,6,7]; + post.mixed = {x: 'ex'}; + post.numbers = [4, 5, 6, 7]; post.owners = [id0, id1]; - post.comments = [{ body: 'been there' }, { body: 'done that' }]; + post.comments = [{body: 'been there'}, {body: 'done that'}]; post.save(function(err) { assert.ifError(err); M.findById(post._id, function(err, cf) { assert.ifError(err); - assert.equal(title, cf.title); - assert.equal(author, cf.author); - assert.equal(0, cf.meta.visitors.valueOf()); - assert.equal(post.date.toString(), cf.date); - assert.equal(true, cf.published); - assert.equal('ex', cf.mixed.x); - assert.deepEqual([4,5,6,7], cf.numbers.toObject()); - assert.equal(2, cf.owners.length); - assert.equal(id0.toString(),cf.owners[0].toString() ); - assert.equal(id1.toString(),cf.owners[1].toString() ); - assert.equal(2, cf.comments.length); - assert.equal('been there', cf.comments[0].body); - assert.equal('done that', cf.comments[1].body); + assert.equal(cf.title, title); + assert.equal(cf.author, author); + assert.equal(cf.meta.visitors.valueOf(), 0); + assert.equal(cf.date, post.date.toString()); + assert.equal(cf.published, true); + assert.equal(cf.mixed.x, 'ex'); + assert.deepEqual([4, 5, 6, 7], cf.numbers.toObject()); + assert.equal(cf.owners.length, 2); + assert.equal(cf.owners[0].toString(), id0.toString()); + assert.equal(cf.owners[1].toString(), id1.toString()); + assert.equal(cf.comments.length, 2); + assert.equal(cf.comments[0].body, 'been there'); + assert.equal(cf.comments[1].body, 'done that'); assert.ok(cf.comments[0]._id); assert.ok(cf.comments[1]._id); assert.ok(cf.comments[0]._id instanceof DocumentObjectId); assert.ok(cf.comments[1]._id instanceof DocumentObjectId); var update = { - title: newTitle // becomes $set - , $inc: { 'meta.visitors': 2 } - , $set: { date: new Date } - , published: false // becomes $set - , 'mixed': { x: 'ECKS', y: 'why' } // $set - , $pullAll: { 'numbers': [4, 6] } - , $pull: { 'owners': id0 } - , 'comments.1.body': 8 // $set + title: newTitle, // becomes $set + $inc: {'meta.visitors': 2}, + $set: {date: new Date}, + published: false, // becomes $set + mixed: {x: 'ECKS', y: 'why'}, // $set + $pullAll: {numbers: [4, 6]}, + $pull: {owners: id0}, + 'comments.1.body': 8 // $set }; - M.findOneAndUpdate({ title: title }, update, { 'new': true }, function(err, up) { + M.findOneAndUpdate({title: title}, update, {new: true}, function(err, up) { db.close(); - assert.equal(err, null, err && err.stack); - - assert.equal(newTitle, up.title); - assert.equal(author, up.author); - assert.equal(2, up.meta.visitors.valueOf()); - assert.equal(update.$set.date.toString(),up.date.toString()); - assert.equal(false, up.published); - assert.equal('ECKS', up.mixed.x); - assert.equal('why', up.mixed.y); - assert.deepEqual([5,7], up.numbers.toObject()); - assert.equal(1, up.owners.length); - assert.equal(id1.toString(),up.owners[0].toString()); - assert.equal('been there', up.comments[0].body); - assert.equal('8', up.comments[1].body); + assert.equal(err && err.stack, err, null); + + assert.equal(up.title, newTitle); + assert.equal(up.author, author); + assert.equal(up.meta.visitors.valueOf(), 2); + assert.equal(up.date.toString(), update.$set.date.toString()); + assert.equal(up.published, false); + assert.equal(up.mixed.x, 'ECKS'); + assert.equal(up.mixed.y, 'why'); + assert.deepEqual([5, 7], up.numbers.toObject()); + assert.equal(up.owners.length, 1); + assert.equal(up.owners[0].toString(), id1.toString()); + assert.equal(up.comments[0].body, 'been there'); + assert.equal(up.comments[1].body, '8'); assert.ok(up.comments[0]._id); assert.ok(up.comments[1]._id); assert.ok(up.comments[0]._id instanceof DocumentObjectId); @@ -159,7 +164,9 @@ describe('model: findOneAndUpdate:', function() { db = start(); var itemSpec = new Schema({ item_id: { - type: ObjectId, required: true, default: function() { return new DocumentObjectId();} + type: ObjectId, required: true, default: function() { + return new DocumentObjectId(); + } }, address: { street: String, @@ -181,51 +188,51 @@ describe('model: findOneAndUpdate:', function() { it('update subdocument in array item', function(done) { var item1 = new ItemChildModel({ address: { - street: "times square", - zipcode: "10036" + street: 'times square', + zipcode: '10036' } }); var item2 = new ItemChildModel({ address: { - street: "bryant park", - zipcode: "10030" + street: 'bryant park', + zipcode: '10030' } }); var item3 = new ItemChildModel({ address: { - street: "queens", - zipcode: "1002?" + street: 'queens', + zipcode: '1002?' } }); - var itemParent = new ItemParentModel({items:[item1, item2, item3]}); + var itemParent = new ItemParentModel({items: [item1, item2, item3]}); itemParent.save(function(err) { assert.ifError(err); ItemParentModel.findOneAndUpdate( - {"_id": itemParent._id, "items.item_id": item1.item_id}, - {"$set":{ "items.$.address":{}}}, - {new: true}, - function(err, updatedDoc) { - assert.ifError(err); - assert.ok(updatedDoc.items); - assert.ok(updatedDoc.items instanceof Array); - assert.ok(updatedDoc.items.length, 3); - assert.ok(Utils.isObject(updatedDoc.items[0].address)); - assert.ok(Object.keys(updatedDoc.items[0].address).length, 0); - done(); - } + {_id: itemParent._id, 'items.item_id': item1.item_id}, + {$set: {'items.$.address': {}}}, + {new: true}, + function(err, updatedDoc) { + assert.ifError(err); + assert.ok(updatedDoc.items); + assert.ok(updatedDoc.items instanceof Array); + assert.ok(updatedDoc.items.length, 3); + assert.ok(Utils.isObject(updatedDoc.items[0].address)); + assert.ok(Object.keys(updatedDoc.items[0].address).length, 0); + done(); + } ); }); }); }); it('returns the original document', function(done) { - var db = start() - , M = db.model(modelname, collection) - , title = 'Tobi ' + random() - , author = 'Brian ' + random() - , newTitle = 'Woot ' + random() - , id0 = new DocumentObjectId - , id1 = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + title = 'Tobi ' + random(), + author = 'Brian ' + random(), + newTitle = 'Woot ' + random(), + id0 = new DocumentObjectId, + id1 = new DocumentObjectId; var post = new M; post.set('title', title); @@ -233,10 +240,10 @@ describe('model: findOneAndUpdate:', function() { post.meta.visitors = 0; post.date = new Date; post.published = true; - post.mixed = { x: 'ex' }; - post.numbers = [4,5,6,7]; + post.mixed = {x: 'ex'}; + post.numbers = [4, 5, 6, 7]; post.owners = [id0, id1]; - post.comments = [{ body: 'been there' }, { body: 'done that' }]; + post.comments = [{body: 'been there'}, {body: 'done that'}]; post.save(function(err) { assert.ifError(err); @@ -244,32 +251,32 @@ describe('model: findOneAndUpdate:', function() { assert.ifError(err); var update = { - title: newTitle // becomes $set - , $inc: { 'meta.visitors': 2 } - , $set: { date: new Date } - , published: false // becomes $set - , 'mixed': { x: 'ECKS', y: 'why' } // $set - , $pullAll: { 'numbers': [4, 6] } - , $pull: { 'owners': id0 } - , 'comments.1.body': 8 // $set + title: newTitle, // becomes $set + $inc: {'meta.visitors': 2}, + $set: {date: new Date}, + published: false, // becomes $set + mixed: {x: 'ECKS', y: 'why'}, // $set + $pullAll: {numbers: [4, 6]}, + $pull: {owners: id0}, + 'comments.1.body': 8 // $set }; - M.findOneAndUpdate({ title: title }, update, { new: false }, function(err, up) { + M.findOneAndUpdate({title: title}, update, {new: false}, function(err, up) { db.close(); assert.ifError(err); - assert.equal(post.title, up.title); - assert.equal(post.author, up.author); - assert.equal(post.meta.visitors, up.meta.visitors.valueOf()); - assert.equal(up.date.toString(),post.date.toString()); - assert.equal(up.published,post.published); - assert.equal(up.mixed.x,post.mixed.x); - assert.equal(up.mixed.y, post.mixed.y); + assert.equal(up.title, post.title); + assert.equal(up.author, post.author); + assert.equal(up.meta.visitors.valueOf(), post.meta.visitors); + assert.equal(post.date.toString(), up.date.toString()); + assert.equal(post.published, up.published); + assert.equal(post.mixed.x, up.mixed.x); + assert.equal(post.mixed.y, up.mixed.y); assert.deepEqual(up.numbers.toObject(), post.numbers.toObject()); - assert.equal(up.owners.length, post.owners.length); - assert.equal(up.owners[0].toString(), post.owners[0].toString()); - assert.equal(up.comments[0].body, post.comments[0].body); - assert.equal(up.comments[1].body, post.comments[1].body); + assert.equal(post.owners.length, up.owners.length); + assert.equal(post.owners[0].toString(), up.owners[0].toString()); + assert.equal(post.comments[0].body, up.comments[0].body); + assert.equal(post.comments[1].body, up.comments[1].body); assert.ok(up.comments[0]._id); assert.ok(up.comments[1]._id); assert.ok(up.comments[0]._id instanceof DocumentObjectId); @@ -281,13 +288,13 @@ describe('model: findOneAndUpdate:', function() { }); it('allows upserting', function(done) { - var db = start() - , M = db.model(modelname, collection) - , title = 'Tobi ' + random() - , author = 'Brian ' + random() - , newTitle = 'Woot ' + random() - , id0 = new DocumentObjectId - , id1 = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + title = 'Tobi ' + random(), + author = 'Brian ' + random(), + newTitle = 'Woot ' + random(), + id0 = new DocumentObjectId, + id1 = new DocumentObjectId; var post = new M; post.set('title', title); @@ -295,29 +302,29 @@ describe('model: findOneAndUpdate:', function() { post.meta.visitors = 0; post.date = new Date; post.published = true; - post.mixed = { x: 'ex' }; - post.numbers = [4,5,6,7]; + post.mixed = {x: 'ex'}; + post.numbers = [4, 5, 6, 7]; post.owners = [id0, id1]; - post.comments = [{ body: 'been there' }, { body: 'done that' }]; + post.comments = [{body: 'been there'}, {body: 'done that'}]; var update = { - title: newTitle // becomes $set - , $inc: { 'meta.visitors': 2 } - , $set: { date: new Date } - , published: false // becomes $set - , 'mixed': { x: 'ECKS', y: 'why' } // $set - , $pullAll: { 'numbers': [4, 6] } - , $pull: { 'owners': id0 } + title: newTitle, // becomes $set + $inc: {'meta.visitors': 2}, + $set: {date: new Date}, + published: false, // becomes $set + mixed: {x: 'ECKS', y: 'why'}, // $set + $pullAll: {numbers: [4, 6]}, + $pull: {owners: id0} }; - M.findOneAndUpdate({ title: title }, update, { upsert: true, new: true }, function(err, up) { + M.findOneAndUpdate({title: title}, update, {upsert: true, new: true}, function(err, up) { db.close(); assert.ifError(err); - assert.equal(newTitle, up.title); - assert.equal(2, up.meta.visitors.valueOf()); - assert.equal(up.date.toString(),update.$set.date.toString()); - assert.equal(update.published, up.published); + assert.equal(up.title, newTitle); + assert.equal(up.meta.visitors.valueOf(), 2); + assert.equal(update.$set.date.toString(), up.date.toString()); + assert.equal(up.published, update.published); assert.deepEqual(update.mixed.x, up.mixed.x); assert.strictEqual(up.mixed.y, update.mixed.y); assert.ok(Array.isArray(up.numbers)); @@ -329,108 +336,113 @@ describe('model: findOneAndUpdate:', function() { }); it('options/conditions/doc are merged when no callback is passed', function(done) { - var db = start() - , M = db.model(modelname, collection); + var db = start(), + M = db.model(modelname, collection); db.close(); - var now = new Date - , query; + var now = new Date, + query; // Model.findOneAndUpdate - query = M.findOneAndUpdate({ author: 'aaron' }, { $set: { date: now }}, { new: false, fields: 'author' }); + query = M.findOneAndUpdate({author: 'aaron'}, {$set: {date: now}}, {new: false, fields: 'author'}); assert.strictEqual(false, query.options.new); assert.strictEqual(1, query._fields.author); - assert.equal(now.toString(), query._update.$set.date.toString()); + assert.equal(query._update.$set.date.toString(), now.toString()); assert.strictEqual('aaron', query._conditions.author); - query = M.findOneAndUpdate({ author: 'aaron' }, { $set: { date: now }}); + query = M.findOneAndUpdate({author: 'aaron'}, {$set: {date: now}}); assert.strictEqual(undefined, query.options.new); - assert.equal(now.toString(), query._update.$set.date.toString()); + assert.equal(query._update.$set.date.toString(), now.toString()); assert.strictEqual('aaron', query._conditions.author); - query = M.findOneAndUpdate({ $set: { date: now }}); + query = M.findOneAndUpdate({$set: {date: now}}); assert.strictEqual(undefined, query.options.new); - assert.equal(now.toString(), query._update.$set.date.toString()); + assert.equal(query._update.$set.date.toString(), now.toString()); assert.strictEqual(undefined, query._conditions.author); query = M.findOneAndUpdate(); assert.strictEqual(undefined, query.options.new); - assert.equal(undefined, query._update); + assert.equal(query._update, undefined); assert.strictEqual(undefined, query._conditions.author); // Query.findOneAndUpdate - query = M.where('author', 'aaron').findOneAndUpdate({ date: now }); + query = M.where('author', 'aaron').findOneAndUpdate({date: now}); assert.strictEqual(undefined, query.options.new); - assert.equal(now.toString(), query._update.date.toString()); + assert.equal(query._update.date.toString(), now.toString()); assert.strictEqual('aaron', query._conditions.author); - query = M.find().findOneAndUpdate({ author: 'aaron' }, { date: now }); + query = M.find().findOneAndUpdate({author: 'aaron'}, {date: now}); assert.strictEqual(undefined, query.options.new); - assert.equal(now.toString(), query._update.date.toString()); + assert.equal(query._update.date.toString(), now.toString()); assert.strictEqual('aaron', query._conditions.author); - query = M.find().findOneAndUpdate({ date: now }); + query = M.find().findOneAndUpdate({date: now}); assert.strictEqual(undefined, query.options.new); - assert.equal(now.toString(), query._update.date.toString()); + assert.equal(query._update.date.toString(), now.toString()); assert.strictEqual(undefined, query._conditions.author); query = M.find().findOneAndUpdate(); assert.strictEqual(undefined, query.options.new); - assert.equal(undefined, query._update); + assert.equal(query._update, undefined); assert.strictEqual(undefined, query._conditions.author); done(); }); it('executes when a callback is passed', function(done) { - var db = start() - , M = db.model(modelname, collection + random()) - , pending = 6; + var db = start(), + M = db.model(modelname, collection + random()), + pending = 6; - M.findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron6'}}, { new: false }, cb); - M.findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron4'}}, cb); - M.where().findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron1'}}, { new: false }, cb); - M.where().findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron2'}}, cb); - M.where().findOneAndUpdate({ $set: { name: 'Aaron6'}}, cb); - M.where('name', 'aaron').findOneAndUpdate({ $set: { name: 'Aaron'}}).findOneAndUpdate(cb); + M.findOneAndUpdate({name: 'aaron'}, {$set: {name: 'Aaron6'}}, {new: false}, cb); + M.findOneAndUpdate({name: 'aaron'}, {$set: {name: 'Aaron4'}}, cb); + M.where().findOneAndUpdate({name: 'aaron'}, {$set: {name: 'Aaron1'}}, {new: false}, cb); + M.where().findOneAndUpdate({name: 'aaron'}, {$set: {name: 'Aaron2'}}, cb); + M.where().findOneAndUpdate({$set: {name: 'Aaron6'}}, cb); + M.where('name', 'aaron').findOneAndUpdate({$set: {name: 'Aaron'}}).findOneAndUpdate(cb); function cb(err, doc) { assert.ifError(err); assert.strictEqual(null, doc); // not an upsert, no previously existing doc - if (--pending) return; + if (--pending) { + return; + } db.close(); done(); } }); it('executes when a callback is passed to a succeeding function', function(done) { - var db = start() - , M = db.model(modelname, collection + random()) - , pending = 6; + var db = start(), + M = db.model(modelname, collection + random()), + pending = 6; - M.findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron'}}, { new: false }).exec(cb); - M.findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron'}}).exec(cb); - M.where().findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron'}}, { new: false }).exec(cb); - M.where().findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron'}}).exec(cb); - M.where().findOneAndUpdate({ $set: { name: 'Aaron'}}).exec(cb); - M.where('name', 'aaron').findOneAndUpdate({ $set: { name: 'Aaron'}}).exec(cb); + M.findOneAndUpdate({name: 'aaron'}, {$set: {name: 'Aaron'}}, {new: false}).exec(cb); + M.findOneAndUpdate({name: 'aaron'}, {$set: {name: 'Aaron'}}).exec(cb); + M.where().findOneAndUpdate({name: 'aaron'}, {$set: {name: 'Aaron'}}, {new: false}).exec(cb); + M.where().findOneAndUpdate({name: 'aaron'}, {$set: {name: 'Aaron'}}).exec(cb); + M.where().findOneAndUpdate({$set: {name: 'Aaron'}}).exec(cb); + M.where('name', 'aaron').findOneAndUpdate({$set: {name: 'Aaron'}}).exec(cb); function cb(err, doc) { assert.ifError(err); assert.strictEqual(null, doc); // not an upsert, no previously existing doc - if (--pending) return; + if (--pending) { + return; + } db.close(); done(); } }); it('executing with only a callback throws', function(done) { - var db = start() - , M = db.model(modelname, collection) - , err; + var db = start(), + M = db.model(modelname, collection), + err; try { - M.findOneAndUpdate(function() {}); + M.findOneAndUpdate(function() { + }); } catch (e) { err = e; } @@ -441,9 +453,9 @@ describe('model: findOneAndUpdate:', function() { }); it('updates numbers atomically', function(done) { - var db = start() - , BlogPost = db.model(modelname, collection) - , totalDocs = 4; + var db = start(), + BlogPost = db.model(modelname, collection), + totalDocs = 4; var post = new BlogPost(); post.set('meta.visitors', 5); @@ -451,19 +463,21 @@ describe('model: findOneAndUpdate:', function() { post.save(function(err) { assert.ifError(err); + function callback(err) { + assert.ifError(err); + --totalDocs || complete(); + } + for (var i = 0; i < 4; ++i) { BlogPost - .findOneAndUpdate({ _id: post._id }, { $inc: { 'meta.visitors': 1 }}, function(err) { - assert.ifError(err); - --totalDocs || complete(); - }); + .findOneAndUpdate({_id: post._id}, {$inc: {'meta.visitors': 1}}, callback); } function complete() { - BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) { + BlogPost.findOne({_id: post.get('_id')}, function(err, doc) { db.close(); assert.ifError(err); - assert.equal(9, doc.get('meta.visitors')); + assert.equal(doc.get('meta.visitors'), 9); done(); }); } @@ -473,29 +487,29 @@ describe('model: findOneAndUpdate:', function() { it('honors strict schemas', function(done) { var db = start(); var S = db.model('UpdateOneStrictSchema'); - var s = new S({ name: 'orange crush' }); + var s = new S({name: 'orange crush'}); s.save(function(err) { assert.ifError(err); var name = Date.now(); - S.findOneAndUpdate({ name: name }, { ignore: true }, { upsert: true, 'new': true }, function(err, doc) { + S.findOneAndUpdate({name: name}, {ignore: true}, {upsert: true, new: true}, function(err, doc) { assert.ifError(err); assert.ok(doc); assert.ok(doc._id); - assert.equal(undefined, doc.ignore); - assert.equal(undefined, doc._doc.ignore); - assert.equal(name, doc.name); - S.findOneAndUpdate({ name: 'orange crush' }, { ignore: true }, { upsert: true }, function(err, doc) { + assert.equal(doc.ignore, undefined); + assert.equal(doc._doc.ignore, undefined); + assert.equal(doc.name, name); + S.findOneAndUpdate({name: 'orange crush'}, {ignore: true}, {upsert: true}, function(err, doc) { assert.ifError(err); assert.ok(!doc.ignore); assert.ok(!doc._doc.ignore); - assert.equal('orange crush', doc.name); - S.findOneAndUpdate({ name: 'orange crush' }, { ignore: true }, function(err, doc) { + assert.equal(doc.name, 'orange crush'); + S.findOneAndUpdate({name: 'orange crush'}, {ignore: true}, function(err, doc) { db.close(); assert.ifError(err); assert.ok(!doc.ignore); assert.ok(!doc._doc.ignore); - assert.equal('orange crush', doc.name); + assert.equal(doc.name, 'orange crush'); done(); }); }); @@ -506,18 +520,18 @@ describe('model: findOneAndUpdate:', function() { it('returns errors with strict:throw schemas', function(done) { var db = start(); var S = db.model('UpdateOneStrictThrowSchema'); - var s = new S({ name: 'orange crush' }); + var s = new S({name: 'orange crush'}); s.save(function(err) { assert.ifError(err); var name = Date.now(); - S.findOneAndUpdate({ name: name }, { ignore: true }, { upsert: true }, function(err, doc) { + S.findOneAndUpdate({name: name}, {ignore: true}, {upsert: true}, function(err, doc) { assert.ok(err); assert.ok(/not in schema/.test(err)); assert.ok(!doc); - S.findOneAndUpdate({ _id: s._id }, { ignore: true }, function(err, doc) { + S.findOneAndUpdate({_id: s._id}, {ignore: true}, function(err, doc) { db.close(); assert.ok(err); assert.ok(/not in schema/.test(err)); @@ -527,16 +541,15 @@ describe('model: findOneAndUpdate:', function() { }); }); }); -}); -describe('model: findByIdAndUpdate:', function() { it('executing with just a callback throws', function(done) { - var db = start() - , M = db.model(modelname, collection) - , err; + var db = start(), + M = db.model(modelname, collection), + err; try { - M.findByIdAndUpdate(function() {}); + M.findByIdAndUpdate(function() { + }); } catch (e) { err = e; } @@ -547,47 +560,51 @@ describe('model: findByIdAndUpdate:', function() { }); it('executes when a callback is passed', function(done) { - var db = start() - , M = db.model(modelname, collection + random()) - , _id = new DocumentObjectId - , pending = 2; + var db = start(), + M = db.model(modelname, collection + random()), + _id = new DocumentObjectId, + pending = 2; - M.findByIdAndUpdate(_id, { $set: { name: 'Aaron'}}, { new: false }, cb); - M.findByIdAndUpdate(_id, { $set: { name: 'changed' }}, cb); + M.findByIdAndUpdate(_id, {$set: {name: 'Aaron'}}, {new: false}, cb); + M.findByIdAndUpdate(_id, {$set: {name: 'changed'}}, cb); function cb(err, doc) { assert.ifError(err); assert.strictEqual(null, doc); // no previously existing doc - if (--pending) return; + if (--pending) { + return; + } db.close(done); } }); it('executes when a callback is passed to a succeeding function', function(done) { - var db = start() - , M = db.model(modelname, collection + random()) - , _id = new DocumentObjectId - , pending = 2; + var db = start(), + M = db.model(modelname, collection + random()), + _id = new DocumentObjectId, + pending = 2; - M.findByIdAndUpdate(_id, { $set: { name: 'Aaron'}}, { new: false }).exec(cb); - M.findByIdAndUpdate(_id, { $set: { name: 'changed' }}).exec(cb); + M.findByIdAndUpdate(_id, {$set: {name: 'Aaron'}}, {new: false}).exec(cb); + M.findByIdAndUpdate(_id, {$set: {name: 'changed'}}).exec(cb); function cb(err, doc) { assert.ifError(err); assert.strictEqual(null, doc); // no previously existing doc - if (--pending) return; + if (--pending) { + return; + } db.close(done); } }); it('returns the original document', function(done) { - var db = start() - , M = db.model(modelname, collection) - , title = 'Tobi ' + random() - , author = 'Brian ' + random() - , newTitle = 'Woot ' + random() - , id0 = new DocumentObjectId - , id1 = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + title = 'Tobi ' + random(), + author = 'Brian ' + random(), + newTitle = 'Woot ' + random(), + id0 = new DocumentObjectId, + id1 = new DocumentObjectId; var post = new M; post.set('title', title); @@ -595,10 +612,10 @@ describe('model: findByIdAndUpdate:', function() { post.meta.visitors = 0; post.date = new Date; post.published = true; - post.mixed = { x: 'ex' }; - post.numbers = [4,5,6,7]; + post.mixed = {x: 'ex'}; + post.numbers = [4, 5, 6, 7]; post.owners = [id0, id1]; - post.comments = [{ body: 'been there' }, { body: 'done that' }]; + post.comments = [{body: 'been there'}, {body: 'done that'}]; post.save(function(err) { assert.ifError(err); @@ -606,31 +623,31 @@ describe('model: findByIdAndUpdate:', function() { assert.ifError(err); var update = { - title: newTitle // becomes $set - , $inc: { 'meta.visitors': 2 } - , $set: { date: new Date } - , published: false // becomes $set - , 'mixed': { x: 'ECKS', y: 'why' } // $set - , $pullAll: { 'numbers': [4, 6] } - , $pull: { 'owners': id0 } - , 'comments.1.body': 8 // $set + title: newTitle, // becomes $set + $inc: {'meta.visitors': 2}, + $set: {date: new Date}, + published: false, // becomes $set + mixed: {x: 'ECKS', y: 'why'}, // $set + $pullAll: {numbers: [4, 6]}, + $pull: {owners: id0}, + 'comments.1.body': 8 // $set }; - M.findByIdAndUpdate(post.id, update, { new: false }, function(err, up) { + M.findByIdAndUpdate(post.id, update, {new: false}, function(err, up) { assert.ifError(err); - assert.equal(up.title,post.title); - assert.equal(up.author,post.author); - assert.equal(up.meta.visitors.valueOf(), post.meta.visitors); - assert.equal(up.date.toString(), post.date.toString()); - assert.equal(up.published, post.published); - assert.equal(up.mixed.x, post.mixed.x); + assert.equal(post.title, up.title); + assert.equal(post.author, up.author); + assert.equal(post.meta.visitors, up.meta.visitors.valueOf()); + assert.equal(post.date.toString(), up.date.toString()); + assert.equal(post.published, up.published); + assert.equal(post.mixed.x, up.mixed.x); assert.strictEqual(up.mixed.y, post.mixed.y); assert.deepEqual(up.numbers.toObject(), post.numbers.toObject()); - assert.equal(up.owners.length, post.owners.length); - assert.equal(up.owners[0].toString(), post.owners[0].toString()); - assert.equal(up.comments[0].body, post.comments[0].body); - assert.equal(up.comments[1].body, post.comments[1].body); + assert.equal(post.owners.length, up.owners.length); + assert.equal(post.owners[0].toString(), up.owners[0].toString()); + assert.equal(post.comments[0].body, up.comments[0].body); + assert.equal(post.comments[1].body, up.comments[1].body); assert.ok(up.comments[0]._id); assert.ok(up.comments[1]._id); assert.ok(up.comments[0]._id instanceof DocumentObjectId); @@ -642,23 +659,23 @@ describe('model: findByIdAndUpdate:', function() { }); it('options/conditions/doc are merged when no callback is passed', function(done) { - var db = start() - , M = db.model(modelname, collection) - , _id = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + _id = new DocumentObjectId; - var now = new Date - , query; + var now = new Date, + query; // Model.findByIdAndUpdate - query = M.findByIdAndUpdate(_id, { $set: { date: now }}, { new: false, fields: 'author' }); + query = M.findByIdAndUpdate(_id, {$set: {date: now}}, {new: false, fields: 'author'}); assert.strictEqual(false, query.options.new); assert.strictEqual(1, query._fields.author); - assert.equal(now.toString(), query._update.$set.date.toString()); + assert.equal(query._update.$set.date.toString(), now.toString()); assert.strictEqual(_id.toString(), query._conditions._id.toString()); - query = M.findByIdAndUpdate(_id, { $set: { date: now }}); + query = M.findByIdAndUpdate(_id, {$set: {date: now}}); assert.strictEqual(undefined, query.options.new); - assert.equal(now.toString(), query._update.$set.date.toString()); + assert.equal(query._update.$set.date.toString(), now.toString()); assert.strictEqual(_id.toString(), query._conditions._id.toString()); query = M.findByIdAndUpdate(_id); @@ -667,100 +684,104 @@ describe('model: findByIdAndUpdate:', function() { query = M.findByIdAndUpdate(); assert.strictEqual(undefined, query.options.new); - assert.equal(undefined, query._update); + assert.equal(query._update, undefined); assert.strictEqual(undefined, query._conditions._id); db.close(done); }); it('supports v3 select string syntax', function(done) { - var db = start() - , M = db.model(modelname, collection) - , _id = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + _id = new DocumentObjectId; - var now = new Date - , query; + var now = new Date, + query; - query = M.findByIdAndUpdate(_id, { $set: { date: now }}, { select: 'author -title' }); + query = M.findByIdAndUpdate(_id, {$set: {date: now}}, {select: 'author -title'}); assert.strictEqual(1, query._fields.author); assert.strictEqual(0, query._fields.title); - query = M.findOneAndUpdate({}, { $set: { date: now }}, { select: 'author -title' }); + query = M.findOneAndUpdate({}, {$set: {date: now}}, {select: 'author -title'}); assert.strictEqual(1, query._fields.author); assert.strictEqual(0, query._fields.title); db.close(done); }); it('supports v3 select object syntax', function(done) { - var db = start() - , M = db.model(modelname, collection) - , _id = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + _id = new DocumentObjectId; - var now = new Date - , query; + var now = new Date, + query; - query = M.findByIdAndUpdate(_id, { $set: { date: now }}, { select: { author: 1, title: 0 }}); + query = M.findByIdAndUpdate(_id, {$set: {date: now}}, {select: {author: 1, title: 0}}); assert.strictEqual(1, query._fields.author); assert.strictEqual(0, query._fields.title); - query = M.findOneAndUpdate({}, { $set: { date: now }}, { select: { author: 1, title: 0 }}); + query = M.findOneAndUpdate({}, {$set: {date: now}}, {select: {author: 1, title: 0}}); assert.strictEqual(1, query._fields.author); assert.strictEqual(0, query._fields.title); db.close(done); }); it('supports v3 sort string syntax', function(done) { - var db = start() - , M = db.model(modelname, collection); + var db = start(), + M = db.model(modelname, collection); var now = new Date; var _id = new DocumentObjectId; var query; - query = M.findByIdAndUpdate(_id, { $set: { date: now }}, { sort: 'author -title' }); - assert.equal(2, Object.keys(query.options.sort).length); - assert.equal(1, query.options.sort.author); - assert.equal(-1, query.options.sort.title); + query = M.findByIdAndUpdate(_id, {$set: {date: now}}, {sort: 'author -title'}); + assert.equal(Object.keys(query.options.sort).length, 2); + assert.equal(query.options.sort.author, 1); + assert.equal(query.options.sort.title, -1); - query = M.findOneAndUpdate({}, { $set: { date: now }}, { sort: 'author -title' }); - assert.equal(2, Object.keys(query.options.sort).length); - assert.equal(1, query.options.sort.author); - assert.equal(-1, query.options.sort.title); + query = M.findOneAndUpdate({}, {$set: {date: now}}, {sort: 'author -title'}); + assert.equal(Object.keys(query.options.sort).length, 2); + assert.equal(query.options.sort.author, 1); + assert.equal(query.options.sort.title, -1); // gh-1887 M.create( - { title: 1, meta: {visitors: 0}} - , { title: 2, meta: {visitors: 10}} - , { title: 3, meta: {visitors: 5}} - , function(err) { - if (err) return done(err); - - M.findOneAndUpdate({}, { title: 'changed' }) - .sort({ 'meta.visitors': -1 }) - .exec(function(err, doc) { - if (err) return done(err); - assert.equal(10, doc.meta.visitors); - db.close(done); - }); - }); + {title: 1, meta: {visitors: 0}} + , {title: 2, meta: {visitors: 10}} + , {title: 3, meta: {visitors: 5}} + , function(err) { + if (err) { + return done(err); + } + + M.findOneAndUpdate({}, {title: 'changed'}) + .sort({'meta.visitors': -1}) + .exec(function(err, doc) { + if (err) { + return done(err); + } + assert.equal(doc.meta.visitors, 10); + db.close(done); + }); + }); }); it('supports v3 sort object syntax', function(done) { - var db = start() - , M = db.model(modelname, collection) - , _id = new DocumentObjectId; + var db = start(), + M = db.model(modelname, collection), + _id = new DocumentObjectId; - var now = new Date - , query; + var now = new Date, + query; - query = M.findByIdAndUpdate(_id, { $set: { date: now }}, { sort: { author: 1, title: -1 }}); - assert.equal(2, Object.keys(query.options.sort).length); - assert.equal(1, query.options.sort.author); - assert.equal(-1, query.options.sort.title); + query = M.findByIdAndUpdate(_id, {$set: {date: now}}, {sort: {author: 1, title: -1}}); + assert.equal(Object.keys(query.options.sort).length, 2); + assert.equal(query.options.sort.author, 1); + assert.equal(query.options.sort.title, -1); - query = M.findOneAndUpdate(_id, { $set: { date: now }}, { sort: { author: 1, title: -1 }}); - assert.equal(2, Object.keys(query.options.sort).length); - assert.equal(1, query.options.sort.author); - assert.equal(-1, query.options.sort.title); + query = M.findOneAndUpdate(_id, {$set: {date: now}}, {sort: {author: 1, title: -1}}); + assert.equal(Object.keys(query.options.sort).length, 2); + assert.equal(query.options.sort.author, 1); + assert.equal(query.options.sort.title, -1); db.close(done); }); @@ -769,27 +790,27 @@ describe('model: findByIdAndUpdate:', function() { var db = start(); var postSchema = new Schema({ - ids: [{type: Schema.ObjectId}] - , title: String + ids: [{type: Schema.ObjectId}], + title: String }); var B = db.model('gh-1091+1100', postSchema); var _id1 = new mongoose.Types.ObjectId; var _id2 = new mongoose.Types.ObjectId; - B.create({ ids: [_id1, _id2] }, function(err, doc) { + B.create({ids: [_id1, _id2]}, function(err, doc) { assert.ifError(err); B - .findByIdAndUpdate(doc._id, { title: 'woot' }, { 'new': true }) - .select({ title: 1, ids: { $elemMatch: { $in: [_id2.toString()] }}}) + .findByIdAndUpdate(doc._id, {title: 'woot'}, {new: true}) + .select({title: 1, ids: {$elemMatch: {$in: [_id2.toString()]}}}) .exec(function(err, found) { assert.ifError(err); assert.ok(found); - assert.equal(found.id, doc.id); - assert.equal('woot', found.title); - assert.equal(1, found.ids.length); - assert.equal(_id2.toString(), found.ids[0].toString()); + assert.equal(doc.id, found.id); + assert.equal(found.title, 'woot'); + assert.equal(found.ids.length, 1); + assert.equal(found.ids[0].toString(), _id2.toString()); db.close(done); }); }); @@ -797,21 +818,27 @@ describe('model: findByIdAndUpdate:', function() { it('supports population (gh-1395)', function(done) { var db = start(); - var M = db.model('A', { name: String }); - var N = db.model('B', { a: { type: Schema.ObjectId, ref: 'A' }, i: Number}); + var M = db.model('A', {name: String}); + var N = db.model('B', {a: {type: Schema.ObjectId, ref: 'A'}, i: Number}); - M.create({ name: 'i am an A' }, function(err, a) { - if (err) return done(err); - N.create({ a: a._id, i: 10 }, function(err, b) { - if (err) return done(err); + M.create({name: 'i am an A'}, function(err, a) { + if (err) { + return done(err); + } + N.create({a: a._id, i: 10}, function(err, b) { + if (err) { + return done(err); + } - N.findOneAndUpdate({ _id: b._id }, { $inc: { i: 1 }}) + N.findOneAndUpdate({_id: b._id}, {$inc: {i: 1}}) .populate('a') .exec(function(err, doc) { - if (err) return done(err); + if (err) { + return done(err); + } assert.ok(doc); assert.ok(doc.a); - assert.equal(doc.a.name, 'i am an A'); + assert.equal('i am an A', doc.a.name); db.close(done); }); }); @@ -824,20 +851,20 @@ describe('model: findByIdAndUpdate:', function() { _id: String, flag: { type: Boolean, - "default": false + default: false } }); var Thing = db.model('Thing', thingSchema); var key = 'some-id'; - Thing.findOneAndUpdate({_id: key}, {$set: {flag: false}}, {upsert: true, "new": false}).exec(function(err, thing) { + Thing.findOneAndUpdate({_id: key}, {$set: {flag: false}}, {upsert: true, new: false}).exec(function(err, thing) { assert.ifError(err); - assert.equal(null, thing); - Thing.findOneAndUpdate({_id: key}, {$set: {flag: false}}, {upsert: true, "new": false}).exec(function(err, thing2) { + assert.equal(thing, null); + Thing.findOneAndUpdate({_id: key}, {$set: {flag: false}}, {upsert: true, new: false}).exec(function(err, thing2) { assert.ifError(err); - assert.equal(key, thing2.id); - assert.equal(false, thing2.flag); + assert.equal(thing2.id, key); + assert.equal(thing2.flag, false); db.close(done); }); }); @@ -847,32 +874,40 @@ describe('model: findByIdAndUpdate:', function() { var db = start(); var thingSchema = new Schema({ - name:[String] + name: [String] }); var Thing = db.model('Thing', thingSchema); - Thing.create({name:["Test"]}, function(err, thing) { - if (err) return done(err); - Thing.findOneAndUpdate({ _id: thing._id }, {name:null}, { 'new': true }) - .exec(function(err, doc) { - if (err) return done(err); - assert.ok(doc); - assert.equal(doc.name, null); - db.close(done); - }); + Thing.create({name: ['Test']}, function(err, thing) { + if (err) { + return done(err); + } + Thing.findOneAndUpdate({_id: thing._id}, {name: null}, {new: true}) + .exec(function(err, doc) { + if (err) { + return done(err); + } + assert.ok(doc); + assert.equal(null, doc.name); + db.close(done); + }); }); }); it('honors the overwrite option (gh-1809)', function(done) { var db = start(); - var M = db.model('1809', { name: String, change: Boolean }); - M.create({ name: 'first' }, function(err, doc) { - if (err) return done(err); - M.findByIdAndUpdate(doc._id, { change: true }, { overwrite: true, 'new': true }, function(err, doc) { - if (err) return done(err); + var M = db.model('1809', {name: String, change: Boolean}); + M.create({name: 'first'}, function(err, doc) { + if (err) { + return done(err); + } + M.findByIdAndUpdate(doc._id, {change: true}, {overwrite: true, new: true}, function(err, doc) { + if (err) { + return done(err); + } assert.ok(doc.change); - assert.equal(undefined, doc.name); + assert.equal(doc.name, undefined); db.close(done); }); }); @@ -881,94 +916,105 @@ describe('model: findByIdAndUpdate:', function() { it('can do deep equals on object id after findOneAndUpdate (gh-2070)', function(done) { var db = start(); - var accountSchema = Schema({ + var accountSchema = new Schema({ name: String, contacts: [{ - account: { type: Schema.Types.ObjectId, ref: 'Account'}, + account: {type: Schema.Types.ObjectId, ref: 'Account'}, name: String }] }); var Account = db.model('2070', accountSchema); - var a1 = new Account({ name: 'parent' }); - var a2 = new Account({ name: 'child' }); + var a1 = new Account({name: 'parent'}); + var a2 = new Account({name: 'child'}); a1.save(function(error) { assert.ifError(error); a2.save(function(error, a2) { assert.ifError(error); Account.findOneAndUpdate( - { name: 'parent' }, - { $push: { contacts: { account: a2._id, name: 'child' } } }, - { 'new': true }, - function(error, doc) { - assert.ifError(error); - assert.ok(Utils.deepEqual(doc.contacts[0].account, a2._id)); - assert.ok(_.isEqual(doc.contacts[0].account, a2._id)); - - Account.findOne({ name : 'parent' }, function(error, doc) { + {name: 'parent'}, + {$push: {contacts: {account: a2._id, name: 'child'}}}, + {new: true}, + function(error, doc) { assert.ifError(error); assert.ok(Utils.deepEqual(doc.contacts[0].account, a2._id)); - assert.ok(_.isEqual(doc.contacts[0].account, a2._id)); - db.close(done); + assert.ok(_.isEqualWith(doc.contacts[0].account, a2._id, compareBuffers)); + // Re: commends on https://github.com/mongodb/js-bson/commit/aa0b54597a0af28cce3530d2144af708e4b66bf0 + // Deep equality checks no longer work as expected with node 0.10. + // Please file an issue if this is a problem for you + if (!/^v0.10.\d+$/.test(process.version)) { + assert.ok(_.isEqual(doc.contacts[0].account, a2._id)); + } + + Account.findOne({name: 'parent'}, function(error, doc) { + assert.ifError(error); + assert.ok(Utils.deepEqual(doc.contacts[0].account, a2._id)); + assert.ok(_.isEqualWith(doc.contacts[0].account, a2._id, compareBuffers)); + if (!/^v0.10.\d+$/.test(process.version)) { + assert.ok(_.isEqual(doc.contacts[0].account, a2._id)); + } + db.close(done); + }); }); - }); }); }); + + function compareBuffers(a, b) { + if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) { + return a.toString('hex') === b.toString('hex'); + } + } }); - it('adds __v on upsert (gh-2122)', function(done) { + it('adds __v on upsert (gh-2122) (gh-4505)', function(done) { var db = start(); - var accountSchema = Schema({ + var accountSchema = new Schema({ name: String }); var Account = db.model('2122', accountSchema); Account.findOneAndUpdate( - { name: 'account' }, - { }, - { upsert: true, new: true }, + {name: 'account'}, + {name: 'test'}, + {upsert: true, new: true}, function(error, doc) { assert.ifError(error); - assert.equal(0, doc.__v); - db.close(done); + assert.equal(doc.__v, 0); + Account.update({ name: 'test' }, {}, { upsert: true }, function(error) { + assert.ifError(error); + Account.findOne({ name: 'test' }, function(error, doc) { + assert.ifError(error); + assert.equal(doc.__v, 0); + db.close(done); + }); + }); }); }); it('works with nested schemas and $pull+$or (gh-1932)', function(done) { var db = start(); - var TickSchema = Schema({ name: String }); - var TestSchema = Schema({ a: Number, b: Number, ticks: [TickSchema] }); + var TickSchema = new Schema({name: String}); + var TestSchema = new Schema({a: Number, b: Number, ticks: [TickSchema]}); var TestModel = db.model('gh-1932', TestSchema, 'gh-1932'); - TestModel.create({ a: 1, b: 0, ticks: [{ name: 'eggs' }, { name: 'bacon' }, { name: 'coffee' }] }, function(error) { + TestModel.create({a: 1, b: 0, ticks: [{name: 'eggs'}, {name: 'bacon'}, {name: 'coffee'}]}, function(error) { assert.ifError(error); - TestModel.findOneAndUpdate( - { a: 1 }, - { - $pull: { - ticks: { - $or: [ - { name: 'eggs' }, - { name: 'bacon' } - ] - } - } - }, - function(error) { - assert.ifError(error); - TestModel.findOne({}, function(error, doc) { + TestModel.findOneAndUpdate({a: 1}, {$pull: {ticks: {$or: [{name: 'eggs'}, {name: 'bacon'}]}}}, + function(error) { assert.ifError(error); - assert.equal(1, doc.ticks.length); - assert.equal('coffee', doc.ticks[0].name); - db.close(done); + TestModel.findOne({}, function(error, doc) { + assert.ifError(error); + assert.equal(doc.ticks.length, 1); + assert.equal(doc.ticks[0].name, 'coffee'); + db.close(done); + }); }); - }); }); }); @@ -983,11 +1029,11 @@ describe('model: findByIdAndUpdate:', function() { var Breakfast = db.model('gh-2272', s); Breakfast. - findOneAndUpdate({}, { time: undefined, base: undefined }, {}). - exec(function(error) { - assert.ifError(error); - db.close(done); - }); + findOneAndUpdate({}, {time: undefined, base: undefined}, {}). + exec(function(error) { + assert.ifError(error); + db.close(done); + }); }); it('cast errors for empty objects as object ids (gh-2732)', function(done) { @@ -1000,11 +1046,11 @@ describe('model: findByIdAndUpdate:', function() { var Breakfast = db.model('gh2732', s); Breakfast. - findOneAndUpdate({}, { base: {} }, {}). - exec(function(error) { - assert.ok(error); - db.close(done); - }); + findOneAndUpdate({}, {base: {}}, {}). + exec(function(error) { + assert.ok(error); + db.close(done); + }); }); it('strict mode with objects (gh-2947)', function(done) { @@ -1012,17 +1058,17 @@ describe('model: findByIdAndUpdate:', function() { var s = new Schema({ test: String - }, { strict: true }); + }, {strict: true}); var Breakfast = db.model('gh2947', s); var q = Breakfast.findOneAndUpdate({}, - { notInSchema: { a: 1 }, test: 'abc' }, - { 'new': true, strict: true, upsert: true }); + {notInSchema: {a: 1}, test: 'abc'}, + {new: true, strict: true, upsert: true}); q.lean(); q.exec(function(error, doc) { assert.ok(!doc.notInSchema); - done(); + db.close(done); }); }); @@ -1039,7 +1085,7 @@ describe('model: findByIdAndUpdate:', function() { it('works', function(done) { var s = new Schema({ - topping: { type: String, default: 'bacon' }, + topping: {type: String, default: 'bacon'}, base: String }); @@ -1056,20 +1102,20 @@ describe('model: findByIdAndUpdate:', function() { var Breakfast = db.model('gh-964', s); Breakfast.findOneAndUpdate( - {}, - { base: 'eggs' }, - {}, - function(error) { - assert.ifError(error); - assert.equal(1, preCount); - assert.equal(1, postCount); - done(); - }); + {}, + {base: 'eggs'}, + {}, + function(error) { + assert.ifError(error); + assert.equal(preCount, 1); + assert.equal(postCount, 1); + done(); + }); }); it('works with exec()', function(done) { var s = new Schema({ - topping: { type: String, default: 'bacon' }, + topping: {type: String, default: 'bacon'}, base: String }); @@ -1086,13 +1132,13 @@ describe('model: findByIdAndUpdate:', function() { var Breakfast = db.model('gh-964-2', s); Breakfast. - findOneAndUpdate({}, { base: 'eggs' }, {}). - exec(function(error) { - assert.ifError(error); - assert.equal(1, preCount); - assert.equal(1, postCount); - done(); - }); + findOneAndUpdate({}, {base: 'eggs'}, {}). + exec(function(error) { + assert.ifError(error); + assert.equal(preCount, 1); + assert.equal(postCount, 1); + done(); + }); }); }); @@ -1101,83 +1147,95 @@ describe('model: findByIdAndUpdate:', function() { var db = start(); var s = new Schema({ - topping: { type: String, default: 'bacon' }, + topping: {type: String, default: 'bacon'}, base: String }); var Breakfast = db.model('fam-gh-860-0', s); - var updateOptions = { upsert: true, setDefaultsOnInsert: true, 'new': true }; + var updateOptions = {upsert: true, setDefaultsOnInsert: true, new: true}; Breakfast.findOneAndUpdate( - {}, - { base: 'eggs' }, - updateOptions, - function(error, breakfast) { - assert.ifError(error); - assert.equal('eggs', breakfast.base); - assert.equal('bacon', breakfast.topping); - Breakfast.count({ topping: 'bacon' }, function(error, count) { + {}, + {base: 'eggs'}, + updateOptions, + function(error, breakfast) { assert.ifError(error); - assert.equal(count, 1); - db.close(done); + assert.equal(breakfast.base, 'eggs'); + assert.equal(breakfast.topping, 'bacon'); + Breakfast.count({topping: 'bacon'}, function(error, count) { + assert.ifError(error); + assert.equal(1, count); + db.close(done); + }); }); - }); }); it('doesnt set default on upsert if query sets it', function(done) { var db = start(); var s = new Schema({ - topping: { type: String, default: 'bacon' }, + topping: {type: String, default: 'bacon'}, + numEggs: {type: Number, default: 3}, base: String - }); + }, { versionKey: null }); var Breakfast = db.model('fam-gh-860-1', s); - var updateOptions = { upsert: true, setDefaultsOnInsert: true, 'new': true }; + var updateOptions = {upsert: true, setDefaultsOnInsert: true, new: true}; Breakfast.findOneAndUpdate( - { topping: 'sausage' }, - { base: 'eggs' }, - updateOptions, - function(error, breakfast) { - assert.ifError(error); - assert.equal('eggs', breakfast.base); - assert.equal('sausage', breakfast.topping); - db.close(); - done(); - }); + {topping: 'sausage', numEggs: 4}, + {base: 'eggs'}, + updateOptions, + function(error, breakfast) { + assert.ifError(error); + assert.equal(breakfast.base, 'eggs'); + assert.equal(breakfast.topping, 'sausage'); + assert.equal(breakfast.numEggs, 4); + db.close(); + done(); + }); }); it('properly sets default on upsert if query wont set it', function(done) { var db = start(); var s = new Schema({ - topping: { type: String, default: 'bacon' }, + topping: {type: String, default: 'bacon'}, base: String }); var Breakfast = db.model('fam-gh-860-2', s); - var updateOptions = { upsert: true, setDefaultsOnInsert: true, 'new': true }; + var updateOptions = {upsert: true, setDefaultsOnInsert: true, new: true}; Breakfast.findOneAndUpdate( - { topping: { $ne: 'sausage' } }, - { base: 'eggs' }, - updateOptions, - function(error, breakfast) { - assert.ifError(error); - assert.equal('eggs', breakfast.base); - assert.equal('bacon', breakfast.topping); - Breakfast.count({ topping: 'bacon' }, function(error, count) { + {topping: {$ne: 'sausage'}}, + {base: 'eggs'}, + updateOptions, + function(error, breakfast) { assert.ifError(error); - assert.equal(count, 1); - db.close(done); + assert.equal(breakfast.base, 'eggs'); + assert.equal(breakfast.topping, 'bacon'); + Breakfast.count({topping: 'bacon'}, function(error, count) { + assert.ifError(error); + assert.equal(1, count); + db.close(done); + }); }); - }); }); it('runs validators if theyre set', function(done) { var db = start(); var s = new Schema({ - topping: { type: String, validate: function() { return false; } }, - base: { type: String, validate: function() { return true; } } + topping: { + type: String, + validate: function() { + return false; + } + }, + base: { + type: String, + validate: function() { + return true; + } + } }); var Breakfast = db.model('fam-gh-860-3', s); @@ -1185,153 +1243,150 @@ describe('model: findByIdAndUpdate:', function() { upsert: true, setDefaultsOnInsert: true, runValidators: true, - 'new': true + new: true }; Breakfast.findOneAndUpdate( - {}, - { topping: 'bacon', base: 'eggs' }, - updateOptions, - function(error, breakfast) { - assert.ok(!!error); - assert.ok(!breakfast); - assert.equal(1, Object.keys(error.errors).length); - assert.equal('topping', Object.keys(error.errors)[0]); - assert.equal('Validator failed for path `topping` with value `bacon`', - error.errors['topping'].message); - - assert.ok(!breakfast); - db.close(); - done(); - }); + {}, + {topping: 'bacon', base: 'eggs'}, + updateOptions, + function(error, breakfast) { + assert.ok(!!error); + assert.ok(!breakfast); + assert.equal(Object.keys(error.errors).length, 1); + assert.equal(Object.keys(error.errors)[0], 'topping'); + assert.equal(error.errors.topping.message, 'Validator failed for path `topping` with value `bacon`'); + + assert.ok(!breakfast); + db.close(); + done(); + }); }); it('validators handle $unset and $setOnInsert', function(done) { var db = start(); var s = new Schema({ - steak: { type: String, required: true }, - eggs: { type: String, validate: function() { return false; } } + steak: {type: String, required: true}, + eggs: { + type: String, validate: function() { + return false; + } + } }); var Breakfast = db.model('fam-gh-860-4', s); - var updateOptions = { runValidators: true, 'new': true }; + var updateOptions = {runValidators: true, new: true}; Breakfast.findOneAndUpdate( - {}, - { $unset: { steak: '' }, $setOnInsert: { eggs: 'softboiled' } }, - updateOptions, - function(error, breakfast) { - assert.ok(!!error); - assert.ok(!breakfast); - assert.equal(2, Object.keys(error.errors).length); - assert.ok(Object.keys(error.errors).indexOf('eggs') != -1); - assert.ok(Object.keys(error.errors).indexOf('steak') != -1); - assert.equal('Validator failed for path `eggs` with value `softboiled`', - error.errors['eggs'].message); - assert.equal('Path `steak` is required.', - error.errors['steak'].message); - db.close(); - done(); - }); + {}, + {$unset: {steak: ''}, $setOnInsert: {eggs: 'softboiled'}}, + updateOptions, + function(error, breakfast) { + assert.ok(!!error); + assert.ok(!breakfast); + assert.equal(Object.keys(error.errors).length, 2); + assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1); + assert.ok(Object.keys(error.errors).indexOf('steak') !== -1); + assert.equal(error.errors.eggs.message, 'Validator failed for path `eggs` with value `softboiled`'); + assert.equal(error.errors.steak.message, 'Path `steak` is required.'); + db.close(); + done(); + }); }); it('min/max, enum, and regex built-in validators work', function(done) { var db = start(); var s = new Schema({ - steak: { type: String, enum: ['ribeye', 'sirloin'] }, - eggs: { type: Number, min: 4, max: 6 }, - bacon: { type: String, match: /strips/ } + steak: {type: String, enum: ['ribeye', 'sirloin']}, + eggs: {type: Number, min: 4, max: 6}, + bacon: {type: String, match: /strips/} }); var Breakfast = db.model('fam-gh-860-5', s); - var updateOptions = { runValidators: true, 'new': true }; + var updateOptions = {runValidators: true, new: true}; Breakfast.findOneAndUpdate( - {}, - { $set: { steak: 'ribeye', eggs: 3, bacon: '3 strips' } }, - updateOptions, - function(error) { - assert.ok(!!error); - assert.equal(1, Object.keys(error.errors).length); - assert.equal('eggs', Object.keys(error.errors)[0]); - assert.equal('Path `eggs` (3) is less than minimum allowed value (4).', - error.errors['eggs'].message); - - Breakfast.findOneAndUpdate( - {}, - { $set: { steak: 'tofu', eggs: 5, bacon: '3 strips' } }, - updateOptions, - function(error) { - assert.ok(!!error); - assert.equal(1, Object.keys(error.errors).length); - assert.equal('steak', Object.keys(error.errors)[0]); - assert.equal('`tofu` is not a valid enum value for path `steak`.', - error.errors['steak']); - + {}, + {$set: {steak: 'ribeye', eggs: 3, bacon: '3 strips'}}, + updateOptions, + function(error) { + assert.ok(!!error); + assert.equal(Object.keys(error.errors).length, 1); + assert.equal(Object.keys(error.errors)[0], 'eggs'); + assert.equal(error.errors.eggs.message, 'Path `eggs` (3) is less than minimum allowed value (4).'); - Breakfast.findOneAndUpdate( + Breakfast.findOneAndUpdate( {}, - { $set: { steak: 'sirloin', eggs: 6, bacon: 'none' } }, + {$set: {steak: 'tofu', eggs: 5, bacon: '3 strips'}}, updateOptions, function(error) { assert.ok(!!error); - assert.equal(1, Object.keys(error.errors).length); - assert.equal('bacon', Object.keys(error.errors)[0]); - assert.equal('Path `bacon` is invalid (none).', - error.errors['bacon'].message); - - db.close(); - done(); + assert.equal(Object.keys(error.errors).length, 1); + assert.equal(Object.keys(error.errors)[0], 'steak'); + assert.equal(error.errors.steak, '`tofu` is not a valid enum value for path `steak`.'); + + Breakfast.findOneAndUpdate( + {}, + {$set: {steak: 'sirloin', eggs: 6, bacon: 'none'}}, + updateOptions, + function(error) { + assert.ok(!!error); + assert.equal(Object.keys(error.errors).length, 1); + assert.equal(Object.keys(error.errors)[0], 'bacon'); + assert.equal(error.errors.bacon.message, 'Path `bacon` is invalid (none).'); + + db.close(); + done(); + }); }); - }); - }); + }); }); it('multiple validation errors', function(done) { var db = start(); var s = new Schema({ - steak: { type: String, enum: ['ribeye', 'sirloin'] }, - eggs: { type: Number, min: 4, max: 6 }, - bacon: { type: String, match: /strips/ } + steak: {type: String, enum: ['ribeye', 'sirloin']}, + eggs: {type: Number, min: 4, max: 6}, + bacon: {type: String, match: /strips/} }); var Breakfast = db.model('fam-gh-860-6', s); - var updateOptions = { runValidators: true, 'new': true }; + var updateOptions = {runValidators: true, new: true}; Breakfast.findOneAndUpdate( - {}, - { $set: { steak: 'tofu', eggs: 2, bacon: '3 strips' } }, - updateOptions, - function(error, breakfast) { - assert.ok(!!error); - assert.equal(2, Object.keys(error.errors).length); - assert.ok(Object.keys(error.errors).indexOf('steak') !== -1); - assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1); - assert.ok(!breakfast); - db.close(); - done(); - }); + {}, + {$set: {steak: 'tofu', eggs: 2, bacon: '3 strips'}}, + updateOptions, + function(error, breakfast) { + assert.ok(!!error); + assert.equal(Object.keys(error.errors).length, 2); + assert.ok(Object.keys(error.errors).indexOf('steak') !== -1); + assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1); + assert.ok(!breakfast); + db.close(); + done(); + }); }); it('validators ignore $inc', function(done) { var db = start(); var s = new Schema({ - steak: { type: String, required: true }, - eggs: { type: Number, min: 4 } + steak: {type: String, required: true}, + eggs: {type: Number, min: 4} }); var Breakfast = db.model('fam-gh-860-7', s); - var updateOptions = { runValidators: true, upsert: true, 'new': true }; + var updateOptions = {runValidators: true, upsert: true, new: true}; Breakfast.findOneAndUpdate( - {}, - { $inc: { eggs: 1 } }, - updateOptions, - function(error, breakfast) { - assert.ifError(error); - assert.ok(!!breakfast); - assert.equal(1, breakfast.eggs); - db.close(done); - }); + {}, + {$inc: {eggs: 1}}, + updateOptions, + function(error, breakfast) { + assert.ifError(error); + assert.ok(!!breakfast); + assert.equal(breakfast.eggs, 1); + db.close(done); + }); }); it('should work with arrays (gh-3035)', function(done) { @@ -1348,19 +1403,13 @@ describe('model: findByIdAndUpdate:', function() { }); var TestModel = db.model('gh3035', testSchema); - TestModel.create({ id: '1' }, function(error) { + TestModel.create({id: '1'}, function(error) { assert.ifError(error); - TestModel.findOneAndUpdate( - { id: '1' }, - { $set: { name: 'Joe' } }, - { - upsert: true, - setDefaultsOnInsert: true - }, - function(error) { - assert.ifError(error); - done(); - }); + TestModel.findOneAndUpdate({id: '1'}, {$set: {name: 'Joe'}}, {upsert: true, setDefaultsOnInsert: true}, + function(error) { + assert.ifError(error); + db.close(done); + }); }); }); @@ -1368,24 +1417,18 @@ describe('model: findByIdAndUpdate:', function() { var db = start(); var testSchema = new mongoose.Schema({ - id: String, - blob: ObjectId, - status: String + id: String, + blob: ObjectId, + status: String }); var TestModel = db.model('gh3135', testSchema); - TestModel.create({ blob: null, status: 'active' }, function(error) { + TestModel.create({blob: null, status: 'active'}, function(error) { assert.ifError(error); - TestModel.findOneAndUpdate( - { id: '1', blob: null }, - { $set: { status: 'inactive' }}, - { - upsert: true, - setDefaultsOnInsert: true - }, + TestModel.findOneAndUpdate({id: '1', blob: null}, {$set: {status: 'inactive'}}, {upsert: true, setDefaultsOnInsert: true}, function(error) { assert.ifError(error); - done(); + db.close(done); }); }); }); @@ -1406,19 +1449,13 @@ describe('model: findByIdAndUpdate:', function() { }); var TestModel = db.model('gh3034', testSchema); - TestModel.create({ id: '1' }, function(error) { + TestModel.create({id: '1'}, function(error) { assert.ifError(error); - TestModel.findOneAndUpdate( - { id: '1' }, - { $set: { name: 'Joe' } }, - { - upsert: true, - setDefaultsOnInsert: true - }, - function(error) { - assert.ifError(error); - db.close(done); - }); + TestModel.findOneAndUpdate({id: '1'}, {$set: {name: 'Joe'}}, {upsert: true, setDefaultsOnInsert: true}, + function(error) { + assert.ifError(error); + db.close(done); + }); }); }); @@ -1434,56 +1471,28 @@ describe('model: findByIdAndUpdate:', function() { }); var TestModel = db.model('gh3107', testSchema); - TestModel.findOneAndUpdate( - { id: '1' }, - { $setOnInsert: { a: [{ foo: 'bar' }], b: [2] } }, - { - upsert: true, - 'new': true, - setDefaultsOnInsert: true - }, - function(error, doc) { - assert.ifError(error); - assert.equal(doc.a.length, 1); - assert.equal(doc.a[0].foo, 'bar'); - assert.equal(doc.b.length, 1); - assert.equal(doc.b[0], 2); - db.close(done); - }); + var update = { $setOnInsert: { a: [{foo: 'bar'}], b: [2] } }; + var opts = {upsert: true, new: true, setDefaultsOnInsert: true}; + TestModel + .findOneAndUpdate({name: 'abc'}, update, opts, + function(error, doc) { + assert.ifError(error); + assert.equal(doc.a.length, 1); + assert.equal(doc.a[0].foo, 'bar'); + assert.equal(doc.b.length, 1); + assert.equal(doc.b[0], 2); + db.close(done); + }); }); - it('passes raw result as 3rd param (gh-3173)', function(done) { - var db = start(); - - var testSchema = new mongoose.Schema({ - test: String - }); - - var TestModel = db.model('gh3173', testSchema); - - TestModel.findOneAndUpdate( - {}, - { $set: { test: 'abc' } }, - { - upsert: true, - 'new': true, - passRawResult: true - }). - exec(function(error, doc, res) { - assert.ifError(error); - assert.ok(res); - assert.ok(res.ok); - done(); - }); - }); it('handles nested cast errors (gh-3468)', function(done) { var db = start(); var recordSchema = new mongoose.Schema({ kind: String, - amount: Number, + amount: Number }, { - _id: false, + _id: false }); var shiftSchema = new mongoose.Schema({ @@ -1495,19 +1504,633 @@ describe('model: findByIdAndUpdate:', function() { Shift.create({ userId: 'tom', - records: [], - }, function(error, shift) { + records: [] + }, function(error) { assert.ifError(error); Shift.findOneAndUpdate({userId: 'tom'}, { - records: [{kind: 'kind1', amount: NaN}], + records: [{kind: 'kind1', amount: NaN}] }, { - 'new': true, - }, function(error, shift) { + new: true + }, function(error) { assert.ok(error); assert.ok(error instanceof CastError); db.close(done); }); }); }); + + it('cast errors with nested schemas (gh-3580)', function(done) { + var db = start(); + + var nested = new Schema({num: Number}); + var s = new Schema({nested: nested}); + + var MyModel = db.model('gh3580', s); + + var update = {nested: {num: 'Not a Number'}}; + MyModel.findOneAndUpdate({}, update, function(error) { + assert.ok(error); + db.close(done); + }); + }); + + it('pull with nested schemas (gh-3616)', function(done) { + var db = start(); + + var nested = new Schema({arr: [{num: Number}]}); + var s = new Schema({nested: nested}); + + var MyModel = db.model('gh3616', s); + + MyModel.create({nested: {arr: [{num: 5}]}}, function(error) { + assert.ifError(error); + var update = {$pull: {'nested.arr': {num: 5}}}; + var options = {new: true}; + MyModel.findOneAndUpdate({}, update, options, function(error, doc) { + assert.ifError(error); + assert.equal(doc.nested.arr.length, 0); + db.close(done); + }); + }); + }); + + it('setting nested schema (gh-3889)', function(done) { + var db = start(); + var nested = new Schema({ test: String }); + var s = new Schema({ nested: nested }); + var MyModel = db.model('gh3889', s); + MyModel.findOneAndUpdate( + {}, + { $set: { nested: { test: 'abc' } } }, + function(error) { + assert.ifError(error); + db.close(done); + }); + }); + }); + + describe('bug fixes', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + it('passes raw result as 3rd param (gh-3173)', function(done) { + var testSchema = new mongoose.Schema({ + test: String + }); + + var TestModel = db.model('gh3173', testSchema); + var options = { upsert: true, new: true, passRawResult: true }; + var update = { $set: { test: 'abc' } }; + + TestModel.findOneAndUpdate({}, update, options). + exec(function(error, doc, res) { + assert.ifError(error); + assert.ok(res); + assert.ok(res.ok); + done(); + }); + }); + + it('passes raw result if rawResult specified (gh-4925)', function(done) { + var testSchema = new mongoose.Schema({ + test: String + }); + + var TestModel = db.model('gh4925', testSchema); + var options = { upsert: true, new: true, rawResult: true }; + var update = { $set: { test: 'abc' } }; + + TestModel.findOneAndUpdate({}, update, options). + exec(function(error, res) { + assert.ifError(error); + assert.ok(res); + assert.ok(res.ok); + assert.equal(res.value.test, 'abc'); + assert.ok(res.value.id); + assert.equal(res.lastErrorObject.n, 1); + done(); + }); + }); + + it('raw result as 3rd param w/ no result (gh-4023)', function(done) { + var testSchema = new mongoose.Schema({ + test: String + }); + + var TestModel = db.model('gh4023', testSchema); + var options = { upsert: true, new: false, passRawResult: true }; + var update = { $set: { test: 'abc' } }; + + TestModel.findOneAndUpdate({}, update, options). + exec(function(error, doc, res) { + assert.ifError(error); + assert.ok(res); + assert.ok(res.ok); + + done(); + }); + }); + + it('raw result as 3rd param w/ lean (gh-4761)', function(done) { + var testSchema = new mongoose.Schema({ + test: String + }); + + var TestModel = db.model('gh4761', testSchema); + var options = { upsert: true, new: true, passRawResult: true }; + var update = { $set: { test: 'abc' } }; + + TestModel.findOneAndUpdate({}, update, options).lean(). + exec(function(error, doc, res) { + assert.ifError(error); + assert.ok(res); + assert.ok(res.ok); + + done(); + }); + }); + + it('handles setting single embedded docs to null (gh-4281)', function(done) { + var foodSchema = new mongoose.Schema({ + name: { type: String, default: 'Bacon' } + }); + + var breakfastSchema = new mongoose.Schema({ + main: foodSchema, + for: String + }); + + var TestModel = db.model('gh4281', breakfastSchema); + var options = { upsert: true, new: true }; + var update = { $set: { main: null, for: 'Val' } }; + + TestModel.findOneAndUpdate({}, update, options). + exec(function(error, doc) { + assert.ifError(error); + assert.ok(doc); + assert.equal(doc.main, null); + + done(); + }); + }); + + it('custom validator on mixed field (gh-4305)', function(done) { + var called = 0; + + var boardSchema = new Schema({ + name: { + type: String, + required: true + }, + structure: { + type: Schema.Types.Mixed, + required: true, + validate: { + validator: function() { + ++called; + return true; + }, + message: 'The structure of the board is invalid' + } + } + }); + var Board = db.model('gh4305', boardSchema); + + var update = { + structure: [ + { + capacity: 0, + size: 0, + category: 0, + isColumn: true, + title: 'Backlog' + } + ] + }; + var opts = { + 'new': true, + upsert: false, + passRawResult: false, + overwrite: false, + runValidators: true, + setDefaultsOnInsert: true + }; + Board. + findOneAndUpdate({}, update, opts). + exec(function(error) { + assert.ifError(error); + assert.equal(called, 1); + done(); + }); + }); + + it('single nested doc cast errors (gh-3602)', function(done) { + var AddressSchema = new Schema({ + street: { + type: Number + } + }); + + var PersonSchema = new Schema({ + addresses: [AddressSchema] + }); + + var Person = db.model('gh3602', PersonSchema); + + var update = { $push: { addresses: { street: 'not a num' } } }; + Person.findOneAndUpdate({}, update, function(error) { + assert.ok(error.message.indexOf('street') !== -1); + assert.equal(error.reason.message, + 'Cast to Number failed for value "not a num" at path "street"'); + done(); + }); + }); + + it('projection option as alias for fields (gh-4315)', function(done) { + var TestSchema = new Schema({ + test1: String, + test2: String + }); + var Test = db.model('gh4315', TestSchema); + var update = { $set: { test1: 'a', test2: 'b' } }; + var options = { projection: { test2: 0 }, new: true, upsert: true }; + Test.findOneAndUpdate({}, update, options, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.test2); + assert.equal(doc.test1, 'a'); + done(); + }); + }); + + it('handles upserting a non-existing field (gh-4757)', function(done) { + var modelSchema = new Schema({ field: Number }, { strict: 'throw' }); + + var Model = db.model('gh4757', modelSchema); + Model.findOneAndUpdate({ nonexistingField: 1 }, { field: 2 }, { + upsert: true, + setDefaultsOnInsert: true, + new: true + }).exec(function(error) { + assert.ok(error); + assert.equal(error.name, 'StrictModeError'); + done(); + }); + }); + + it('strictQuery option (gh-4136)', function(done) { + var modelSchema = new Schema({ field: Number }, { strictQuery: 'throw' }); + + var Model = db.model('gh4136', modelSchema); + Model.find({ nonexistingField: 1 }).exec(function(error) { + assert.ok(error); + assert.ok(error.message.indexOf('strictQuery') !== -1, error.message); + done(); + }); + }); + + it('strict option (gh-5108)', function(done) { + var modelSchema = new Schema({ field: Number }, { strict: 'throw' }); + + var Model = db.model('gh5108', modelSchema); + Model.findOneAndUpdate({}, { field: 2, otherField: 3 }, { + upsert: true, + strict: false, + new: true + }).exec(function(error, doc) { + assert.ifError(error); + assert.equal(doc.field, 2); + assert.equal(doc.get('otherField'), 3); + done(); + }); + }); + + it('honors retainKeyOrder (gh-6484)', function() { + var modelSchema = new Schema({ + nested: { field1: Number, field2: Number } + }, { retainKeyOrder: true }); + + var Model = db.model('gh6484', modelSchema); + var opts = { upsert: true, new: true }; + return Model.findOneAndUpdate({}, { nested: { field1: 1, field2: 2 } }, opts).exec(). + then(function() { + return Model.collection.findOne(); + }). + then(function(doc) { + // Make sure order is correct + assert.deepEqual(Object.keys(doc.nested), ['field1', 'field2']); + }); + }); + + it('should not apply schema transforms (gh-4574)', function(done) { + var options = { + toObject: { + transform: function() { + assert.ok(false, 'should not call transform'); + } + } + }; + + var SubdocSchema = new Schema({ test: String }, options); + + var CollectionSchema = new Schema({ + field1: { type: String }, + field2 : { + arrayField: [SubdocSchema] + } + }, options); + + var Collection = db.model('test', CollectionSchema); + + Collection.create({ field2: { arrayField: [] } }). + then(function(doc) { + return Collection.findByIdAndUpdate(doc._id, { + $push: { 'field2.arrayField': { test: 'test' } } + }, { new: true }); + }). + then(function() { + done(); + }); + }); + + it('overwrite doc with update validators (gh-3556)', function(done) { + var testSchema = new Schema({ + name: { + type: String, + required: true + }, + otherName: String + }); + var Test = db.model('gh3556', testSchema); + + var opts = { overwrite: true, runValidators: true }; + Test.findOneAndUpdate({}, { otherName: 'test' }, opts, function(error) { + assert.ok(error); + assert.ok(error.errors['name']); + Test.findOneAndUpdate({}, { $set: { otherName: 'test' } }, opts, function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('update using $ (gh-5628)', function(done) { + var schema = new mongoose.Schema({ + elems: [String] + }); + + var Model = db.model('gh5628', schema); + Model.create({ elems: ['a', 'b'] }, function(error, doc) { + assert.ifError(error); + var query = { _id: doc._id, elems: 'a' }; + var update = { $set: { 'elems.$': 'c' } }; + Model.findOneAndUpdate(query, update, { new: true }, function(error) { + assert.ifError(error); + Model.collection.findOne({ _id: doc._id }, function(error, doc) { + assert.ifError(error); + assert.deepEqual(doc.elems, ['c', 'b']); + done(); + }); + }); + }); + }); + + it('projection with $elemMatch (gh-5661)', function(done) { + var schema = new mongoose.Schema({ + name: { type: String, default: 'test' }, + arr: [{ tag: String }] + }); + + var Model = db.model('gh5661', schema); + var doc = { arr: [{ tag: 't1' }, { tag: 't2' }] }; + Model.create(doc, function(error) { + assert.ifError(error); + var query = {}; + var update = { $set: { name: 'test2' } }; + var opts = { + new: true, + fields: { arr: { $elemMatch: { tag: 't1' } } } + }; + Model.findOneAndUpdate(query, update, opts, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.name); + assert.equal(doc.arr.length, 1); + assert.equal(doc.arr[0].tag, 't1'); + done(); + }); + }); + }); + + it('multi cast error (gh-5609)', function(done) { + var schema = new mongoose.Schema({ + num1: Number, + num2: Number + }); + + var Model = db.model('gh5609', schema); + + var opts = { multipleCastError: true }; + Model.findOneAndUpdate({}, { num1: 'fail', num2: 'fail' }, opts, function(error) { + assert.ok(error); + assert.equal(error.name, 'ValidationError'); + assert.ok(error.errors['num1']); + assert.equal(error.errors['num1'].name, 'CastError'); + assert.ok(error.errors['num2']); + assert.equal(error.errors['num2'].name, 'CastError'); + done(); + }); + }); + + it('update validators with pushing null (gh-5710)', function(done) { + var schema = new mongoose.Schema({ + arr: [String] + }); + + var Model = db.model('gh5710', schema); + + var update = { $addToSet: { arr: null } }; + var options = { runValidators: true }; + Model.findOneAndUpdate({}, update, options, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('avoids edge case with middleware cloning buffers (gh-5702)', function(done) { + if (parseInt(process.version.substr(1).split('.')[0], 10) < 4) { + // Don't run on node 0.x because of `const` issues + this.skip(); + } + + var uuidParse = require('uuid-parse'); + + function toUUID(string) { + if (!string) { + return null; + } + if (Buffer.isBuffer(string) || Buffer.isBuffer(string.buffer)) { + return string; + } + var buffer = uuidParse.parse(string); + return new mongoose.Types.Buffer(buffer).toObject(0x04); + } + + function fromUUID(buffer) { + if (!buffer || buffer.length !== 16) { + return null; + } + return uuidParse.unparse(buffer); + } + + var UserSchema = new mongoose.Schema({ + name: String, + lastUpdate: {type: Date}, + friends: [{ + _id: false, + status: {type: String, required: true}, + id: { + type: mongoose.Schema.Types.Buffer, + get: fromUUID, + set: toUUID + } + }] + }, { collection: 'users', runSettersOnQuery: true }); + + UserSchema.pre('findOneAndUpdate', function() { + this.update({},{ $set: {lastUpdate: new Date()} }); + }); + + var User = db.model('gh5702', UserSchema); + + var friendId = uuid.v4(); + var user = { + name: 'Sean', + friends: [{status: 'New', id: friendId}] + }; + + User.create(user, function(error, user) { + assert.ifError(error); + + var q = { _id: user._id, 'friends.id': friendId }; + var upd = {'friends.$.status': 'Active'}; + User.findOneAndUpdate(q, upd, {new: true}).lean().exec(function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('setting subtype when saving (gh-5551)', function(done) { + if (parseInt(process.version.substr(1).split('.')[0], 10) < 4) { + // Don't run on node 0.x because of `const` issues + this.skip(); + } + + var uuidParse = require('uuid-parse'); + function toUUID(string) { + if (!string) { + return null; + } + if (Buffer.isBuffer(string) || Buffer.isBuffer(string.buffer)) { + return string; + } + var buffer = uuidParse.parse(string); + return new mongoose.Types.Buffer(buffer).toObject(0x04); + } + + var UserSchema = new mongoose.Schema({ + name: String, + foo: { + type: mongoose.Schema.Types.Buffer, + set: toUUID + } + }); + + var User = db.model('gh5551', UserSchema); + + var user = { name: 'upsert', foo: uuid.v4() }; + var opts = { + upsert: true, + setDefaultsOnInsert: true, + runSettersOnQuery: true, + new: true + }; + User.findOneAndUpdate({}, user, opts).exec(function(error, doc) { + assert.ifError(error); + User.collection.findOne({ _id: doc._id }, function(error, doc) { + assert.ifError(error); + assert.equal(doc.foo.sub_type, 4); + done(); + }); + }); + }); + + it('properly handles casting nested objects in update (gh-4724)', function(done) { + var locationSchema = new Schema({ + _id: false, + location: { + type: { type: String, default: 'Point' }, + coordinates: [Number] + } + }); + + var testSchema = new Schema({ + locations: [locationSchema] + }); + + var T = db.model('gh4724', testSchema); + + var t = new T({ + locations: [{ + location: { type: 'Point', coordinates: [-122, 44] } + }] + }); + + t.save(). + then(function(t) { + return T.findByIdAndUpdate(t._id, { + $set: { + 'locations.0': { + location: { type: 'Point', coordinates: [-123, 45] } + } + } + }, { new: true }); + }). + then(function(res) { + assert.equal(res.locations[0].location.coordinates[0], -123); + done(); + }). + catch(done); + }); + + it('doesnt do double validation on document arrays during updates (gh-4440)', function(done) { + var A = new Schema({str: String}); + var B = new Schema({a: [A]}); + var validateCalls = 0; + B.path('a').validate(function(val, next) { + ++validateCalls; + assert(Array.isArray(val)); + next(); + }); + + B = db.model('b', B); + + B.findOneAndUpdate( + {foo: 'bar'}, + {$set: {a: [{str: 'asdf'}]}}, + {runValidators: true}, + function(err) { + assert.ifError(err); + assert.equal(validateCalls, 1); // Assertion error: 1 == 2 + db.close(done); + } + ); + }); }); }); diff --git a/test/model.geonear.test.js b/test/model.geonear.test.js index 8ddc2066b02..ab9497d209c 100644 --- a/test/model.geonear.test.js +++ b/test/model.geonear.test.js @@ -1,29 +1,20 @@ +'use strict'; -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema; +var start = require('./common'); +var assert = require('power-assert'); +var mongoose = start.mongoose; +var Schema = mongoose.Schema; /** * Setup */ -var schema = new Schema({ - coordinates : { type: [Number], index: '2dsphere' }, - type: String -}); - -function getModel(db) { - return db.model('GeoNear', schema, 'geonear' + random()); -} - var testLocations = { - MONGODB_NYC_OFFICE : [-73.987732, 40.757471] - , BRYANT_PART_NY : [-73.983677, 40.753628] - , EAST_HARLEM_SHOP : [-73.93831, 40.794963] - , CENTRAL_PARK_ZOO : [-73.972299, 40.767732] - , PORT_AUTHORITY_STATION : [-73.990147, 40.757253] + MONGODB_NYC_OFFICE: [-73.987732, 40.757471], + BRYANT_PART_NY: [-73.983677, 40.753628], + EAST_HARLEM_SHOP: [-73.93831, 40.794963], + CENTRAL_PARK_ZOO: [-73.972299, 40.767732], + PORT_AUTHORITY_STATION: [-73.990147, 40.757253] }; // convert meters to radians for use as legacy coordinates @@ -32,35 +23,67 @@ function metersToRadians(m) { } describe('model', function() { + var schema = new Schema({ + coordinates: {type: [Number]}, + type: String, + priority: Number + }); + schema.index({ coordinates: '2dsphere' }, { background: false }); + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + var count = 0; + function getModel(db) { + ++count; + return db.model('GeoNear' + count, schema, 'geonear' + count); + } + var mongo24_or_greater = false; before(function(done) { start.mongodVersion(function(err, version) { if (err) throw err; - mongo24_or_greater = 2 < version[0] || (2 == version[0] && 4 <= version[1]); + mongo24_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 4); if (!mongo24_or_greater) console.log('not testing mongodb 2.4 features'); done(); }); }); + describe('geoNear', function() { + beforeEach(function() { + if (!mongo24_or_greater) { + this.skip(); + } + }); it('works with legacy coordinate points', function(done) { - if (!mongo24_or_greater) return done(); - var db = start(); var Geo = getModel(db); assert.ok(Geo.geoNear instanceof Function); - Geo.on('index', function(err) { - assert.ifError(err); - + Geo.init().then(function() { var geos = []; - geos[0] = new Geo({ coordinates : testLocations.MONGODB_NYC_OFFICE - , type : "Point"}); - geos[1] = new Geo({ coordinates : testLocations.BRYANT_PARK_NY - , type : "Point"}); - geos[2] = new Geo({ coordinates : testLocations.EAST_HARLEM_SHOP - , type : "Point"}); - geos[3] = new Geo({ coordinates : testLocations.CENTRAL_PARK_ZOO - , type : "Point"}); + geos[0] = new Geo({ + coordinates: testLocations.MONGODB_NYC_OFFICE, + type: 'Point' + }); + geos[1] = new Geo({ + coordinates: testLocations.BRYANT_PARK_NY, + type: 'Point' + }); + geos[2] = new Geo({ + coordinates: testLocations.EAST_HARLEM_SHOP, + type: 'Point' + }); + geos[3] = new Geo({ + coordinates: testLocations.CENTRAL_PARK_ZOO, + type: 'Point' + }); var count = geos.length; for (var i = 0; i < geos.length; i++) { @@ -72,7 +95,7 @@ describe('model', function() { function next() { // using legacy coordinates -- maxDistance units in radians - var options = { spherical: true, maxDistance: metersToRadians(300) }; + var options = {spherical: true, maxDistance: metersToRadians(300)}; Geo.geoNear(testLocations.PORT_AUTHORITY_STATION, options).then(function(results) { assert.equal(1, results.length); @@ -82,30 +105,34 @@ describe('model', function() { assert.equal(results[0].obj.coordinates[1], testLocations.MONGODB_NYC_OFFICE[1]); assert.equal(results[0].obj.id, geos[0].id); assert.ok(results[0].obj instanceof Geo); - db.close(done); + done(); }); } }); }); it('works with GeoJSON coordinate points', function(done) { - if (!mongo24_or_greater) return done(); - var db = start(); var Geo = getModel(db); assert.ok(Geo.geoNear instanceof Function); - Geo.on('index', function(err) { - assert.ifError(err); - + Geo.init().then(function() { var geos = []; - geos[0] = new Geo({ coordinates : testLocations.MONGODB_NYC_OFFICE - , type : "Point"}); - geos[1] = new Geo({ coordinates : testLocations.BRANT_PARK_NY - , type : "Point"}); - geos[2] = new Geo({ coordinates : testLocations.EAST_HARLEM_SHOP - , type : "Point"}); - geos[3] = new Geo({ coordinates : testLocations.CENTRAL_PARK_ZOO - , type : "Point"}); + geos[0] = new Geo({ + coordinates: testLocations.MONGODB_NYC_OFFICE, + type: 'Point' + }); + geos[1] = new Geo({ + coordinates: testLocations.BRANT_PARK_NY, + type: 'Point' + }); + geos[2] = new Geo({ + coordinates: testLocations.EAST_HARLEM_SHOP, + type: 'Point' + }); + geos[3] = new Geo({ + coordinates: testLocations.CENTRAL_PARK_ZOO, + type: 'Point' + }); var count = geos.length; for (var i = 0; i < geos.length; i++) { @@ -115,11 +142,11 @@ describe('model', function() { } function next() { - var pnt = { type : "Point", coordinates : testLocations.PORT_AUTHORITY_STATION }; - Geo.geoNear(pnt, { spherical : true, maxDistance : 300 }, function(err, results) { + var pnt = {type: 'Point', coordinates: testLocations.PORT_AUTHORITY_STATION}; + Geo.geoNear(pnt, {spherical: true, maxDistance: 300}, function(err, results) { assert.ifError(err); - assert.equal(1, results.length); + assert.equal(results.length, 1); assert.equal(results[0].obj.type, 'Point'); assert.equal(results[0].obj.coordinates.length, 2); @@ -127,30 +154,34 @@ describe('model', function() { assert.equal(results[0].obj.coordinates[1], testLocations.MONGODB_NYC_OFFICE[1]); assert.equal(results[0].obj.id, geos[0].id); assert.ok(results[0].obj instanceof Geo); - db.close(done); + done(); }); } - }); + }).catch(done); }); it('works with lean', function(done) { - if (!mongo24_or_greater) return done(); - var db = start(); var Geo = getModel(db); assert.ok(Geo.geoNear instanceof Function); - Geo.on('index', function(err) { - assert.ifError(err); - + Geo.init().then(function() { var geos = []; - geos[0] = new Geo({ coordinates : testLocations.MONGODB_NYC_OFFICE - , type : "Point"}); - geos[1] = new Geo({ coordinates : testLocations.BRANT_PARK_NY - , type : "Point"}); - geos[2] = new Geo({ coordinates : testLocations.EAST_HARLEM_SHOP - , type : "Point"}); - geos[3] = new Geo({ coordinates : testLocations.CENTRAL_PARK_ZOO - , type : "Point"}); + geos[0] = new Geo({ + coordinates: testLocations.MONGODB_NYC_OFFICE, + type: 'Point' + }); + geos[1] = new Geo({ + coordinates: testLocations.BRANT_PARK_NY, + type: 'Point' + }); + geos[2] = new Geo({ + coordinates: testLocations.EAST_HARLEM_SHOP, + type: 'Point' + }); + geos[3] = new Geo({ + coordinates: testLocations.CENTRAL_PARK_ZOO, + type: 'Point' + }); var count = geos.length; for (var i = 0; i < geos.length; i++) { @@ -160,11 +191,11 @@ describe('model', function() { } function next() { - var pnt = { type : "Point", coordinates : testLocations.PORT_AUTHORITY_STATION }; - Geo.geoNear(pnt, { spherical : true, maxDistance : 300, lean : true }, function(err, results) { + var pnt = {type: 'Point', coordinates: testLocations.PORT_AUTHORITY_STATION}; + Geo.geoNear(pnt, {spherical: true, maxDistance: 300, lean: true}, function(err, results) { assert.ifError(err); - assert.equal(1, results.length); + assert.equal(results.length, 1); assert.equal(results[0].obj.type, 'Point'); assert.equal(results[0].obj.coordinates.length, 2); @@ -172,40 +203,35 @@ describe('model', function() { assert.equal(results[0].obj.coordinates[1], testLocations.MONGODB_NYC_OFFICE[1]); assert.equal(results[0].obj._id, geos[0].id); assert.ok(!(results[0].obj instanceof Geo)); - db.close(done); + done(); }); } }); }); it('throws the correct error messages', function(done) { - if (!mongo24_or_greater) return done(); - - var db = start(); var Geo = getModel(db); - Geo.on('index', function(err) { - assert.ifError(err); - - var g = new Geo({ coordinates : [10,10], type : "place"}); + Geo.init().then(function() { + var g = new Geo({coordinates: [10, 10], type: 'place'}); g.save(function() { - Geo.geoNear("1,2", {}, function(e) { + Geo.geoNear('1,2', {}, function(e) { assert.ok(e); - assert.equal(e.message, "Must pass either a legacy coordinate array or GeoJSON Point to geoNear"); + assert.equal(e.message, 'Must pass either a legacy coordinate array or GeoJSON Point to geoNear'); Geo.geoNear([1], {}, function(e) { assert.ok(e); - assert.equal(e.message, "If using legacy coordinates, must be an array of size 2 for geoNear"); + assert.equal(e.message, 'If using legacy coordinates, must be an array of size 2 for geoNear'); - Geo.geoNear({ type : "Square" }, {}, function(e) { + Geo.geoNear({type: 'Square'}, {}, function(e) { assert.ok(e); - assert.equal(e.message, "Must pass either a legacy coordinate array or GeoJSON Point to geoNear"); + assert.equal(e.message, 'Must pass either a legacy coordinate array or GeoJSON Point to geoNear'); - Geo.geoNear({ type : "Point", coordinates : "1,2" }, {}, function(e) { + Geo.geoNear({type: 'Point', coordinates: '1,2'}, {}, function(e) { assert.ok(e); - assert.equal(e.message, "Must pass either a legacy coordinate array or GeoJSON Point to geoNear"); + assert.equal(e.message, 'Must pass either a legacy coordinate array or GeoJSON Point to geoNear'); - db.close(done); + done(); }); }); }); @@ -213,73 +239,85 @@ describe('model', function() { }); }); }); + it('returns a promise (gh-1614)', function(done) { - if (!mongo24_or_greater) return done(); - var db = start(); var Geo = getModel(db); - var pnt = { type : "Point", coordinates : testLocations.PORT_AUTHORITY_STATION }; + var pnt = {type: 'Point', coordinates: testLocations.PORT_AUTHORITY_STATION}; // using GeoJSON point - var prom = Geo.geoNear(pnt, { spherical : true, maxDistance : 300 }, function() {}); + var prom = Geo.geoNear(pnt, {spherical: true, maxDistance: 300}, function() {}); assert.ok(prom instanceof mongoose.Promise); - db.close(); done(); }); it('allows not passing a callback (gh-1614)', function(done) { - if (!mongo24_or_greater) return done(); - var db = start(); var Geo = getModel(db); - Geo.on('index', function(err) { - assert.ifError(err); - var g = new Geo({ coordinates : testLocations.MONGODB_NYC_OFFICE, type : "Point"}); + Geo.init().then(function() { + var g = new Geo({coordinates: testLocations.MONGODB_NYC_OFFICE, type: 'Point'}); g.save(function(err) { assert.ifError(err); - var pnt = { type : "Point", coordinates : testLocations.PORT_AUTHORITY_STATION }; + var pnt = {type: 'Point', coordinates: testLocations.PORT_AUTHORITY_STATION}; var promise; assert.doesNotThrow(function() { - promise = Geo.geoNear(pnt, { spherical : true, maxDistance : 300 }); + promise = Geo.geoNear(pnt, {spherical: true, maxDistance: 300}); }); function validate(ret, stat) { - assert.equal(1, ret.length); + assert.equal(ret.length, 1); assert.equal(ret[0].obj.coordinates[0], testLocations.MONGODB_NYC_OFFICE[0]); assert.equal(ret[0].obj.coordinates[1], testLocations.MONGODB_NYC_OFFICE[1]); assert.ok(stat); } function finish() { - db.close(done); + done(); } promise.then(validate, assert.ifError).then(finish).end(); - }); }); }); + it('promise fulfill even when no results returned', function(done) { - if (!mongo24_or_greater) return done(); - var db = start(); var Geo = getModel(db); - Geo.on('index', function(err) { - assert.ifError(err); - var g = new Geo({ coordinates : [1,1], type : "Point"}); + Geo.init().then(function() { + var g = new Geo({coordinates: [1, 1], type: 'Point'}); g.save(function(err) { assert.ifError(err); - var pnt = { type : "Point", coordinates : [90, 45] }; + var pnt = {type: 'Point', coordinates: [90, 45]}; var promise; assert.doesNotThrow(function() { - promise = Geo.geoNear(pnt, { spherical : true, maxDistance : 1000 }); + promise = Geo.geoNear(pnt, {spherical: true, maxDistance: 1000}); }); function finish() { - db.close(done); + done(); } promise.then(finish).end(); + }); + }); + }); + it('casts (gh-5765)', function(done) { + var Geo = getModel(db); + Geo.init().then(function() { + var g = new Geo({coordinates: [1, 1], type: 'Point', priority: 1}); + g.save(function(error) { + assert.ifError(error); + var opts = { + maxDistance: 1000, + query: { priority: '1' }, + spherical: true + }; + Geo.geoNear([1, 1], opts, function(error, res) { + assert.ifError(error); + assert.equal(res.length, 1); + assert.equal(res[0].obj.priority, 1); + done(); + }); }); }); }); diff --git a/test/model.geosearch.test.js b/test/model.geosearch.test.js index b03e1cb059c..f144a05c5bc 100644 --- a/test/model.geosearch.test.js +++ b/test/model.geosearch.test.js @@ -1,29 +1,30 @@ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema; - -/** - * Setup - */ - -var schema = new Schema({ - pos : [Number], - complex : {}, - type: String -}); -schema.index({ "pos" : "geoHaystack", type : 1},{ bucketSize : 1}); - -function getModel(db) { - return db.model('GeoSearch', schema, 'geosearch-' + random()); -} +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema; describe('model', function() { + var schema; + + function getModel(db) { + return db.model('GeoSearch', schema, 'geosearch-' + random()); + } + + before(function() { + schema = new Schema({ + pos: [Number], + complex: {}, + type: String + }); + schema.index({pos: 'geoHaystack', type: 1}, {bucketSize: 1}); + }); + describe('geoSearch', function() { - it('works', function(done) { + this.timeout(process.env.TRAVIS ? 8000 : 4500); + it('works', function(done) { var db = start(); var Geo = getModel(db); assert.ok(Geo.geoSearch instanceof Function); @@ -32,10 +33,10 @@ describe('model', function() { assert.ifError(err); var geos = []; - geos[0] = new Geo({ pos : [10,10], type : "place"}); - geos[1] = new Geo({ pos : [15,5], type : "place"}); - geos[2] = new Geo({ pos : [20,15], type : "house"}); - geos[3] = new Geo({ pos : [1,-1], type : "house"}); + geos[0] = new Geo({pos: [10, 10], type: 'place'}); + geos[1] = new Geo({pos: [15, 5], type: 'place'}); + geos[2] = new Geo({pos: [20, 15], type: 'house'}); + geos[3] = new Geo({pos: [1, -1], type: 'house'}); var count = geos.length; for (var i = 0; i < geos.length; i++) { @@ -46,9 +47,9 @@ describe('model', function() { } function next() { - Geo.geoSearch({ type : "place" }, { near : [9,9], maxDistance : 5 }, function(err, results) { + Geo.geoSearch({type: 'place'}, {near: [9, 9], maxDistance: 5}, function(err, results) { assert.ifError(err); - assert.equal(1, results.length); + assert.equal(results.length, 1); assert.equal(results[0].type, 'place'); assert.equal(results[0].pos.length, 2); @@ -57,9 +58,9 @@ describe('model', function() { assert.equal(results[0].id, geos[0].id); assert.ok(results[0] instanceof Geo); - Geo.geoSearch({ type : "place" }, { near : [40,40], maxDistance : 5 }, function(err, results) { + Geo.geoSearch({type: 'place'}, {near: [40, 40], maxDistance: 5}, function(err, results) { assert.ifError(err); - assert.equal(0, results.length); + assert.equal(results.length, 0); db.close(done); }); }); @@ -67,7 +68,6 @@ describe('model', function() { }); }); it('works with lean', function(done) { - var db = start(); var Geo = getModel(db); assert.ok(Geo.geoSearch instanceof Function); @@ -76,10 +76,10 @@ describe('model', function() { assert.ifError(err); var geos = []; - geos[0] = new Geo({ pos : [10,10], type : "place"}); - geos[1] = new Geo({ pos : [15,5], type : "place"}); - geos[2] = new Geo({ pos : [20,15], type : "house"}); - geos[3] = new Geo({ pos : [1,-1], type : "house"}); + geos[0] = new Geo({pos: [10, 10], type: 'place'}); + geos[1] = new Geo({pos: [15, 5], type: 'place'}); + geos[2] = new Geo({pos: [20, 15], type: 'house'}); + geos[3] = new Geo({pos: [1, -1], type: 'house'}); var count = geos.length; for (var i = 0; i < geos.length; i++) { @@ -90,9 +90,9 @@ describe('model', function() { } function next() { - Geo.geoSearch({ type : "place" }, { near : [9,9], maxDistance : 5, lean : true }, function(err, results) { + Geo.geoSearch({type: 'place'}, {near: [9, 9], maxDistance: 5, lean: true}, function(err, results) { assert.ifError(err); - assert.equal(1, results.length); + assert.equal(results.length, 1); assert.equal(results[0].type, 'place'); assert.equal(results[0].pos.length, 2); @@ -107,7 +107,6 @@ describe('model', function() { }); }); it('throws the correct error messages', function(done) { - var db = start(); var Geo = getModel(db); assert.ok(Geo.geoSearch instanceof Function); @@ -115,21 +114,21 @@ describe('model', function() { Geo.on('index', function(err) { assert.ifError(err); - var g = new Geo({ pos : [10,10], type : "place"}); + var g = new Geo({pos: [10, 10], type: 'place'}); g.save(function() { Geo.geoSearch([], {}, function(e) { assert.ok(e); - assert.equal(e.message, "Must pass conditions to geoSearch"); + assert.equal(e.message, 'Must pass conditions to geoSearch'); - Geo.geoSearch({ type : "test"}, {}, function(e) { + Geo.geoSearch({type: 'test'}, {}, function(e) { assert.ok(e); - assert.equal(e.message, "Must specify the near option in geoSearch"); + assert.equal(e.message, 'Must specify the near option in geoSearch'); - Geo.geoSearch({ type : "test" }, { near : "hello" }, function(e) { + Geo.geoSearch({type: 'test'}, {near: 'hello'}, function(e) { assert.ok(e); - assert.equal(e.message, "near option must be an array [x, y]"); + assert.equal(e.message, 'near option must be an array [x, y]'); - Geo.geoSearch({ type : "test" }, { near : [1,2] }, function(err) { + Geo.geoSearch({type: 'test'}, {near: [1, 2]}, function(err) { assert.ok(err); assert.ok(/maxDistance needs a number/.test(err)); db.close(done); @@ -144,7 +143,7 @@ describe('model', function() { var db = start(); var Geo = getModel(db); - var prom = Geo.geoSearch({ type : "place" }, { near : [9,9], maxDistance : 5 }, function() {}); + var prom = Geo.geoSearch({type: 'place'}, {near: [9, 9], maxDistance: 5}, function() {}); assert.ok(prom instanceof mongoose.Promise); db.close(); done(); @@ -155,16 +154,16 @@ describe('model', function() { var Geo = getModel(db); Geo.on('index', function(err) { assert.ifError(err); - var g = new Geo({ pos : [10,10], type : "place"}); + var g = new Geo({pos: [10, 10], type: 'place'}); g.save(function(err) { assert.ifError(err); var promise; assert.doesNotThrow(function() { - promise = Geo.geoSearch({ type : "place" }, { near : [9,9], maxDistance : 5 }); + promise = Geo.geoSearch({type: 'place'}, {near: [9, 9], maxDistance: 5}); }); function validate(ret, stat) { - assert.equal(1, ret.length); + assert.equal(ret.length, 1); assert.equal(ret[0].pos[0], 10); assert.equal(ret[0].pos[1], 10); assert.ok(stat); @@ -174,7 +173,6 @@ describe('model', function() { db.close(done); } promise.then(validate, assert.ifError).then(finish).end(); - }); }); }); diff --git a/test/model.hydrate.test.js b/test/model.hydrate.test.js index de4ab11041d..cd63088881b 100644 --- a/test/model.hydrate.test.js +++ b/test/model.hydrate.test.js @@ -2,40 +2,42 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , Schema = mongoose.Schema - , DocumentObjectId = mongoose.Types.ObjectId; - -/** - * Setup - */ - -var schemaB = Schema({ - title: String, - type: String -}, {discriminatorKey: 'type'}); - -var schemaC = Schema({ - test: { - type: String, - default: 'test' - } -}, {discriminatorKey: 'type'}); - +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + Schema = mongoose.Schema, + DocumentObjectId = mongoose.Types.ObjectId; describe('model', function() { + var schemaB; + var schemaC; + + before(function() { + schemaB = new Schema({ + title: String, + type: String + }, {discriminatorKey: 'type'}); + + schemaC = new Schema({ + test: { + type: String, + default: 'test' + } + }, {discriminatorKey: 'type'}); + }); + describe('hydrate()', function() { var db; var B; var Breakfast; - var breakfastSchema = Schema({ - food: { type: String, enum: ['bacon', 'eggs'] } - }); + var breakfastSchema; before(function() { + breakfastSchema = new Schema({ + food: {type: String, enum: ['bacon', 'eggs']} + }); + db = start(); B = db.model('model-create', schemaB, 'gh-2637-1'); B.discriminator('C', schemaC); @@ -47,7 +49,7 @@ describe('model', function() { }); it('hydrates documents with no modified paths', function(done) { - var hydrated = B.hydrate({ _id: '541085faedb2f28965d0e8e7', title: 'chair' }); + var hydrated = B.hydrate({_id: '541085faedb2f28965d0e8e7', title: 'chair'}); assert.ok(hydrated.get('_id') instanceof DocumentObjectId); assert.equal(hydrated.title, 'chair'); @@ -67,7 +69,7 @@ describe('model', function() { hydrated.validate(function(err) { assert.ok(err); - assert.ok(err.errors['food']); + assert.ok(err.errors.food); assert.deepEqual(['food'], Object.keys(err.errors)); done(); }); @@ -77,7 +79,7 @@ describe('model', function() { var hydrated = B.hydrate({_id: '541085faedb2f28965d0e8e8', title: 'chair', type: 'C'}); assert.equal(hydrated.test, 'test'); - assert.ok(hydrated.schema === schemaC); + assert.deepEqual(hydrated.schema, schemaC); done(); }); }); diff --git a/test/model.indexes.test.js b/test/model.indexes.test.js index c8aca09274b..7d1255740fb 100644 --- a/test/model.indexes.test.js +++ b/test/model.indexes.test.js @@ -1,34 +1,42 @@ - /** * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ObjectId = Schema.Types.ObjectId; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema, + ObjectId = Schema.Types.ObjectId; describe('model', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + describe('indexes', function() { it('are created when model is compiled', function(done) { var Indexed = new Schema({ - name : { type: String, index: true } - , last : String - , email : String - , date : Date + name: {type: String, index: true}, + last: String, + email: String, + date: Date }); - Indexed.index({ last: 1, email: 1 }, { unique: true }); - Indexed.index({ date: 1 }, { expires: 10 }); + Indexed.index({last: 1, email: 1}, {unique: true}); + Indexed.index({date: 1}, {expires: 10}); - var db = start() - , IndexedModel = db.model('IndexedModel', Indexed, 'indexedmodel' + random()) - , assertions = 0; + var IndexedModel = db.model('IndexedModel1', Indexed, 'indexedmodel' + random()); + var assertions = 0; IndexedModel.on('index', function() { - IndexedModel.collection.getIndexes({full:true}, function(err, indexes) { + IndexedModel.collection.getIndexes({full: true}, function(err, indexes) { assert.ifError(err); indexes.forEach(function(index) { @@ -45,112 +53,150 @@ describe('model', function() { } }); - assert.equal(4, assertions); - db.close(done); + assert.equal(assertions, 4); + done(); }); }); }); it('of embedded documents', function(done) { var BlogPosts = new Schema({ - _id : { type: ObjectId, index: true } - , title : { type: String, index: true } - , desc : String + _id: {type: ObjectId, index: true}, + title: {type: String, index: true}, + desc: String }); var User = new Schema({ - name : { type: String, index: true } - , blogposts : [BlogPosts] + name: {type: String, index: true}, + blogposts: [BlogPosts] }); - var db = start() - , UserModel = db.model('DeepIndexedModel', User, 'deepindexedmodel' + random()) - , assertions = 0; + var UserModel = db.model('DeepIndexedModel2', User, 'deepindexedmodel' + random()); + var assertions = 0; UserModel.on('index', function() { UserModel.collection.getIndexes(function(err, indexes) { assert.ifError(err); + function iter(index) { + if (index[0] === 'name') { + assertions++; + } + if (index[0] === 'blogposts._id') { + assertions++; + } + if (index[0] === 'blogposts.title') { + assertions++; + } + } + for (var i in indexes) { - indexes[i].forEach(function(index) { - if (index[0] == 'name') - assertions++; - if (index[0] == 'blogposts._id') - assertions++; - if (index[0] == 'blogposts.title') - assertions++; - }); + indexes[i].forEach(iter); } - assert.equal(3, assertions); - db.close(done); + assert.equal(assertions, 3); + done(); + }); + }); + }); + + it('of embedded documents unless excludeIndexes (gh-5575)', function(done) { + var BlogPost = new Schema({ + _id: {type: ObjectId}, + title: {type: String, index: true}, + desc: String + }); + + var User = new Schema({ + name: {type: String, index: true}, + blogposts: { + type: [BlogPost], + excludeIndexes: true + }, + otherblogposts: [{ type: BlogPost, excludeIndexes: true }], + blogpost: { + type: BlogPost, + excludeIndexes: true + } + }); + + var UserModel = db.model('gh5575', User); + + UserModel.on('index', function() { + UserModel.collection.getIndexes(function(err, indexes) { + assert.ifError(err); + + // Should only have _id and name indexes + var indexNames = Object.keys(indexes); + assert.deepEqual(indexNames.sort(), ['_id_', 'name_1']); + done(); }); }); }); it('of multiple embedded documents with same schema', function(done) { var BlogPosts = new Schema({ - _id: { type: ObjectId, index: true }, - title: { type: String, index: true }, + _id: {type: ObjectId, index: true}, + title: {type: String, index: true}, desc: String }); var User = new Schema({ - name: { type: String, index: true }, + name: {type: String, index: true}, blogposts: [BlogPosts], featured: [BlogPosts] }); - var db = start(); - var UserModel = db.model('DeepIndexedModel', User, 'gh-2322'); + var UserModel = db.model('DeepIndexedModelMulti3', User, 'gh2322'); var assertions = 0; UserModel.on('index', function() { UserModel.collection.getIndexes(function(err, indexes) { assert.ifError(err); + function iter(index) { + if (index[0] === 'name') { + ++assertions; + } + if (index[0] === 'blogposts._id') { + ++assertions; + } + if (index[0] === 'blogposts.title') { + ++assertions; + } + if (index[0] === 'featured._id') { + ++assertions; + } + if (index[0] === 'featured.title') { + ++assertions; + } + } + for (var i in indexes) { - indexes[i].forEach(function(index) { - if (index[0] === 'name') { - ++assertions; - } - if (index[0] === 'blogposts._id') { - ++assertions; - } - if (index[0] === 'blogposts.title') { - ++assertions; - } - if (index[0] === 'featured._id') { - ++assertions; - } - if (index[0] === 'featured.title') { - ++assertions; - } - }); + indexes[i].forEach(iter); } - assert.equal(5, assertions); - db.close(done); + assert.equal(assertions, 5); + done(); }); }); }); it('compound: on embedded docs', function(done) { var BlogPosts = new Schema({ - title : String - , desc : String + title: String, + desc: String }); - BlogPosts.index({ title: 1, desc: 1 }); + BlogPosts.index({title: 1, desc: 1}); var User = new Schema({ - name : { type: String, index: true } - , blogposts : [BlogPosts] + name: {type: String, index: true}, + blogposts: [BlogPosts] }); - var db = start() - , UserModel = db.model('DeepCompoundIndexModel', User, 'deepcompoundindexmodel' + random()) - , found = 0; + var UserModel = db.model('DeepCompoundIndexModel4', User, 'deepcompoundindexmodel' + random()); + var found = 0; UserModel.on('index', function() { UserModel.collection.getIndexes(function(err, indexes) { @@ -165,54 +211,94 @@ describe('model', function() { } } - db.close(); - assert.equal(2, found); + assert.equal(found, 2); done(); }); }); }); - it('error should emit on the model', function(done) { - var db = start(); + it('nested embedded docs (gh-5199)', function(done) { + var SubSubSchema = mongoose.Schema({ + nested2: String + }); - var schema = new Schema({ name: { type: String } }) - , Test = db.model('IndexError', schema, "x" + random()); + SubSubSchema.index({ nested2: 1 }); - Test.on('index', function(err) { - db.close(); - assert.ok(/E11000 duplicate key error/.test(err.message), err); - done(); + var SubSchema = mongoose.Schema({ + nested1: String, + subSub: SubSubSchema + }); + + SubSchema.index({ nested1: 1 }); + + var ContainerSchema = mongoose.Schema({ + nested0: String, + sub: SubSchema + }); + + ContainerSchema.index({ nested0: 1 }); + + assert.deepEqual(ContainerSchema.indexes().map(function(v) { return v[0]; }), [ + { 'sub.subSub.nested2': 1 }, + { 'sub.nested1': 1 }, + { 'nested0': 1 } + ]); + + done(); + }); + + it('primitive arrays (gh-3347)', function(done) { + var schema = new Schema({ + arr: [{ type: String, unique: true }] }); - Test.create({ name: 'hi' }, { name: 'hi' }, function(err) { + var indexes = schema.indexes(); + assert.equal(indexes.length, 1); + assert.deepEqual(indexes[0][0], { arr: 1 }); + assert.ok(indexes[0][1].unique); + + done(); + }); + + it('error should emit on the model', function(done) { + var schema = new Schema({name: {type: String}}); + var Test = db.model('IndexError5', schema, 'x' + random()); + + Test.create({name: 'hi'}, {name: 'hi'}, function(err) { assert.strictEqual(err, null); - Test.schema.index({ name: 1 }, { unique: true }); - Test.schema.index({ other: 1 }); + Test.schema.index({name: 1}, {unique: true}); + Test.schema.index({other: 1}); + + Test.on('index', function(err) { + assert.ok(/E11000 duplicate key error/.test(err.message), err); + done(); + }); + + delete Test.$init; Test.init(); }); }); describe('auto creation', function() { it('can be disabled', function(done) { - var db = start(); - var schema = new Schema({ name: { type: String, index: true }}); + var schema = new Schema({name: {type: String, index: true}}); schema.set('autoIndex', false); - var Test = db.model('AutoIndexing', schema, 'autoindexing-disable'); + var Test = db.model('AutoIndexing6', schema, 'autoindexing-disable'); Test.on('index', function() { assert.ok(false, 'Model.ensureIndexes() was called'); }); // Create a doc because mongodb 3.0 getIndexes errors if db doesn't // exist - Test.create({ name: 'Bacon' }, function(err) { + Test.create({name: 'Bacon'}, function(err) { assert.ifError(err); setTimeout(function() { Test.collection.getIndexes(function(err, indexes) { assert.ifError(err); // Only default _id index should exist assert.deepEqual(['_id_'], Object.keys(indexes)); - db.close(done); + done(); }); }, 100); }); @@ -220,29 +306,28 @@ describe('model', function() { describe('global autoIndexes (gh-1875)', function() { it('will create indexes as a default', function(done) { - var db = start(); - var schema = new Schema({name : { type: String, index: true } }); - var Test = db.model('GlobalAutoIndex', schema, 'gh-1875-1'); + var schema = new Schema({name: {type: String, index: true}}); + var Test = db.model('GlobalAutoIndex7', schema, 'gh-1875-1'); Test.on('index', function(error) { assert.ifError(error); assert.ok(true, 'Model.ensureIndexes() was called'); Test.collection.getIndexes(function(err, indexes) { assert.ifError(err); - assert.equal(2, Object.keys(indexes).length); - db.close(done); + assert.equal(Object.keys(indexes).length, 2); + done(); }); }); }); it('will not create indexes if the global auto index is false and schema option isnt set (gh-1875)', function(done) { - var db = start({config: {autoIndex : false}}); - var schema = new Schema({name : {type: String, index: true}}); - var Test = db.model('GlobalAutoIndex', schema, "x" + random()); + var db = start({config: {autoIndex: false}}); + var schema = new Schema({name: {type: String, index: true}}); + var Test = db.model('GlobalAutoIndex8', schema, 'x' + random()); Test.on('index', function() { assert.ok(false, 'Model.ensureIndexes() was called'); }); - Test.create({ name: 'Bacon' }, function(err) { + Test.create({name: 'Bacon'}, function(err) { assert.ifError(err); setTimeout(function() { Test.collection.getIndexes(function(err, indexes) { @@ -256,60 +341,16 @@ describe('model', function() { }); }); - it('do not trigger "MongoError: cannot add index with a background operation in progress" (gh-1365) LONG', function(done) { - this.timeout(45000); - - var db = start({ uri: 'mongodb://localhost/mongoose_test_indexing'}); - - var schema = Schema({ - name: { type:String, index: true } - , furryness: { type:Number, index: true } - }, { autoIndex: false }); - - schema.index({ name:1, furryness:1}); - - var K = db.model('Kitten', schema); - K.on('index', function(err) { - assert.ifError(err); - db.close(done); - }); - - var neededKittens = 30000; - - db.on('open', function() { - K.count({}, function(err, n) { - assert.ifError(err); - if (n >= neededKittens) return index(); - var pending = neededKittens - n; - for (var i = n; i < neededKittens; ++i) (function(i) { - K.create({ name: 'kitten' + i, furryness: i }, function(err) { - assert.ifError(err); - if (--pending) return; - index(); - }); - })(i); - }); - - function index() { - K.collection.dropAllIndexes(function(err) { - assert.ifError(err); - K.ensureIndexes(); - }); - } - }); - }); - - describe('model.ensureIndexes()', function() { it('is a function', function(done) { - var schema = mongoose.Schema({ x: 'string' }); + var schema = mongoose.Schema({x: 'string'}); var Test = mongoose.createConnection().model('ensureIndexes-' + random, schema); - assert.equal('function', typeof Test.ensureIndexes); + assert.equal(typeof Test.ensureIndexes, 'function'); done(); }); it('returns a Promise', function(done) { - var schema = mongoose.Schema({ x: 'string' }); + var schema = mongoose.Schema({x: 'string'}); var Test = mongoose.createConnection().model('ensureIndexes-' + random, schema); var p = Test.ensureIndexes(); assert.ok(p instanceof mongoose.Promise); @@ -317,11 +358,10 @@ describe('model', function() { }); it('creates indexes', function(done) { - var db = start(); - var schema = new Schema({ name: { type: String } }) - , Test = db.model('ManualIndexing', schema, "x" + random()); + var schema = new Schema({name: {type: String}}); + var Test = db.model('ManualIndexing' + random(), schema, 'x' + random()); - Test.schema.index({ name: 1 }, { sparse: true }); + Test.schema.index({name: 1}, {sparse: true}); var called = false; Test.on('index', function() { @@ -331,7 +371,7 @@ describe('model', function() { Test.ensureIndexes(function(err) { assert.ifError(err); assert.ok(called); - db.close(done); + done(); }); }); }); diff --git a/test/model.mapreduce.test.js b/test/model.mapreduce.test.js index 2f82ad29883..c445c73bba9 100644 --- a/test/model.mapreduce.test.js +++ b/test/model.mapreduce.test.js @@ -1,71 +1,75 @@ -/* eslint unused-vars: 1 */ /* global emit */ /** * Test dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ObjectId = Schema.Types.ObjectId; +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + random = require('../lib/utils').random, + Schema = mongoose.Schema, + ObjectId = Schema.Types.ObjectId; -/** - * Setup. - */ - -var Comments = new Schema(); - -Comments.add({ - title : String - , date : Date - , body : String - , comments : [Comments] -}); - -var BlogPost = new Schema({ - title : String - , author : String - , slug : String - , date : Date - , meta : { - date : Date - , visitors : Number - } - , published : Boolean - , mixed : {} - , numbers : [Number] - , owners : [ObjectId] - , comments : [Comments] -}); +describe('model: mapreduce:', function() { + var Comments; + var BlogPost; + var collection; + + before(function() { + Comments = new Schema(); + + Comments.add({ + title: String, + date: Date, + body: String, + comments: [Comments] + }); + BlogPost = new Schema({ + title: String, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number + }, + published: Boolean, + mixed: {}, + numbers: [Number], + owners: [ObjectId], + comments: [Comments] + }); -var collection = 'mapreduce_' + random(); -mongoose.model('MapReduce', BlogPost); + collection = 'mapreduce_' + random(); + mongoose.model('MapReduce', BlogPost); + }); -describe('model: mapreduce:', function() { it('works', function(done) { - var db = start() - , MR = db.model('MapReduce', collection); + var db = start(), + MR = db.model('MapReduce', collection); var magicID; var id = new mongoose.Types.ObjectId; var authors = 'aaron guillermo brian nathan'.split(' '); var num = 10; var docs = []; - for (var i = 0; i < num; ++i) - docs.push({ author: authors[i % authors.length], owners: [id], published: true }); + for (var i = 0; i < num; ++i) { + docs.push({author: authors[i % authors.length], owners: [id], published: true}); + } MR.create(docs, function(err, insertedDocs) { assert.ifError(err); - var b = insertedDocs[1]; - magicID = b._id; + magicID = insertedDocs[1]._id; var o = { - map: function() { emit(this.author, 1); } - , reduce: function(k, vals) { return vals.length; } + map: function() { + emit(this.author, 1); + }, + reduce: function(k, vals) { + return vals.length; + } }; MR.mapReduce(o, function(err, ret, stats) { @@ -73,25 +77,37 @@ describe('model: mapreduce:', function() { assert.ok(Array.isArray(ret)); assert.ok(stats); ret.forEach(function(res) { - if ('aaron' == res._id) assert.equal(3, res.value); - if ('guillermo' == res._id) assert.equal(3, res.value); - if ('brian' == res._id) assert.equal(2, res.value); - if ('nathan' == res._id) assert.equal(2, res.value); + if (res._id === 'aaron') { + assert.equal(res.value, 3); + } + if (res._id === 'guillermo') { + assert.equal(res.value, 3); + } + if (res._id === 'brian') { + assert.equal(res.value, 2); + } + if (res._id === 'nathan') { + assert.equal(res.value, 2); + } }); var o = { - map: function() { emit(this.author, 1); } - , reduce: function(k, vals) { return vals.length; } - , query: { author: 'aaron', published: 1, owners: id } + map: function() { + emit(this.author, 1); + }, + reduce: function(k, vals) { + return vals.length; + }, + query: {author: 'aaron', published: 1, owners: id} }; MR.mapReduce(o, function(err, ret, stats) { assert.ifError(err); assert.ok(Array.isArray(ret)); - assert.equal(1, ret.length); - assert.equal('aaron', ret[0]._id); - assert.equal(3, ret[0].value); + assert.equal(ret.length, 1); + assert.equal(ret[0]._id, 'aaron'); + assert.equal(ret[0].value, 3); assert.ok(stats); modeling(); @@ -100,10 +116,14 @@ describe('model: mapreduce:', function() { function modeling() { var o = { - map: function() { emit(this.author, { own: magicID }); } - , scope: { magicID: magicID } - , reduce: function(k, vals) { return { own: vals[0].own, count: vals.length };} - , out: { replace: '_mapreduce_test_' + random() } + map: function() { + emit(this.author, {own: magicID}); + }, + scope: {magicID: magicID}, + reduce: function(k, vals) { + return {own: vals[0].own, count: vals.length}; + }, + out: {replace: '_mapreduce_test_' + random()} }; MR.mapReduce(o, function(err, ret) { @@ -111,32 +131,32 @@ describe('model: mapreduce:', function() { // ret is a model assert.ok(!Array.isArray(ret)); - assert.equal('function', typeof ret.findOne); - assert.equal('function', typeof ret.mapReduce); + assert.equal(typeof ret.findOne, 'function'); + assert.equal(typeof ret.mapReduce, 'function'); // queries work ret.where('value.count').gt(1).sort({_id: 1}).exec(function(err, docs) { assert.ifError(err); - assert.equal('aaron', docs[0]._id); - assert.equal('brian', docs[1]._id); - assert.equal('guillermo', docs[2]._id); - assert.equal('nathan', docs[3]._id); + assert.equal(docs[0]._id, 'aaron'); + assert.equal(docs[1]._id, 'brian'); + assert.equal(docs[2]._id, 'guillermo'); + assert.equal(docs[3]._id, 'nathan'); // update casting works - ret.findOneAndUpdate({ _id: 'aaron' }, { published: true }, { 'new': true }, function(err, doc) { + ret.findOneAndUpdate({_id: 'aaron'}, {published: true}, {new: true}, function(err, doc) { assert.ifError(err); assert.ok(doc); - assert.equal('aaron', doc._id); - assert.equal(true, doc.published); + assert.equal(doc._id, 'aaron'); + assert.equal(doc.published, true); // ad-hoc population works ret - .findOne({ _id: 'aaron' }) - .populate({ path: 'value.own', model: 'MapReduce' }) + .findOne({_id: 'aaron'}) + .populate({path: 'value.own', model: 'MapReduce'}) .exec(function(err, doc) { db.close(); assert.ifError(err); - assert.equal('guillermo', doc.value.own.author); + assert.equal(doc.value.own.author, 'guillermo'); done(); }); }); @@ -147,52 +167,63 @@ describe('model: mapreduce:', function() { }); it('withholds stats with false verbosity', function(done) { - var db = start() - , MR = db.model('MapReduce', collection); + var db = start(), + MR = db.model('MapReduce', collection); var o = { - map: function() {} - , reduce: function() { return 'test'; } - , verbose: false + map: function() { + }, + reduce: function() { + return 'test'; + }, + verbose: false }; MR.mapReduce(o, function(err, results, stats) { - assert.equal('undefined', typeof stats); + assert.equal(typeof stats, 'undefined'); db.close(done); }); }); describe('promises (gh-1628)', function() { it('are returned', function(done) { - var db = start() - , MR = db.model('MapReduce', collection); + var db = start(), + MR = db.model('MapReduce', collection); var o = { - map: function() {} - , reduce: function() { return 'test'; } + map: function() { + }, + reduce: function() { + return 'test'; + } }; - var promise = MR.mapReduce(o, function() {}); + var promise = MR.mapReduce(o, function() { + }); assert.ok(promise instanceof mongoose.Promise); db.close(done); }); it('allow not passing a callback', function(done) { - var db = start() - , MR = db.model('MapReduce', collection); + var db = start(), + MR = db.model('MapReduce', collection); var o = { - map: function() { emit(this.author, 1); } - , reduce: function(k, vals) { return vals.length; } - , query: { author: 'aaron', published: 1 } + map: function() { + emit(this.author, 1); + }, + reduce: function(k, vals) { + return vals.length; + }, + query: {author: 'aaron', published: 1} }; function validate(ret, stats) { assert.ok(Array.isArray(ret)); - assert.equal(1, ret.length); - assert.equal('aaron', ret[0]._id); - assert.equal(3, ret[0].value); + assert.equal(ret.length, 1); + assert.equal(ret[0]._id, 'aaron'); + assert.equal(ret[0].value, 6); assert.ok(stats); } @@ -208,6 +239,163 @@ describe('model: mapreduce:', function() { promise.then(validate, assert.ifError).then(finish).end(); }); + }); + + it('works using then', function(done) { + var db = start(), + MR = db.model('MapReduce', collection); + + var magicID; + var id = new mongoose.Types.ObjectId; + var authors = 'aaron guillermo brian nathan'.split(' '); + var num = 10; + var docs = []; + for (var i = 0; i < num; ++i) { + docs.push({author: authors[i % authors.length], owners: [id], published: true}); + } + MR.create(docs, function(err, insertedDocs) { + assert.ifError(err); + + var b = insertedDocs[1]; + magicID = b._id; + + var o = { + map: function() { + emit(this.author, 1); + }, + reduce: function(k, vals) { + return vals.length; + } + }; + + MR.mapReduce(o).then(function(ret, stats) { + assert.ok(Array.isArray(ret)); + assert.ok(stats); + ret.forEach(function(res) { + if (res._id === 'aaron') { + assert.equal(res.value, 6); + } + if (res._id === 'guillermo') { + assert.equal(res.value, 6); + } + if (res._id === 'brian') { + assert.equal(res.value, 4); + } + if (res._id === 'nathan') { + assert.equal(res.value, 4); + } + }); + + var o = { + map: function() { + emit(this.author, 1); + }, + reduce: function(k, vals) { + return vals.length; + }, + query: {author: 'aaron', published: 1, owners: id} + }; + + MR.mapReduce(o).then(function(ret, stats) { + assert.ok(Array.isArray(ret)); + assert.equal(ret.length, 1); + assert.equal(ret[0]._id, 'aaron'); + assert.equal(ret[0].value, 3); + assert.ok(stats); + modeling(); + }); + }); + + function modeling() { + var o = { + map: function() { + emit(this.author, {own: magicID}); + }, + scope: {magicID: magicID}, + reduce: function(k, vals) { + return {own: vals[0].own, count: vals.length}; + }, + out: {replace: '_mapreduce_test_' + random()} + }; + + MR.mapReduce(o).then(function(ret) { + // ret is a model + assert.ok(!Array.isArray(ret)); + assert.equal(typeof ret.findOne, 'function'); + assert.equal(typeof ret.mapReduce, 'function'); + + // queries work + ret.where('value.count').gt(1).sort({_id: 1}).exec(function(err, docs) { + assert.ifError(err); + assert.equal(docs[0]._id, 'aaron'); + assert.equal(docs[1]._id, 'brian'); + assert.equal(docs[2]._id, 'guillermo'); + assert.equal(docs[3]._id, 'nathan'); + + // update casting works + ret.findOneAndUpdate({_id: 'aaron'}, {published: true}, {new: true}, function(err, doc) { + assert.ifError(err); + assert.ok(doc); + assert.equal(doc._id, 'aaron'); + assert.equal(doc.published, true); + + // ad-hoc population works + ret + .findOne({_id: 'aaron'}) + .populate({path: 'value.own', model: 'MapReduce'}) + .exec(function(err, doc) { + db.close(); + assert.ifError(err); + assert.equal(doc.value.own.author, 'guillermo'); + done(); + }); + }); + }); + }); + } + }); + }); + + it('withholds stats with false verbosity using then', function(done) { + var db = start(), + MR = db.model('MapReduce', collection); + + var o = { + map: function() { + }, + reduce: function() { + return 'test'; + }, + verbose: false + }; + + MR.mapReduce(o).then(function(results, stats) { + assert.equal(typeof stats, 'undefined'); + db.close(done); + }); + }); + + it('resolveToObject (gh-4945)', function(done) { + var db = start(); + var MR = db.model('MapReduce', collection); + + var o = { + map: function() { + }, + reduce: function() { + return 'test'; + }, + verbose: false, + resolveToObject: true + }; + + MR.create({ title: 'test' }, function(error) { + assert.ifError(error); + MR.mapReduce(o).then(function(obj) { + assert.ok(obj.model); + db.close(done); + }).catch(done); + }); }); }); diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js index 6271c5bd70c..41f28e301bc 100644 --- a/test/model.middleware.test.js +++ b/test/model.middleware.test.js @@ -3,10 +3,10 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , Schema = mongoose.Schema; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + Schema = mongoose.Schema; describe('model middleware', function() { it('post save', function(done) { @@ -19,33 +19,33 @@ describe('model middleware', function() { schema.post('save', function(obj) { assert.equal(obj.title, 'Little Green Running Hood'); assert.equal(this.title, 'Little Green Running Hood'); - assert.equal(0, called); + assert.equal(called, 0); called++; }); schema.post('save', function(obj) { assert.equal(obj.title, 'Little Green Running Hood'); assert.equal(this.title, 'Little Green Running Hood'); - assert.equal(1, called); + assert.equal(called, 1); called++; }); schema.post('save', function(obj, next) { assert.equal(obj.title, 'Little Green Running Hood'); - assert.equal(2, called); + assert.equal(called, 2); called++; next(); }); - var db = start() - , TestMiddleware = db.model('TestPostSaveMiddleware', schema); + var db = start(), + TestMiddleware = db.model('TestPostSaveMiddleware', schema); - var test = new TestMiddleware({ title: 'Little Green Running Hood'}); + var test = new TestMiddleware({title: 'Little Green Running Hood'}); test.save(function(err) { assert.ifError(err); - assert.equal(test.title,'Little Green Running Hood'); - assert.equal(3, called); + assert.equal(test.title, 'Little Green Running Hood'); + assert.equal(called, 3); db.close(); done(); }); @@ -58,12 +58,12 @@ describe('model middleware', function() { var count = 0; schema.pre('validate', function(next) { - assert.equal(0, count++); + assert.equal(count++, 0); next(); }); schema.pre('save', function(next) { - assert.equal(1, count++); + assert.equal(count++, 1); next(); }); @@ -100,8 +100,8 @@ describe('model middleware', function() { mongoose.model('TestMiddleware', schema); - var db = start() - , TestMiddleware = db.model('TestMiddleware'); + var db = start(), + TestMiddleware = db.model('TestMiddleware'); var test = new TestMiddleware(); @@ -109,17 +109,17 @@ describe('model middleware', function() { title: 'Test' }); - assert.equal(1, called); + assert.equal(called, 1); test.save(function(err) { assert.ok(err instanceof Error); - assert.equal(err.message,'Error 101'); - assert.equal(2, called); + assert.equal(err.message, 'Error 101'); + assert.equal(called, 2); test.remove(function(err) { db.close(); assert.ifError(err); - assert.equal(3, called); + assert.equal(called, 3); done(); }); }); @@ -130,8 +130,8 @@ describe('model middleware', function() { title: String }); - var preinit = 0 - , postinit = 0; + var preinit = 0, + postinit = 0; schema.pre('init', function(next) { ++preinit; @@ -145,18 +145,18 @@ describe('model middleware', function() { mongoose.model('TestPostInitMiddleware', schema); - var db = start() - , Test = db.model('TestPostInitMiddleware'); + var db = start(), + Test = db.model('TestPostInitMiddleware'); - var test = new Test({ title: "banana" }); + var test = new Test({title: 'banana'}); test.save(function(err) { assert.ifError(err); Test.findById(test._id, function(err, test) { assert.ifError(err); - assert.equal(1, preinit); - assert.equal(1, postinit); + assert.equal(preinit, 1); + assert.equal(postinit, 1); test.remove(function() { db.close(); done(); @@ -197,26 +197,26 @@ describe('model middleware', function() { var parent = new Parent({ name: 'Han', children: [ - { name: 'Jaina' }, - { name: 'Jacen' } + {name: 'Jaina'}, + {name: 'Jacen'} ] }); parent.save(function(error) { assert.ifError(error); - assert.equal(2, childPreCalls); - assert.equal(1, childPreCallsByName['Jaina']); - assert.equal(1, childPreCallsByName['Jacen']); - assert.equal(1, parentPreCalls); + assert.equal(childPreCalls, 2); + assert.equal(childPreCallsByName.Jaina, 1); + assert.equal(childPreCallsByName.Jacen, 1); + assert.equal(parentPreCalls, 1); parent.children[0].name = 'Anakin'; parent.save(function(error) { assert.ifError(error); - assert.equal(4, childPreCalls); - assert.equal(1, childPreCallsByName['Anakin']); - assert.equal(1, childPreCallsByName['Jaina']); - assert.equal(2, childPreCallsByName['Jacen']); + assert.equal(childPreCalls, 4); + assert.equal(childPreCallsByName.Anakin, 1); + assert.equal(childPreCallsByName.Jaina, 1); + assert.equal(childPreCallsByName.Jacen, 2); - assert.equal(2, parentPreCalls); + assert.equal(parentPreCalls, 2); db.close(); done(); }); @@ -228,10 +228,10 @@ describe('model middleware', function() { title: String }); - var preValidate = 0 - , postValidate = 0 - , preRemove = 0 - , postRemove = 0; + var preValidate = 0, + postValidate = 0, + preRemove = 0, + postRemove = 0; schema.pre('validate', function(next) { ++preValidate; @@ -253,26 +253,27 @@ describe('model middleware', function() { ++postRemove; }); - var db = start() - , Test = db.model('TestPostValidateMiddleware', schema); + var db = start(), + Test = db.model('TestPostValidateMiddleware', schema); - var test = new Test({ title: "banana" }); + var test = new Test({title: 'banana'}); test.save(function(err) { assert.ifError(err); - assert.equal(1, preValidate); - assert.equal(1, postValidate); - assert.equal(0, preRemove); - assert.equal(0, postRemove); + assert.equal(preValidate, 1); + assert.equal(postValidate, 1); + assert.equal(preRemove, 0); + assert.equal(postRemove, 0); test.remove(function(err) { db.close(); assert.ifError(err); - assert.equal(1, preValidate); - assert.equal(1, postValidate); - assert.equal(1, preRemove); - assert.equal(1, postRemove); + assert.equal(preValidate, 1); + assert.equal(postValidate, 1); + assert.equal(preRemove, 1); + assert.equal(postRemove, 1); done(); }); }); }); }); + diff --git a/test/model.populate.divergent.test.js b/test/model.populate.divergent.test.js index 02da3e742d4..4ff8a399977 100644 --- a/test/model.populate.divergent.test.js +++ b/test/model.populate.divergent.test.js @@ -3,12 +3,12 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , DivergentArrayError = mongoose.Error.DivergentArrayError - , utils = require('../lib/utils') - , random = utils.random; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + DivergentArrayError = mongoose.Error.DivergentArrayError, + utils = require('../lib/utils'), + random = utils.random; /** * Tests. @@ -28,15 +28,15 @@ describe('model: populate: divergent arrays', function() { before(function(done) { db = start(); - C = db.model("Child", { _id: Number, name: String }, 'child-' + random()); - M = db.model("Parent", { array: { type: [{ type: Number, ref: 'Child' }] }}, 'parent-' + random()); + C = db.model('Child', {_id: Number, name: String}, 'child-' + random()); + M = db.model('Parent', {array: {type: [{type: Number, ref: 'Child'}]}}, 'parent-' + random()); C.create( - { _id: 0, name: 'zero' } - , { _id: 1, name: 'one' } - , { _id: 2, name: 'two' }, function(err) { + {_id: 0, name: 'zero'} + , {_id: 1, name: 'one'} + , {_id: 2, name: 'two'}, function(err) { assert.ifError(err); - M.create({ array: [0, 1, 2] }, function(err) { + M.create({array: [0, 1, 2]}, function(err) { assert.ifError(err); done(); }); @@ -51,7 +51,7 @@ describe('model: populate: divergent arrays', function() { it('using $set', function(done) { fn(function(err, doc) { assert.ifError(err); - doc.array.unshift({ _id: 10, name: 'ten' }); + doc.array.unshift({_id: 10, name: 'ten'}); doc.save(function(err) { check(err); done(); @@ -93,52 +93,52 @@ describe('model: populate: divergent arrays', function() { describe('from match', function() { testFails(function(cb) { - M.findOne().populate({ path: 'array', match: { name: 'one' }}).exec(cb); + M.findOne().populate({path: 'array', match: {name: 'one'}}).exec(cb); }); }); describe('from skip', function() { describe('2', function() { testFails(function(cb) { - M.findOne().populate({ path: 'array', options: { skip: 2 }}).exec(cb); + M.findOne().populate({path: 'array', options: {skip: 2}}).exec(cb); }); }); describe('0', function() { testOk(function(cb) { - M.findOne().populate({ path: 'array', options: { skip: 0 }}).exec(cb); + M.findOne().populate({path: 'array', options: {skip: 0}}).exec(cb); }); }); }); describe('from limit', function() { describe('0', function() { testFails(function(cb) { - M.findOne().populate({ path: 'array', options: { limit: 0 }}).exec(cb); + M.findOne().populate({path: 'array', options: {limit: 0}}).exec(cb); }); }); describe('1', function() { testFails(function(cb) { - M.findOne().populate({ path: 'array', options: { limit: 1 }}).exec(cb); + M.findOne().populate({path: 'array', options: {limit: 1}}).exec(cb); }); }); }); describe('from deselected _id', function() { describe('using string and only -_id', function() { testFails(function(cb) { - M.findOne().populate({ path: 'array', select: '-_id'}).exec(cb); + M.findOne().populate({path: 'array', select: '-_id'}).exec(cb); }); }); describe('using string', function() { testFails(function(cb) { - M.findOne().populate({ path: 'array', select: 'name -_id'}).exec(cb); + M.findOne().populate({path: 'array', select: 'name -_id'}).exec(cb); }); }); describe('using object and only _id: 0', function() { testFails(function(cb) { - M.findOne().populate({ path: 'array', select: { _id: 0 }}).exec(cb); + M.findOne().populate({path: 'array', select: {_id: 0}}).exec(cb); }); }); describe('using object', function() { testFails(function(cb) { - M.findOne().populate({ path: 'array', select: { _id: 0, name: 1 }}).exec(cb); + M.findOne().populate({path: 'array', select: {_id: 0, name: 1}}).exec(cb); }); }); }); diff --git a/test/model.populate.setting.test.js b/test/model.populate.setting.test.js index f1d37950605..a663b18bf1e 100644 --- a/test/model.populate.setting.test.js +++ b/test/model.populate.setting.test.js @@ -4,32 +4,34 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , utils = require('../lib/utils') - , random = utils.random - , Schema = mongoose.Schema - , DocObjectId = mongoose.Types.ObjectId; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + utils = require('../lib/utils'), + random = utils.random, + Schema = mongoose.Schema, + DocObjectId = mongoose.Types.ObjectId; /** * Setup. */ -var posts = 'blogposts_' + random() - , users = 'users_' + random(); +var posts = 'blogposts_' + random(), + users = 'users_' + random(); /** * Tests. */ describe('model: populate:', function() { + this.timeout(process.env.TRAVIS ? 8000 : 4500); + describe('setting populated paths (gh-570)', function() { var types = { - 'ObjectId': DocObjectId - , 'String': String - , 'Number': Number - , 'Buffer': Buffer + ObjectId: DocObjectId, + String: String, + Number: Number, + Buffer: Buffer }; var construct = {}; @@ -51,21 +53,22 @@ describe('model: populate:', function() { before(function(done) { refuser = 'RefUser-' + id; - var bSchema = Schema({ - title: String - , fans: [{type: id, ref: refuser }] - , adhoc: [{ subdoc: id, subarray: [{ things: [id] }] }] - , _creator: { type: id, ref: refuser } - , embed: [{ - other: { type: id, ref: refuser } - , array: [{ type: id, ref: refuser }] + var bSchema = new Schema({ + title: String, + fans: [{type: id, ref: refuser}], + adhoc: [{subdoc: id, subarray: [{things: [id]}]}], + _creator: {type: id, ref: refuser}, + embed: [{ + other: {type: id, ref: refuser}, + array: [{type: id, ref: refuser}], + nested: [{subdoc: {type: id, ref: refuser}}] }] }); - var uSchema = Schema({ - _id: id - , name: String - , email: String + var uSchema = new Schema({ + _id: id, + name: String, + email: String }); db = start(); @@ -73,30 +76,30 @@ describe('model: populate:', function() { U = db.model(refuser, uSchema, users + random()); U.create({ - _id: construct[id]() - , name : 'Fan 1' - , email : 'fan1@learnboost.com' + _id: construct[id](), + name: 'Fan 1', + email: 'fan1@learnboost.com' }, { - _id: construct[id]() - , name : 'Fan 2' - , email : 'fan2@learnboost.com' + _id: construct[id](), + name: 'Fan 2', + email: 'fan2@learnboost.com' }, function(err, fan1, fan2) { assert.ifError(err); u1 = fan1; B.create({ - title : 'Woot' - , fans : [fan1, fan2] - , adhoc : [{ subdoc: fan2, subarray: [{ things: [fan1] }]}] - , _creator: fan1 - , embed : [{ other: fan1, array: [fan1, fan2] }, { other: fan2, array: [fan2, fan1] }] + title: 'Woot', + fans: [fan1, fan2], + adhoc: [{subdoc: fan2, subarray: [{things: [fan1]}]}], + _creator: fan1, + embed: [{other: fan1, array: [fan1, fan2]}, {other: fan2, array: [fan2, fan1]}] }, { - title : 'Woot2' - , fans : [fan2, fan1] - , adhoc : [{ subdoc: fan1, subarray: [{ things: [fan2] }]}] - , _creator: fan1 - , _creator: fan2 - , embed : [{ other: fan2, array: [fan2, fan1] }, { other: fan1, array: [fan1, fan2] }] + title: 'Woot2', + fans: [fan2, fan1], + adhoc: [{subdoc: fan1, subarray: [{things: [fan2]}]}], + _creator: fan1, + _creator: fan2, + embed: [{other: fan2, array: [fan2, fan1]}, {other: fan1, array: [fan1, fan2]}] }, function(err, post1, post2) { assert.ifError(err); b1 = post1; @@ -111,7 +114,7 @@ describe('model: populate:', function() { }); function userLiteral(name) { - return { _id: construct[id](), name: name }; + return {_id: construct[id](), name: name}; } function user(name) { @@ -119,138 +122,143 @@ describe('model: populate:', function() { } it('if a document', function(done) { - B.findById(b1) - .populate('fans _creator embed.other embed.array') - .populate({ path: 'adhoc.subdoc', model: refuser }) - .populate({ path: 'adhoc.subarray.things', model: refuser }) - .exec(function(err, doc) { - assert.ifError(err); + var query = B.findById(b1). + populate('fans _creator embed.other embed.array embed.nested.subdoc'). + populate({path: 'adhoc.subdoc', model: refuser}). + populate({path: 'adhoc.subarray.things', model: refuser}); + query.exec(function(err, doc) { + assert.ifError(err); - var user3 = user('user3'); - doc.fans.push(user3); - assert.deepEqual(doc.fans[2].toObject(), user3.toObject()); + var user3 = user('user3'); + doc.fans.push(user3); + assert.deepEqual(doc.fans[2].toObject(), user3.toObject()); - var user4 = user('user4'); - doc.fans.nonAtomicPush(user4); - assert.deepEqual(doc.fans[3].toObject(), user4.toObject()); + var user4 = user('user4'); + doc.fans.nonAtomicPush(user4); + assert.deepEqual(doc.fans[3].toObject(), user4.toObject()); - var user5 = user('user5'); - doc.fans.splice(2, 1, user5); - assert.deepEqual(doc.fans[2].toObject(), user5.toObject()); + var user5 = user('user5'); + doc.fans.splice(2, 1, user5); + assert.deepEqual(doc.fans[2].toObject(), user5.toObject()); - var user6 = user('user6'); - doc.fans.unshift(user6); - assert.deepEqual(doc.fans[0].toObject(), user6.toObject()); + var user6 = user('user6'); + doc.fans.unshift(user6); + assert.deepEqual(doc.fans[0].toObject(), user6.toObject()); - var user7 = user('user7'); - doc.fans.addToSet(user7); - assert.deepEqual(doc.fans[5].toObject(), user7.toObject()); + var user7 = user('user7'); + doc.fans.addToSet(user7); + assert.deepEqual(doc.fans[5].toObject(), user7.toObject()); - doc.fans.forEach(function(doc) { - assert.ok(doc instanceof U); - }); + doc.fans.forEach(function(doc) { + assert.ok(doc instanceof U); + }); - var user8 = user('user8'); - doc.fans.set(0, user8); - assert.deepEqual(doc.fans[0].toObject(), user8.toObject()); + var user8 = user('user8'); + doc.fans.set(0, user8); + assert.deepEqual(doc.fans[0].toObject(), user8.toObject()); - doc.fans.push(null); - assert.equal(doc.fans[6], null); + doc.fans.push(null); + assert.equal(doc.fans[6], null); - var _id = construct[id](); - doc.fans.addToSet(_id); - if (Buffer.isBuffer(_id)) { - assert.equal(doc.fans[7]._id.toString('utf8'), _id.toString('utf8')); - } else { - assert.equal(doc.fans[7]._id, String(_id)); - } + var _id = construct[id](); + doc.fans.addToSet(_id); + if (Buffer.isBuffer(_id)) { + assert.equal(doc.fans[7]._id.toString('utf8'), _id.toString('utf8')); + } else { + assert.equal(doc.fans[7]._id, String(_id)); + } - assert.equal(doc._creator.email, u1.email); + assert.equal(doc._creator.email, u1.email); - doc._creator = null; - assert.equal(null, doc._creator); + doc._creator = null; + assert.equal(doc._creator, null); - var creator = user('creator'); - doc._creator = creator; - assert.ok(doc._creator instanceof mongoose.Document); - assert.deepEqual(doc._creator.toObject(), creator.toObject()); + var creator = user('creator'); + doc._creator = creator; + assert.ok(doc._creator instanceof mongoose.Document); + assert.deepEqual(doc._creator.toObject(), creator.toObject()); // embedded with declared ref in schema - var user1a = user('user1a'); - doc.embed[0].array.set(0, user1a); - assert.deepEqual(doc.embed[0].array[0].toObject(), user1a.toObject()); + var user1a = user('user1a'); + doc.embed[0].array.set(0, user1a); + assert.deepEqual(doc.embed[0].array[0].toObject(), user1a.toObject()); - var user1b = user('user1b'); - doc.embed[0].other = user1b; - assert.deepEqual(doc.embed[0].other.toObject(), user1b.toObject()); + var user1b = user('user1b'); + doc.embed[0].other = user1b; + assert.deepEqual(doc.embed[0].other.toObject(), user1b.toObject()); + + var user1c = user('user2c'); + doc.embed[0].nested = [{subdoc: user1c}]; + assert.deepEqual(doc.embed[0].nested[0].subdoc.toObject(), user1c.toObject()); // embedded without declared ref in schema - var user2a = user('user2a'); - doc.adhoc[0].subdoc = user2a; - assert.deepEqual(doc.adhoc[0].subdoc.toObject(), user2a.toObject()); + var user2a = user('user2a'); + doc.adhoc[0].subdoc = user2a; + assert.deepEqual(doc.adhoc[0].subdoc.toObject(), user2a.toObject()); - var user2b = user('user2b'); - doc.adhoc[0].subarray[0].things.push(user2b); - assert.deepEqual(doc.adhoc[0].subarray[0].things[1].toObject(), user2b.toObject()); + var user2b = user('user2b'); + doc.adhoc[0].subarray[0].things.push(user2b); + assert.deepEqual(doc.adhoc[0].subarray[0].things[1].toObject(), user2b.toObject()); - doc.save(function(err) { - assert.ifError(err); - B.findById(b1).exec(function(err, doc) { + doc.save(function(err) { + assert.ifError(err); + B.findById(b1).exec(function(err, doc) { // db is closed in after() - assert.ifError(err); - assert.equal(8, doc.fans.length); - assert.equal(doc.fans[0], user8.id); - assert.equal(doc.fans[5], user7.id); - assert.equal(doc.fans[6], null); - assert.equal(doc.fans[7], String(_id)); - assert.equal(String(doc._creator), creator._id); - assert.equal(doc.embed[0].array[0], user1a.id); - assert.equal(doc.embed[0].other, user1b.id); - assert.equal(doc.adhoc[0].subdoc, user2a.id); - assert.equal(doc.adhoc[0].subarray[0].things[1], user2b.id); - done(); - }); - }); - }); + assert.ifError(err); + assert.equal(doc.fans.length, 8); + assert.equal(doc.fans[0], user8.id); + assert.equal(doc.fans[5], user7.id); + assert.equal(doc.fans[6], null); + assert.equal(doc.fans[7], String(_id)); + assert.equal(String(doc._creator), creator._id); + assert.equal(doc.embed[0].array[0], user1a.id); + assert.equal(doc.embed[0].other, user1b.id); + assert.equal(doc.embed[0].nested[0].subdoc, user1c.id); + assert.equal(doc.adhoc[0].subdoc, user2a.id); + assert.equal(doc.adhoc[0].subarray[0].things[1], user2b.id); + done(); + }); + }); + }); }); it('if an object', function(done) { B.findById(b2) - .populate('fans _creator embed.other embed.array') - .populate({ path: 'adhoc.subdoc', model: refuser }) - .populate({ path: 'adhoc.subarray.things', model: refuser }) + .populate('fans _creator embed.other embed.array embed.nested.subdoc') + .populate({path: 'adhoc.subdoc', model: refuser}) + .populate({path: 'adhoc.subarray.things', model: refuser}) .exec(function(err, doc) { assert.ifError(err); var name = 'fan1'; doc.fans.push(userLiteral(name)); assert.ok(doc.fans[2]._id); - assert.equal(name, doc.fans[2].name); + assert.equal(doc.fans[2].name, name); name = 'fan2'; doc.fans.nonAtomicPush(userLiteral(name)); assert.ok(doc.fans[3]._id); - assert.equal(name, doc.fans[3].name); + assert.equal(doc.fans[3].name, name); name = 'fan3'; - doc.fans.splice(2,1,userLiteral(name)); + doc.fans.splice(2, 1, userLiteral(name)); assert.ok(doc.fans[2]._id); - assert.equal(name, doc.fans[2].name); + assert.equal(doc.fans[2].name, name); name = 'fan4'; doc.fans.unshift(userLiteral(name)); assert.ok(doc.fans[0]._id); - assert.equal(name, doc.fans[0].name); + assert.equal(doc.fans[0].name, name); name = 'fan5'; doc.fans.addToSet(userLiteral(name)); assert.ok(doc.fans[5]._id); - assert.equal(name, doc.fans[5].name); + assert.equal(doc.fans[5].name, name); name = 'fan6'; doc.fans.set(0, userLiteral(name)); assert.ok(doc.fans[0]._id); - assert.equal(name, doc.fans[0].name); + assert.equal(doc.fans[0].name, name); doc.fans.forEach(function(doc) { assert.ok(doc instanceof U); @@ -261,7 +269,7 @@ describe('model: populate:', function() { doc._creator = creator; var creatorId = doc._creator._id; assert.ok(creatorId); - assert.equal(name, doc._creator.name); + assert.equal(doc._creator.name, name); assert.ok(doc._creator instanceof U); var fan2Id = doc.fans[2]._id; @@ -270,20 +278,26 @@ describe('model: populate:', function() { name = 'user1a'; var user1a = userLiteral(name); doc.embed[0].array.set(0, user1a); - assert.equal(name, doc.embed[0].array[0].name); + assert.equal(doc.embed[0].array[0].name, name); var user1aId = doc.embed[0].array[0]._id; name = 'user1b'; var user1b = userLiteral(name); doc.embed[0].other = user1b; - assert.equal(name, doc.embed[0].other.name); + assert.equal(doc.embed[0].other.name, name); var user1bId = doc.embed[0].other._id; + name = 'user1c'; + var user1c = userLiteral(name); + doc.embed[0].nested = [{subdoc: user1c}]; + assert.equal(doc.embed[0].nested[0].subdoc.name, name); + var user1cId = doc.embed[0].nested[0].subdoc._id; + // embedded without declared ref in schema name = 'user2a'; var user2a = userLiteral(name); doc.adhoc[0].subdoc = user2a; - assert.equal(name, doc.adhoc[0].subdoc.name); + assert.equal(doc.adhoc[0].subdoc.name, name); var user2aId = doc.adhoc[0].subdoc._id; name = 'user2b'; @@ -297,12 +311,13 @@ describe('model: populate:', function() { B.findById(b2).exec(function(err, doc) { // db is closed in after() assert.ifError(err); - assert.equal(6, doc.fans.length); + assert.equal(doc.fans.length, 6); assert.equal(String(doc._creator), creatorId); assert.equal(doc.fans[2], String(fan2Id)); assert.equal(doc.fans[5], String(fan5Id)); assert.equal(doc.embed[0].array[0], String(user1aId)); assert.equal(doc.embed[0].other, String(user1bId)); + assert.equal(doc.embed[0].nested[0].subdoc, String(user1cId)); assert.equal(doc.adhoc[0].subdoc, String(user2aId)); assert.equal(doc.adhoc[0].subarray[0].things[1], String(user2bId)); done(); @@ -310,7 +325,6 @@ describe('model: populate:', function() { }); }); }); - }); }); }); diff --git a/test/model.populate.test.js b/test/model.populate.test.js index bfb706a3962..1271d2b3364 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -1,91 +1,94 @@ - /** * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , utils = require('../lib/utils') - , random = utils.random - , Schema = mongoose.Schema - , ObjectId = Schema.ObjectId - , DocObjectId = mongoose.Types.ObjectId; - -/** - * Setup. - */ +var _ = require('lodash'); +var start = require('./common'); +var async = require('async'); +var assert = require('power-assert'); +var mongoose = start.mongoose; +var utils = require('../lib/utils'); +var random = utils.random; +var Schema = mongoose.Schema; +var ObjectId = Schema.ObjectId; +var DocObjectId = mongoose.Types.ObjectId; /** - * User schema. + * Tests. */ -var User = new Schema({ - name : String - , email : String - , gender : { type: String, enum: ['male', 'female'], default: 'male' } - , age : { type: Number, default: 21 } - , blogposts : [{ type: ObjectId, ref: 'RefBlogPost' }] - , followers : [{ type: ObjectId, ref: 'RefUser' }] -}); +describe('model: populate:', function() { + this.timeout(process.env.TRAVIS ? 8000 : 4500); -/** - * Comment subdocument schema. - */ + var User; + var Comment; + var BlogPost; + var posts; + var users; -var Comment = new Schema({ - asers : [{ type: ObjectId, ref: 'RefUser' }] - , _creator : { type: ObjectId, ref: 'RefUser' } - , content : String -}); + before(function() { + User = new Schema({ + name: String, + email: String, + gender: {type: String, enum: ['male', 'female'], default: 'male'}, + age: {type: Number, default: 21}, + blogposts: [{type: ObjectId, ref: 'RefBlogPost'}], + followers: [{type: ObjectId, ref: 'RefUser'}] + }); -/** - * Blog post schema. - */ + /** + * Comment subdocument schema. + */ -var BlogPost = new Schema({ - _creator : { type: ObjectId, ref: 'RefUser' } - , title : String - , comments : [Comment] - , fans : [{ type: ObjectId, ref: 'RefUser' }] -}); + Comment = new Schema({ + asers: [{type: ObjectId, ref: 'RefUser'}], + _creator: {type: ObjectId, ref: 'RefUser'}, + content: String + }); -var posts = 'blogposts_' + random() - , users = 'users_' + random(); + /** + * Blog post schema. + */ -mongoose.model('RefBlogPost', BlogPost); -mongoose.model('RefUser', User); -mongoose.model('RefAlternateUser', User); + BlogPost = new Schema({ + _creator: {type: ObjectId, ref: 'RefUser'}, + title: String, + comments: [Comment], + fans: [{type: ObjectId, ref: 'RefUser'}] + }); + posts = 'blogposts_' + random(); + users = 'users_' + random(); -/** - * Tests. - */ + mongoose.model('RefBlogPost', BlogPost); + mongoose.model('RefUser', User); + mongoose.model('RefAlternateUser', User); + }); -describe('model: populate:', function() { it('populating array of object', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); - User.create({ name: 'User 1' }, function(err, user1) { + User.create({name: 'User 1'}, function(err, user1) { assert.ifError(err); - User.create({ name: 'User 2' }, function(err, user2) { + User.create({name: 'User 2'}, function(err, user2) { assert.ifError(err); BlogPost.create({ - title: 'Woot' - , _creator: user1._id - , comments: [ - { _creator: user1._id, content: 'Woot woot' } - , { _creator: user2._id, content: 'Wha wha' } + title: 'Woot', + _creator: user1._id, + comments: [ + {_creator: user1._id, content: 'Woot woot'}, + {_creator: user2._id, content: 'Wha wha'} ] }, function(err, post) { assert.ifError(err); assert.doesNotThrow(function() { - post.populate('comments', function() {}); + post.populate('comments', function() { + }); }); db.close(done); }); @@ -93,58 +96,58 @@ describe('model: populate:', function() { }); }); - it('deep population', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + it('deep population (gh-3103)', function(done) { + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); - User.create({ name: 'User 01' }, function(err, user1) { + User.create({name: 'User 01'}, function(err, user1) { assert.ifError(err); - User.create({ name: 'User 02', followers: [user1._id] }, function(err, user2) { + User.create({name: 'User 02', followers: [user1._id]}, function(err, user2) { assert.ifError(err); - User.create({ name: 'User 03', followers: [user2._id] }, function(err, user3) { + User.create({name: 'User 03', followers: [user2._id]}, function(err, user3) { assert.ifError(err); BlogPost.create({ - title: 'w00tabulous' - , _creator: user3._id + title: 'w00tabulous', + _creator: user3._id }, function(err, post) { assert.ifError(err); assert.doesNotThrow(function() { BlogPost - .findById(post._id) - .select('_creator') - .populate({ - path: '_creator', - model: 'RefUser', + .findById(post._id) + .select('_creator') + .populate({ + path: '_creator', + model: 'RefUser', + select: 'name followers', + populate: [{ + path: 'followers', select: 'name followers', - populate: [{ + options: {limit: 5}, + populate: { // can also use a single object instead of array of objects path: 'followers', - select: 'name followers', - options: { limit: 5 }, - populate: { // can also use a single object instead of array of objects - path: 'followers', - select: 'name', - options: { limit: 2 } - } - }] - }) - .exec(function(err, post) { - db.close(); - assert.ifError(err); - assert.ok(post._creator); - assert.equal(post._creator.name,'User 03'); - assert.ok(post._creator.followers); - assert.ok(post._creator.followers[0]); - assert.equal(post._creator.followers[0].name,'User 02'); - assert.ok(post._creator.followers[0].followers); - assert.ok(post._creator.followers[0].followers[0]); - assert.equal(post._creator.followers[0].followers[0].name,'User 01'); - done(); - }); + select: 'name', + options: {limit: 2} + } + }] + }) + .exec(function(err, post) { + db.close(); + assert.ifError(err); + assert.ok(post._creator); + assert.equal(post._creator.name, 'User 03'); + assert.ok(post._creator.followers); + assert.ok(post._creator.followers[0]); + assert.equal(post._creator.followers[0].name, 'User 02'); + assert.ok(post._creator.followers[0].followers); + assert.ok(post._creator.followers[0].followers[0]); + assert.equal(post._creator.followers[0].followers[0].name, 'User 01'); + done(); + }); }); }); }); @@ -152,20 +155,145 @@ describe('model: populate:', function() { }); }); + describe('deep populate', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + it('deep population with refs (gh-3507)', function(done) { + // handler schema + var handlerSchema = new Schema({ + name: String + }); + + // task schema + var taskSchema = new Schema({ + name: String, + handler: {type: Schema.Types.ObjectId, ref: 'gh3507_0'} + }); + + // application schema + var applicationSchema = new Schema({ + name: String, + tasks: [{type: Schema.Types.ObjectId, ref: 'gh3507_1'}] + }); + + var Handler = db.model('gh3507_0', handlerSchema); + var Task = db.model('gh3507_1', taskSchema); + var Application = db.model('gh3507_2', applicationSchema); + + Handler.create({name: 'test'}, function(error, doc) { + assert.ifError(error); + Task.create({name: 'test2', handler: doc._id}, function(error, doc) { + assert.ifError(error); + var obj = {name: 'test3', tasks: [doc._id]}; + Application.create(obj, function(error, doc) { + assert.ifError(error); + test(doc._id); + }); + }); + }); + + function test(id) { + Application. + findById(id). + populate([ + {path: 'tasks', populate: {path: 'handler'}} + ]). + exec(function(error, doc) { + assert.ifError(error); + assert.ok(doc.tasks[0].handler._id); + done(); + }); + } + }); + + it('multiple paths with same options (gh-3808)', function(done) { + var companySchema = new Schema({ + name: String, + description: String + }); + + var userSchema = new Schema({ + name: String, + company: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + select: false + } + }); + + var messageSchema = new Schema({ + message: String, + author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + target: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } + }); + + var Company = db.model('Company', companySchema); + var User = db.model('User', userSchema); + var Message = db.model('Message', messageSchema); + + var company = new Company({ name: 'IniTech' }); + var user1 = new User({ name: 'Bill', company: company._id }); + var user2 = new User({ name: 'Peter', company: company._id }); + var message = new Message({ + message: 'Problems with TPS Report', + author: user1._id, + target: user2._id + }); + + company.save(function(error) { + assert.ifError(error); + User.create(user1, user2, function(error) { + assert.ifError(error); + message.save(function(error) { + assert.ifError(error); + next(); + }); + }); + }); + + function next() { + Message.findOne({ _id: message._id }, function(error, message) { + assert.ifError(error); + var options = { + path: 'author target', + select: '_id name company', + populate: { + path: 'company', + model: 'Company' + } + }; + message.populate(options, function(error) { + assert.ifError(error); + assert.equal(message.target.company.name, 'IniTech'); + done(); + }); + }); + } + }); + }); + it('populating a single ref', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Guillermo' - , email : 'rauchg@gmail.com' + name: 'Guillermo', + email: 'rauchg@gmail.com' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'woot' - , _creator : creator + title: 'woot', + _creator: creator }, function(err, post) { assert.ifError(err); @@ -185,12 +313,12 @@ describe('model: populate:', function() { }); it('not failing on null as ref', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts); + var db = start(), + BlogPost = db.model('RefBlogPost', posts); BlogPost.create({ - title : 'woot' - , _creator : null + title: 'woot', + _creator: null }, function(err, post) { assert.ifError(err); @@ -207,37 +335,37 @@ describe('model: populate:', function() { }); it('not failing on empty object as ref', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts); + var db = start(), + BlogPost = db.model('RefBlogPost', posts); BlogPost.create( - { title : 'woot' }, - function(err, post) { - assert.ifError(err); + {title: 'woot'}, + function(err, post) { + assert.ifError(err); - BlogPost. - findByIdAndUpdate(post._id, { $set: { _creator: {} } }, function(err) { - assert.ok(err); - db.close(done); - }); - }); + BlogPost. + findByIdAndUpdate(post._id, {$set: {_creator: {}}}, function(err) { + assert.ok(err); + db.close(done); + }); + }); }); it('across DBs', function(done) { - var db = start() - , db2 = db.useDb('mongoose_test2') - , BlogPost = db.model('RefBlogPost', posts + '2') - , User = db2.model('RefUser', users + '2'); + var db = start(), + db2 = db.useDb('mongoose_test2'), + BlogPost = db.model('RefBlogPost', posts + '2'), + User = db2.model('RefUser', users + '2'); User.create({ - name: 'Guillermo' - , email: 'rauchg@gmail.com' + name: 'Guillermo', + email: 'rauchg@gmail.com' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'woot' - , _creator : creator._id + title: 'woot', + _creator: creator._id }, function(err, post) { assert.ifError(err); BlogPost @@ -248,7 +376,7 @@ describe('model: populate:', function() { db.close(); db2.close(); assert.ifError(err); - assert.ok(post._creator.name == 'Guillermo'); + assert.ok(post._creator.name === 'Guillermo'); done(); }); }); @@ -257,32 +385,32 @@ describe('model: populate:', function() { }); it('an error in single ref population propagates', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts + '1') - , User = db.model('RefUser', users + '1'); + var db = start(), + BlogPost = db.model('RefBlogPost', posts + '1'), + User = db.model('RefUser', users + '1'); User.create({ - name: 'Guillermo' - , email: 'rauchg@gmail.com' + name: 'Guillermo', + email: 'rauchg@gmail.com' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'woot' - , _creator : creator + title: 'woot', + _creator: creator }, function(err, post) { assert.ifError(err); - var origFind = User.find; + var origExec = User.Query.prototype.exec; // mock an error - User.find = function() { + User.Query.prototype.exec = function() { var args = Array.prototype.map.call(arguments, function(arg) { - return 'function' == typeof arg ? function() { + return typeof arg === 'function' ? function() { arg(new Error('woot')); } : arg; }); - return origFind.apply(this, args); + return origExec.apply(this, args); }; BlogPost @@ -291,7 +419,8 @@ describe('model: populate:', function() { .exec(function(err) { db.close(); assert.ok(err instanceof Error); - assert.equal('woot', err.message); + assert.equal(err.message, 'woot'); + User.Query.prototype.exec = origExec; done(); }); }); @@ -299,19 +428,19 @@ describe('model: populate:', function() { }); it('populating with partial fields selection', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Guillermo' - , email : 'rauchg@gmail.com' + name: 'Guillermo', + email: 'rauchg@gmail.com' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'woot' - , _creator : creator + title: 'woot', + _creator: creator }, function(err, post) { assert.ifError(err); @@ -323,8 +452,8 @@ describe('model: populate:', function() { assert.ifError(err); assert.ok(post._creator instanceof User); - assert.equal(false, post._creator.isInit('name')); - assert.equal(post._creator.email,'rauchg@gmail.com'); + assert.equal(post._creator.isInit('name'), false); + assert.equal(post._creator.email, 'rauchg@gmail.com'); done(); }); }); @@ -332,32 +461,32 @@ describe('model: populate:', function() { }); it('population of single oid with partial field selection and filter', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', 'blogposts_' + random()) - , User = db.model('RefUser', 'users_' + random()); + var db = start(), + BlogPost = db.model('RefBlogPost', 'blogposts_' + random()), + User = db.model('RefUser', 'users_' + random()); User.create({ - name : 'Banana' - , email : 'cats@example.com' + name: 'Banana', + email: 'cats@example.com' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'woot' - , _creator : creator + title: 'woot', + _creator: creator }, function(err, post) { assert.ifError(err); BlogPost .findById(post._id) - .populate('_creator', 'email', { name: 'Peanut' }) + .populate('_creator', 'email', {name: 'Peanut'}) .exec(function(err, post) { assert.ifError(err); assert.strictEqual(post._creator, null); BlogPost .findById(post._id) - .populate('_creator', 'email', { name: 'Banana' }) + .populate('_creator', 'email', {name: 'Banana'}) .exec(function(err, post) { db.close(); assert.ifError(err); @@ -372,9 +501,9 @@ describe('model: populate:', function() { }); it('population of undefined fields in a collection of docs', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', 'blogposts_' + random()) - , User = db.model('RefUser', 'users_' + random()); + var db = start(), + BlogPost = db.model('RefBlogPost', 'blogposts_' + random()), + User = db.model('RefUser', 'users_' + random()); User.create({ name: 'Eloy', email: 'eloytoro@gmail.com' @@ -397,8 +526,9 @@ describe('model: populate:', function() { .exec(function(err, posts) { db.close(); posts.forEach(function(post) { - if ('_creator' in post) + if ('_creator' in post) { assert.ok(post._creator !== null); + } }); done(); }); @@ -407,20 +537,72 @@ describe('model: populate:', function() { }); }); + it('undefined for nested paths (gh-3859)', function(done) { + var db = start(); + + var companySchema = new mongoose.Schema({ + name: String, + description: String + }); + + var userSchema = new mongoose.Schema({ + name: String, + company: {type: mongoose.Schema.Types.ObjectId, ref: 'Company'} + }); + + var sampleSchema = new mongoose.Schema({ + items: [userSchema] + }); + + var Company = db.model('gh3859_0', companySchema); + var User = db.model('gh3859_1', userSchema); + var Sample = db.model('gh3859_2', sampleSchema); + + var company = new Company({name: 'Reynholm Industrie'}); + var user1 = new User({name: 'Douglas', company: company._id}); + var user2 = new User({name: 'Lambda'}); + var sample = new Sample({ + items: [user1, user2] + }); + + company.save(function(error) { + assert.ifError(error); + User.create(user1, user2, function(error) { + assert.ifError(error); + sample.save(function(error) { + assert.ifError(error); + next(); + }); + }); + }); + + function next() { + Sample.findOne({}, function(error, sample) { + assert.ifError(error); + var opts = { path: 'items.company', options: { lean: true } }; + Company.populate(sample, opts, function(error) { + assert.ifError(error); + assert.strictEqual(sample.items[1].company, void 0); + db.close(done); + }); + }); + } + }); + it('population and changing a reference', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Guillermo' - , email : 'rauchg@gmail.com' + name: 'Guillermo', + email: 'rauchg@gmail.com' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'woot' - , _creator : creator + title: 'woot', + _creator: creator }, function(err, post) { assert.ifError(err); @@ -431,12 +613,12 @@ describe('model: populate:', function() { assert.ifError(err); assert.ok(post._creator instanceof User); - assert.equal(post._creator.name,'Guillermo'); - assert.equal(post._creator.email,'rauchg@gmail.com'); + assert.equal(post._creator.name, 'Guillermo'); + assert.equal(post._creator.email, 'rauchg@gmail.com'); User.create({ - name : 'Aaron' - , email : 'aaron.heckmann@gmail.com' + name: 'Aaron', + email: 'aaron.heckmann@gmail.com' }, function(err, newCreator) { assert.ifError(err); @@ -452,8 +634,8 @@ describe('model: populate:', function() { .exec(function(err, post) { db.close(); assert.ifError(err); - assert.equal(post._creator.name,'Aaron'); - assert.equal(post._creator.email,'aaron.heckmann@gmail.com'); + assert.equal(post._creator.name, 'Aaron'); + assert.equal(post._creator.email, 'aaron.heckmann@gmail.com'); done(); }); }); @@ -464,19 +646,19 @@ describe('model: populate:', function() { }); it('populating with partial fields selection and changing ref', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Guillermo' - , email : 'rauchg@gmail.com' + name: 'Guillermo', + email: 'rauchg@gmail.com' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'woot' - , _creator : creator + title: 'woot', + _creator: creator }, function(err, post) { assert.ifError(err); @@ -487,11 +669,11 @@ describe('model: populate:', function() { assert.ifError(err); assert.ok(post._creator instanceof User); - assert.equal(post._creator.name,'Guillermo'); + assert.equal(post._creator.name, 'Guillermo'); User.create({ - name : 'Aaron' - , email : 'aaron@learnboost.com' + name: 'Aaron', + email: 'aaron@learnboost.com' }, function(err, newCreator) { assert.ifError(err); @@ -506,7 +688,7 @@ describe('model: populate:', function() { db.close(); assert.ifError(err); - assert.equal(post._creator.name,'Aaron'); + assert.equal(post._creator.name, 'Aaron'); assert.ok(!post._creator.email); done(); }); @@ -518,50 +700,50 @@ describe('model: populate:', function() { }); it('populating an array of refs and fetching many', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' + name: 'Fan 1', + email: 'fan1@learnboost.com' }, function(err, fan1) { assert.ifError(err); User.create({ - name : 'Fan 2' - , email : 'fan2@learnboost.com' + name: 'Fan 2', + email: 'fan2@learnboost.com' }, function(err, fan2) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan1, fan2] + title: 'Woot', + fans: [fan1, fan2] }, function(err, post1) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan2, fan1] + title: 'Woot', + fans: [fan2, fan1] }, function(err, post2) { assert.ifError(err); BlogPost - .find({ _id: { $in: [post1._id, post2._id ] } }) + .find({_id: {$in: [post1._id, post2._id]}}) .populate('fans') .exec(function(err, blogposts) { db.close(); assert.ifError(err); - assert.equal(blogposts[0].fans[0].name,'Fan 1'); - assert.equal(blogposts[0].fans[0].email,'fan1@learnboost.com'); - assert.equal(blogposts[0].fans[1].name,'Fan 2'); - assert.equal(blogposts[0].fans[1].email,'fan2@learnboost.com'); + assert.equal(blogposts[0].fans[0].name, 'Fan 1'); + assert.equal(blogposts[0].fans[0].email, 'fan1@learnboost.com'); + assert.equal(blogposts[0].fans[1].name, 'Fan 2'); + assert.equal(blogposts[0].fans[1].email, 'fan2@learnboost.com'); - assert.equal(blogposts[1].fans[0].name,'Fan 2'); - assert.equal(blogposts[1].fans[0].email,'fan2@learnboost.com'); - assert.equal(blogposts[1].fans[1].name,'Fan 1'); - assert.equal(blogposts[1].fans[1].email,'fan1@learnboost.com'); + assert.equal(blogposts[1].fans[0].name, 'Fan 2'); + assert.equal(blogposts[1].fans[0].email, 'fan2@learnboost.com'); + assert.equal(blogposts[1].fans[1].name, 'Fan 1'); + assert.equal(blogposts[1].fans[1].email, 'fan1@learnboost.com'); done(); }); }); @@ -571,53 +753,54 @@ describe('model: populate:', function() { }); it('an error in array reference population propagates', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts + '2') - , User = db.model('RefUser', users + '2'); + var db = start(), + BlogPost = db.model('RefBlogPost', posts + '2'), + User = db.model('RefUser', users + '2'); User.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' + name: 'Fan 1', + email: 'fan1@learnboost.com' }, function(err, fan1) { assert.ifError(err); User.create({ - name : 'Fan 2' - , email : 'fan2@learnboost.com' + name: 'Fan 2', + email: 'fan2@learnboost.com' }, function(err, fan2) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan1, fan2] + title: 'Woot', + fans: [fan1, fan2] }, function(err, post1) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan2, fan1] + title: 'Woot', + fans: [fan2, fan1] }, function(err, post2) { assert.ifError(err); // mock an error - var origFind = User.find; - User.find = function() { + var origExec = User.Query.prototype.exec; + User.Query.prototype.exec = function() { var args = Array.prototype.map.call(arguments, function(arg) { - return 'function' == typeof arg ? function() { + return typeof arg === 'function' ? function() { arg(new Error('woot 2')); } : arg; }); - return origFind.apply(this, args); + return origExec.apply(this, args); }; BlogPost - .find({ $or: [{ _id: post1._id }, { _id: post2._id }] }) + .find({$or: [{_id: post1._id}, {_id: post2._id}]}) .populate('fans') .exec(function(err) { db.close(); assert.ok(err instanceof Error); - assert.equal(err.message,'woot 2'); + assert.equal(err.message, 'woot 2'); + User.Query.prototype.exec = origExec; done(); }); }); @@ -627,42 +810,42 @@ describe('model: populate:', function() { }); it('populating an array of references with fields selection', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' + name: 'Fan 1', + email: 'fan1@learnboost.com' }, function(err, fan1) { assert.ifError(err); User.create({ - name : 'Fan 2' - , email : 'fan2@learnboost.com' + name: 'Fan 2', + email: 'fan2@learnboost.com' }, function(err, fan2) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan1, fan2] + title: 'Woot', + fans: [fan1, fan2] }, function(err, post1) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan2, fan1] + title: 'Woot', + fans: [fan2, fan1] }, function(err, post2) { assert.ifError(err); BlogPost - .find({ _id: { $in: [post1._id, post2._id ] } }) + .find({_id: {$in: [post1._id, post2._id]}}) .populate('fans', 'name') .exec(function(err, blogposts) { db.close(); assert.ifError(err); - assert.equal(blogposts[0].fans[0].name,'Fan 1'); + assert.equal(blogposts[0].fans[0].name, 'Fan 1'); assert.equal(blogposts[0].fans[0].isInit('email'), false); assert.equal(blogposts[0].fans[1].name, 'Fan 2'); assert.equal(blogposts[0].fans[1].isInit('email'), false); @@ -682,84 +865,83 @@ describe('model: populate:', function() { }); it('populating an array of references and filtering', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' + name: 'Fan 1', + email: 'fan1@learnboost.com' }, function(err, fan1) { assert.ifError(err); User.create({ - name : 'Fan 2' - , email : 'fan2@learnboost.com' - , gender : 'female' + name: 'Fan 2', + email: 'fan2@learnboost.com', + gender: 'female' }, function(err, fan2) { assert.ifError(err); User.create({ - name : 'Fan 3' - , email : 'fan3@learnboost.com' - , gender : 'female' + name: 'Fan 3', + email: 'fan3@learnboost.com', + gender: 'female' }, function(err, fan3) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan1, fan2, fan3] + title: 'Woot', + fans: [fan1, fan2, fan3] }, function(err, post1) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan3, fan2, fan1] + title: 'Woot', + fans: [fan3, fan2, fan1] }, function(err, post2) { assert.ifError(err); BlogPost - .find({ _id: { $in: [post1._id, post2._id ] } }) - .populate('fans', '', { gender: 'female', _id: { $in: [fan2] }}) + .find({_id: {$in: [post1._id, post2._id]}}) + .populate('fans', '', {gender: 'female', _id: {$in: [fan2]}}) .exec(function(err, blogposts) { assert.ifError(err); assert.equal(blogposts[0].fans.length, 1); assert.equal(blogposts[0].fans[0].gender, 'female'); - assert.equal(blogposts[0].fans[0].name,'Fan 2'); - assert.equal(blogposts[0].fans[0].email,'fan2@learnboost.com'); + assert.equal(blogposts[0].fans[0].name, 'Fan 2'); + assert.equal(blogposts[0].fans[0].email, 'fan2@learnboost.com'); assert.equal(blogposts[1].fans.length, 1); - assert.equal(blogposts[1].fans[0].gender,'female'); - assert.equal(blogposts[1].fans[0].name,'Fan 2'); - assert.equal(blogposts[1].fans[0].email,'fan2@learnboost.com'); + assert.equal(blogposts[1].fans[0].gender, 'female'); + assert.equal(blogposts[1].fans[0].name, 'Fan 2'); + assert.equal(blogposts[1].fans[0].email, 'fan2@learnboost.com'); BlogPost - .find({ _id: { $in: [post1._id, post2._id ] } }) - .populate('fans', false, { gender: 'female' }) + .find({_id: {$in: [post1._id, post2._id]}}) + .populate('fans', false, {gender: 'female'}) .exec(function(err, blogposts) { db.close(); assert.ifError(err); assert.strictEqual(blogposts[0].fans.length, 2); - assert.equal(blogposts[0].fans[0].gender,'female'); - assert.equal(blogposts[0].fans[0].name,'Fan 2'); - assert.equal(blogposts[0].fans[0].email,'fan2@learnboost.com'); - assert.equal(blogposts[0].fans[1].gender,'female'); - assert.equal(blogposts[0].fans[1].name,'Fan 3'); - assert.equal(blogposts[0].fans[1].email,'fan3@learnboost.com'); + assert.equal(blogposts[0].fans[0].gender, 'female'); + assert.equal(blogposts[0].fans[0].name, 'Fan 2'); + assert.equal(blogposts[0].fans[0].email, 'fan2@learnboost.com'); + assert.equal(blogposts[0].fans[1].gender, 'female'); + assert.equal(blogposts[0].fans[1].name, 'Fan 3'); + assert.equal(blogposts[0].fans[1].email, 'fan3@learnboost.com'); assert.strictEqual(blogposts[1].fans.length, 2); - assert.equal(blogposts[1].fans[0].gender,'female'); - assert.equal(blogposts[1].fans[0].name,'Fan 3'); - assert.equal(blogposts[1].fans[0].email,'fan3@learnboost.com'); - assert.equal(blogposts[1].fans[1].gender,'female'); - assert.equal(blogposts[1].fans[1].name,'Fan 2'); - assert.equal(blogposts[1].fans[1].email,'fan2@learnboost.com'); + assert.equal(blogposts[1].fans[0].gender, 'female'); + assert.equal(blogposts[1].fans[0].name, 'Fan 3'); + assert.equal(blogposts[1].fans[0].email, 'fan3@learnboost.com'); + assert.equal(blogposts[1].fans[1].gender, 'female'); + assert.equal(blogposts[1].fans[1].name, 'Fan 2'); + assert.equal(blogposts[1].fans[1].email, 'fan2@learnboost.com'); done(); }); - }); }); }); @@ -769,71 +951,71 @@ describe('model: populate:', function() { }); it('populating an array of references and multi-filtering', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' + name: 'Fan 1', + email: 'fan1@learnboost.com' }, function(err, fan1) { assert.ifError(err); User.create({ - name : 'Fan 2' - , email : 'fan2@learnboost.com' - , gender : 'female' + name: 'Fan 2', + email: 'fan2@learnboost.com', + gender: 'female' }, function(err, fan2) { assert.ifError(err); User.create({ - name : 'Fan 3' - , email : 'fan3@learnboost.com' - , gender : 'female' - , age : 25 + name: 'Fan 3', + email: 'fan3@learnboost.com', + gender: 'female', + age: 25 }, function(err, fan3) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan1, fan2, fan3] + title: 'Woot', + fans: [fan1, fan2, fan3] }, function(err, post1) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan3, fan2, fan1] + title: 'Woot', + fans: [fan3, fan2, fan1] }, function(err, post2) { assert.ifError(err); BlogPost - .find({ _id: { $in: [post1._id, post2._id ] } }) - .populate('fans', undefined, { _id: fan3 }) + .find({_id: {$in: [post1._id, post2._id]}}) + .populate('fans', undefined, {_id: fan3}) .exec(function(err, blogposts) { assert.ifError(err); assert.equal(blogposts[0].fans.length, 1); - assert.equal(blogposts[0].fans[0].gender,'female'); - assert.equal(blogposts[0].fans[0].name,'Fan 3'); - assert.equal(blogposts[0].fans[0].email,'fan3@learnboost.com'); + assert.equal(blogposts[0].fans[0].gender, 'female'); + assert.equal(blogposts[0].fans[0].name, 'Fan 3'); + assert.equal(blogposts[0].fans[0].email, 'fan3@learnboost.com'); assert.equal(blogposts[0].fans[0].age, 25); - assert.equal(blogposts[1].fans.length,1); - assert.equal(blogposts[1].fans[0].gender,'female'); - assert.equal(blogposts[1].fans[0].name,'Fan 3'); - assert.equal(blogposts[1].fans[0].email,'fan3@learnboost.com'); + assert.equal(blogposts[1].fans.length, 1); + assert.equal(blogposts[1].fans[0].gender, 'female'); + assert.equal(blogposts[1].fans[0].name, 'Fan 3'); + assert.equal(blogposts[1].fans[0].email, 'fan3@learnboost.com'); assert.equal(blogposts[1].fans[0].age, 25); BlogPost - .find({ _id: { $in: [post1._id, post2._id ] } }) - .populate('fans', 0, { gender: 'female' }) + .find({_id: {$in: [post1._id, post2._id]}}) + .populate('fans', 0, {gender: 'female'}) .exec(function(err, blogposts) { db.close(); assert.ifError(err); assert.equal(blogposts[0].fans.length, 2); - assert.equal(blogposts[0].fans[0].gender,'female'); - assert.equal(blogposts[0].fans[0].name,'Fan 2'); + assert.equal(blogposts[0].fans[0].gender, 'female'); + assert.equal(blogposts[0].fans[0].name, 'Fan 2'); assert.equal(blogposts[0].fans[0].email, 'fan2@learnboost.com'); assert.equal(blogposts[0].fans[1].gender, 'female'); assert.equal(blogposts[0].fans[1].name, 'Fan 3'); @@ -860,60 +1042,60 @@ describe('model: populate:', function() { }); it('populating an array of references and multi-filtering with field selection', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' + name: 'Fan 1', + email: 'fan1@learnboost.com' }, function(err, fan1) { assert.ifError(err); User.create({ - name : 'Fan 2' - , email : 'fan2@learnboost.com' - , gender : 'female' + name: 'Fan 2', + email: 'fan2@learnboost.com', + gender: 'female' }, function(err, fan2) { assert.ifError(err); User.create({ - name : 'Fan 3' - , email : 'fan3@learnboost.com' - , gender : 'female' - , age : 25 + name: 'Fan 3', + email: 'fan3@learnboost.com', + gender: 'female', + age: 25 }, function(err, fan3) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan1, fan2, fan3] + title: 'Woot', + fans: [fan1, fan2, fan3] }, function(err, post1) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan3, fan2, fan1] + title: 'Woot', + fans: [fan3, fan2, fan1] }, function(err, post2) { assert.ifError(err); BlogPost - .find({ _id: { $in: [post1._id, post2._id ] } }) - .populate('fans', 'name email', { gender: 'female', age: 25 }) + .find({_id: {$in: [post1._id, post2._id]}}) + .populate('fans', 'name email', {gender: 'female', age: 25}) .exec(function(err, blogposts) { db.close(); assert.ifError(err); assert.strictEqual(blogposts[0].fans.length, 1); - assert.equal(blogposts[0].fans[0].name,'Fan 3'); - assert.equal(blogposts[0].fans[0].email,'fan3@learnboost.com'); + assert.equal(blogposts[0].fans[0].name, 'Fan 3'); + assert.equal(blogposts[0].fans[0].email, 'fan3@learnboost.com'); assert.equal(blogposts[0].fans[0].isInit('email'), true); assert.equal(blogposts[0].fans[0].isInit('gender'), false); assert.equal(blogposts[0].fans[0].isInit('age'), false); assert.strictEqual(blogposts[1].fans.length, 1); - assert.equal(blogposts[1].fans[0].name,'Fan 3'); - assert.equal(blogposts[1].fans[0].email,'fan3@learnboost.com'); + assert.equal(blogposts[1].fans[0].name, 'Fan 3'); + assert.equal(blogposts[1].fans[0].email, 'fan3@learnboost.com'); assert.equal(blogposts[1].fans[0].isInit('email'), true); assert.equal(blogposts[1].fans[0].isInit('gender'), false); assert.equal(blogposts[1].fans[0].isInit('age'), false); @@ -928,49 +1110,49 @@ describe('model: populate:', function() { }); it('populating an array of refs changing one and removing one', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' + name: 'Fan 1', + email: 'fan1@learnboost.com' }, { - name : 'Fan 2' - , email : 'fan2@learnboost.com' + name: 'Fan 2', + email: 'fan2@learnboost.com' }, { - name : 'Fan 3' - , email : 'fan3@learnboost.com' + name: 'Fan 3', + email: 'fan3@learnboost.com' }, { - name : 'Fan 4' - , email : 'fan4@learnboost.com' + name: 'Fan 4', + email: 'fan4@learnboost.com' }, function(err, fan1, fan2, fan3, fan4) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan1, fan2] + title: 'Woot', + fans: [fan1, fan2] }, { - title : 'Woot' - , fans : [fan2, fan1] + title: 'Woot', + fans: [fan2, fan1] }, function(err, post1, post2) { assert.ifError(err); BlogPost - .find({ _id: { $in: [post1._id, post2._id ] } }) + .find({_id: {$in: [post1._id, post2._id]}}) .populate('fans', 'name') .exec(function(err, blogposts) { assert.ifError(err); - assert.equal(blogposts[0].fans[0].name,'Fan 1'); + assert.equal(blogposts[0].fans[0].name, 'Fan 1'); assert.equal(blogposts[0].fans[0].isInit('email'), false); - assert.equal(blogposts[0].fans[1].name,'Fan 2'); + assert.equal(blogposts[0].fans[1].name, 'Fan 2'); assert.equal(blogposts[0].fans[1].isInit('email'), false); - assert.equal(blogposts[1].fans[0].name,'Fan 2'); + assert.equal(blogposts[1].fans[0].name, 'Fan 2'); assert.equal(blogposts[1].fans[0].isInit('email'), false); - assert.equal(blogposts[1].fans[1].name,'Fan 1'); - assert.equal(blogposts[1].fans[1].isInit('email'),false); + assert.equal(blogposts[1].fans[1].name, 'Fan 1'); + assert.equal(blogposts[1].fans[1].isInit('email'), false); blogposts[1].fans = [fan3, fan4]; @@ -978,12 +1160,12 @@ describe('model: populate:', function() { assert.ifError(err); BlogPost - .findById(blogposts[1]._id, '', { populate: ['fans'] }) + .findById(blogposts[1]._id, '', {populate: ['fans']}) .exec(function(err, post) { assert.ifError(err); - assert.equal(post.fans[0].name,'Fan 3'); - assert.equal(post.fans[1].name,'Fan 4'); + assert.equal(post.fans[0].name, 'Fan 3'); + assert.equal(post.fans[1].name, 'Fan 4'); post.fans.splice(0, 1); post.save(function(err) { @@ -995,8 +1177,8 @@ describe('model: populate:', function() { .exec(function(err, post) { db.close(); assert.ifError(err); - assert.equal(post.fans.length,1); - assert.equal(post.fans[0].name,'Fan 4'); + assert.equal(post.fans.length, 1); + assert.equal(post.fans[0].name, 'Fan 4'); done(); }); }); @@ -1009,22 +1191,22 @@ describe('model: populate:', function() { describe('populating sub docs', function() { it('works with findById', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); - User.create({ name: 'User 1' }, function(err, user1) { + User.create({name: 'User 1'}, function(err, user1) { assert.ifError(err); - User.create({ name: 'User 2' }, function(err, user2) { + User.create({name: 'User 2'}, function(err, user2) { assert.ifError(err); BlogPost.create({ - title: 'Woot' - , _creator: user1._id - , comments: [ - { _creator: user1._id, content: 'Woot woot' } - , { _creator: user2._id, content: 'Wha wha' } + title: 'Woot', + _creator: user1._id, + comments: [ + {_creator: user1._id, content: 'Woot woot'}, + {_creator: user2._id, content: 'Wha wha'} ] }, function(err, post) { assert.ifError(err); @@ -1036,9 +1218,9 @@ describe('model: populate:', function() { .exec(function(err, post) { assert.ifError(err); - assert.equal(post._creator.name,'User 1'); - assert.equal(post.comments[0]._creator.name,'User 1'); - assert.equal(post.comments[1]._creator.name,'User 2'); + assert.equal(post._creator.name, 'User 1'); + assert.equal(post.comments[0]._creator.name, 'User 1'); + assert.equal(post.comments[1]._creator.name, 'User 2'); db.close(done); }); }); @@ -1047,41 +1229,41 @@ describe('model: populate:', function() { }); it('works when first doc returned has empty array for populated path (gh-1055)', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); - User.create({ name: 'gh-1055-1' }, { name: 'gh-1055-2' }, function(err, user1, user2) { + User.create({name: 'gh-1055-1'}, {name: 'gh-1055-2'}, function(err, user1, user2) { assert.ifError(err); BlogPost.create({ - title: 'gh-1055 post1' - , _creator: user1._id - , comments: [] - },{ - title: 'gh-1055 post2' - , _creator: user1._id - , comments: [ - { _creator: user1._id, content: 'Woot woot', asers: [] } - , { _creator: user2._id, content: 'Wha wha', asers: [user1, user2] } + title: 'gh-1055 post1', + _creator: user1._id, + comments: [] + }, { + title: 'gh-1055 post2', + _creator: user1._id, + comments: [ + {_creator: user1._id, content: 'Woot woot', asers: []}, + {_creator: user2._id, content: 'Wha wha', asers: [user1, user2]} ] }, function(err) { assert.ifError(err); var ran = false; BlogPost - .find({ title: /gh-1055/ }) + .find({title: /gh-1055/}) .sort('title') .select('comments') .populate('comments._creator') .populate('comments.asers') .exec(function(err, posts) { - assert.equal(false, ran); + assert.equal(ran, false); ran = true; assert.ifError(err); assert.ok(posts.length); assert.ok(posts[1].comments[0]._creator); - assert.equal('gh-1055-1', posts[1].comments[0]._creator.name); + assert.equal(posts[1].comments[0]._creator.name, 'gh-1055-1'); db.close(done); }); }); @@ -1094,7 +1276,7 @@ describe('model: populate:', function() { var BlogPost = db.model('RefBlogPost', posts, 'gh-2176-1'); var User = db.model('RefUser', users, 'gh-2176-2'); - User.create({ name: 'aaron' }, { name: 'val' }, function(err, user1, user2) { + User.create({name: 'aaron'}, {name: 'val'}, function(err, user1, user2) { assert.ifError(err); BlogPost.create( @@ -1103,53 +1285,53 @@ describe('model: populate:', function() { _creator: user1._id, comments: [] }, - function(err) { - assert.ifError(err); - BlogPost. - find({ title: 'gh-2176' }). + function(err) { + assert.ifError(err); + BlogPost. + find({title: 'gh-2176'}). populate('_creator'). exec(function(error, posts) { assert.ifError(error); - assert.equal(1, posts.length); - assert.equal('aaron', posts[0]._creator.name); + assert.equal(posts.length, 1); + assert.equal(posts[0]._creator.name, 'aaron'); posts[0]._creator = user2; - assert.equal('val', posts[0]._creator.name); + assert.equal(posts[0]._creator.name, 'val'); posts[0].save(function(error, post) { assert.ifError(error); - assert.equal('val', post._creator.name); + assert.equal(post._creator.name, 'val'); posts[0].populate('_creator', function(error, doc) { assert.ifError(error); - assert.equal('val', doc._creator.name); + assert.equal(doc._creator.name, 'val'); db.close(done); }); }); }); - }); + }); }); }); it('populating subdocuments partially', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'User 1' - , email : 'user1@learnboost.com' + name: 'User 1', + email: 'user1@learnboost.com' }, function(err, user1) { assert.ifError(err); User.create({ - name : 'User 2' - , email : 'user2@learnboost.com' + name: 'User 2', + email: 'user2@learnboost.com' }, function(err, user2) { assert.ifError(err); BlogPost.create({ - title: 'Woot' - , comments: [ - { _creator: user1, content: 'Woot woot' } - , { _creator: user2, content: 'Wha wha' } + title: 'Woot', + comments: [ + {_creator: user1, content: 'Woot woot'}, + {_creator: user2, content: 'Wha wha'} ] }, function(err, post) { assert.ifError(err); @@ -1161,9 +1343,9 @@ describe('model: populate:', function() { db.close(); assert.ifError(err); - assert.equal(post.comments[0]._creator.email,'user1@learnboost.com'); + assert.equal(post.comments[0]._creator.email, 'user1@learnboost.com'); assert.equal(post.comments[0]._creator.isInit('name'), false); - assert.equal(post.comments[1]._creator.email,'user2@learnboost.com'); + assert.equal(post.comments[1]._creator.email, 'user2@learnboost.com'); assert.equal(post.comments[1]._creator.isInit('name'), false); done(); @@ -1174,41 +1356,41 @@ describe('model: populate:', function() { }); it('populating subdocuments partially with conditions', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'User 1' - , email : 'user1@learnboost.com' + name: 'User 1', + email: 'user1@learnboost.com' }, function(err, user1) { assert.ifError(err); User.create({ - name : 'User 2' - , email : 'user2@learnboost.com' + name: 'User 2', + email: 'user2@learnboost.com' }, function(err, user2) { assert.ifError(err); BlogPost.create({ - title: 'Woot' - , comments: [ - { _creator: user1, content: 'Woot woot' } - , { _creator: user2, content: 'Wha wha' } + title: 'Woot', + comments: [ + {_creator: user1, content: 'Woot woot'}, + {_creator: user2, content: 'Wha wha'} ] }, function(err, post) { assert.ifError(err); BlogPost .findById(post._id) - .populate('comments._creator', {'email': 1}, { name: /User/ }) + .populate('comments._creator', {'email': 1}, {name: /User/}) .exec(function(err, post) { db.close(); assert.ifError(err); - assert.equal(post.comments[0]._creator.email,'user1@learnboost.com'); - assert.equal(post.comments[0]._creator.isInit('name'),false); - assert.equal(post.comments[1]._creator.email,'user2@learnboost.com'); + assert.equal(post.comments[0]._creator.email, 'user1@learnboost.com'); + assert.equal(post.comments[0]._creator.isInit('name'), false); + assert.equal(post.comments[1]._creator.email, 'user2@learnboost.com'); assert.equal(post.comments[1]._creator.isInit('name'), false); done(); @@ -1219,27 +1401,27 @@ describe('model: populate:', function() { }); it('populating subdocs with invalid/missing subproperties', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create({ - name : 'T-100' - , email : 'terminator100@learnboost.com' + name: 'T-100', + email: 'terminator100@learnboost.com' }, function(err) { assert.ifError(err); User.create({ - name : 'T-1000' - , email : 'terminator1000@learnboost.com' + name: 'T-1000', + email: 'terminator1000@learnboost.com' }, function(err, user2) { assert.ifError(err); BlogPost.create({ - title: 'Woot' - , comments: [ - { _creator: null, content: 'Woot woot' } - , { _creator: user2, content: 'Wha wha' } + title: 'Woot', + comments: [ + {_creator: null, content: 'Woot woot'}, + {_creator: user2, content: 'Wha wha'} ] }, function(err, post) { assert.ifError(err); @@ -1253,45 +1435,45 @@ describe('model: populate:', function() { // add a non-schema property to the document. BlogPost.collection.update( - { _id: post._id } - , { $set: { 'comments.0._idontexist': user2._id }}, function(err) { - assert.ifError(err); - - // allow population of unknown property by passing model name. - // helpful when populating mapReduce results too. - BlogPost - .findById(post._id) - .populate('comments._idontexist', 'email', 'RefUser') - .exec(function(err, post) { - assert.ifError(err); - assert.ok(post); - assert.equal(post.comments.length, 2); - assert.ok(post.comments[0].get('_idontexist')); - assert.equal(String(post.comments[0].get('_idontexist')._id), user2.id); - assert.equal(post.comments[0].get('_idontexist').email, 'terminator1000@learnboost.com'); - assert.equal(post.comments[0].get('_idontexist').isInit('name'), false); - assert.strictEqual(post.comments[0]._creator, null); - assert.equal(post.comments[1]._creator.toString(),user2.id); - - // subprop is null in a doc - BlogPost - .findById(post._id) - .populate('comments._creator', 'email') - .exec(function(err, post) { + {_id: post._id} + , {$set: {'comments.0._idontexist': user2._id}}, function(err) { assert.ifError(err); - assert.ok(post.comments); - assert.equal(post.comments.length,2); - assert.strictEqual(post.comments[0]._creator, null); - assert.strictEqual(post.comments[0].content, 'Woot woot'); - assert.equal(post.comments[1]._creator.email,'terminator1000@learnboost.com'); - assert.equal(post.comments[1]._creator.isInit('name'), false); - assert.equal(post.comments[1].content,'Wha wha'); - - db.close(done); + // allow population of unknown property by passing model name. + // helpful when populating mapReduce results too. + BlogPost + .findById(post._id) + .populate('comments._idontexist', 'email', 'RefUser') + .exec(function(err, post) { + assert.ifError(err); + assert.ok(post); + assert.equal(post.comments.length, 2); + assert.ok(post.comments[0].get('_idontexist')); + assert.equal(String(post.comments[0].get('_idontexist')._id), user2.id); + assert.equal(post.comments[0].get('_idontexist').email, 'terminator1000@learnboost.com'); + assert.equal(post.comments[0].get('_idontexist').isInit('name'), false); + assert.strictEqual(post.comments[0]._creator, null); + assert.equal(post.comments[1]._creator.toString(), user2.id); + + // subprop is null in a doc + BlogPost + .findById(post._id) + .populate('comments._creator', 'email') + .exec(function(err, post) { + assert.ifError(err); + + assert.ok(post.comments); + assert.equal(post.comments.length, 2); + assert.strictEqual(post.comments[0]._creator, null); + assert.strictEqual(post.comments[0].content, 'Woot woot'); + assert.equal(post.comments[1]._creator.email, 'terminator1000@learnboost.com'); + assert.equal(post.comments[1]._creator.isInit('name'), false); + assert.equal(post.comments[1].content, 'Wha wha'); + + db.close(done); + }); + }); }); - }); - }); }); }); }); @@ -1311,7 +1493,7 @@ describe('model: populate:', function() { }); var User = db.model('gh-2151-1', user, 'gh-2151-1'); - var blogpost = Schema({ + var blogpost = new Schema({ title: String, tags: [String], author: { @@ -1369,36 +1551,37 @@ describe('model: populate:', function() { assert.ifError(err); BlogPost. - find({ tags: 'fun' }). - lean(). - populate('author'). - exec(function(err, docs) { - assert.ifError(err); - var opts = { - path: 'author.friends', - select: 'name', - options: { limit: 1 } - }; + find({tags: 'fun'}). + lean(). + populate('author'). + exec(function(err, docs) { + assert.ifError(err); + var opts = { + path: 'author.friends', + select: 'name', + options: { limit: 1 } + }; - BlogPost.populate(docs, opts, function(err, docs) { - assert.ifError(err); - assert.equal(2, docs.length); - assert.equal(1, docs[0].author.friends.length); - assert.equal(1, docs[1].author.friends.length); - db.close(done); - }); + BlogPost.populate(docs, opts, function(err, docs) { + assert.ifError(err); + assert.equal(docs.length, 2); + assert.equal(docs[0].author.friends.length, 1); + assert.equal(docs[1].author.friends.length, 1); + assert.equal(opts.options.limit, 1); + db.close(done); }); + }); }); }); }); it('populating subdocuments partially with empty array (gh-481)', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts); + var db = start(), + BlogPost = db.model('RefBlogPost', posts); BlogPost.create({ - title: 'Woot' - , comments: [] // EMPTY ARRAY + title: 'Woot', + comments: [] // EMPTY ARRAY }, function(err, post) { assert.ifError(err); @@ -1408,19 +1591,19 @@ describe('model: populate:', function() { .exec(function(err, returned) { db.close(); assert.ifError(err); - assert.equal(returned.id,post.id); + assert.equal(returned.id, post.id); done(); }); }); }); it('populating subdocuments partially with null array', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts); + var db = start(), + BlogPost = db.model('RefBlogPost', posts); BlogPost.create({ - title: 'Woot' - , comments: null + title: 'Woot', + comments: null }, function(err, post) { assert.ifError(err); @@ -1437,22 +1620,22 @@ describe('model: populate:', function() { }); it('populating subdocuments with array including nulls', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); - var user = new User({ name: 'hans zimmer' }); + var user = new User({name: 'hans zimmer'}); user.save(function(err) { assert.ifError(err); BlogPost.create({ - title: 'Woot' - , fans: [] + title: 'Woot', + fans: [] }, function(err, post) { assert.ifError(err); // shove some uncasted vals - BlogPost.collection.update({ _id: post._id }, { $set: { fans: [null, undefined, user.id, null] } }, function(err) { + BlogPost.collection.update({_id: post._id}, {$set: {fans: [null, undefined, user.id, null]}}, function(err) { assert.ifError(err); BlogPost @@ -1461,7 +1644,7 @@ describe('model: populate:', function() { .exec(function(err, returned) { db.close(); assert.ifError(err); - assert.equal(returned.id,post.id); + assert.equal(returned.id, post.id); assert.equal(returned.fans.length, 1); done(); }); @@ -1471,73 +1654,73 @@ describe('model: populate:', function() { }); it('populating more than one array at a time', function(done) { - var db = start() - , User = db.model('RefUser', users) - , M = db.model('PopMultiSubDocs', new Schema({ - users: [{ type: ObjectId, ref: 'RefUser' }] - , fans: [{ type: ObjectId, ref: 'RefUser' }] - , comments: [Comment] - })); + var db = start(), + User = db.model('RefUser', users), + M = db.model('PopMultiSubDocs', new Schema({ + users: [{type: ObjectId, ref: 'RefUser'}], + fans: [{type: ObjectId, ref: 'RefUser'}], + comments: [Comment] + })); User.create({ - email : 'fan1@learnboost.com' + email: 'fan1@learnboost.com' }, { - name : 'Fan 2' - , email : 'fan2@learnboost.com' - , gender : 'female' + name: 'Fan 2', + email: 'fan2@learnboost.com', + gender: 'female' }, { name: 'Fan 3' }, function(err, fan1, fan2, fan3) { assert.ifError(err); M.create({ - users: [fan3] - , fans: [fan1] - , comments: [ - { _creator: fan1, content: 'bejeah!' } - , { _creator: fan2, content: 'chickfila' } + users: [fan3], + fans: [fan1], + comments: [ + {_creator: fan1, content: 'bejeah!'}, + {_creator: fan2, content: 'chickfila'} ] }, { - users: [fan1] - , fans: [fan2] - , comments: [ - { _creator: fan3, content: 'hello' } - , { _creator: fan1, content: 'world' } + users: [fan1], + fans: [fan2], + comments: [ + {_creator: fan3, content: 'hello'}, + {_creator: fan1, content: 'world'} ] }, function(err, post1, post2) { assert.ifError(err); M.where('_id').in([post1, post2]) - .populate('fans', 'name', { gender: 'female' }) - .populate('users', 'name', { gender: 'male' }) - .populate('comments._creator', 'email', { name: null }) + .populate('fans', 'name', {gender: 'female'}) + .populate('users', 'name', {gender: 'male'}) + .populate('comments._creator', 'email', {name: null}) .exec(function(err, posts) { db.close(); assert.ifError(err); assert.ok(posts); - assert.equal(posts.length,2); + assert.equal(posts.length, 2); var p1 = posts[0]; var p2 = posts[1]; assert.strictEqual(p1.fans.length, 0); assert.strictEqual(p2.fans.length, 1); - assert.equal(p2.fans[0].name,'Fan 2'); + assert.equal(p2.fans[0].name, 'Fan 2'); assert.equal(p2.fans[0].isInit('email'), false); assert.equal(p2.fans[0].isInit('gender'), false); - assert.equal(p1.comments.length,2); - assert.equal(p2.comments.length,2); + assert.equal(p1.comments.length, 2); + assert.equal(p2.comments.length, 2); assert.ok(p1.comments[0]._creator.email); assert.ok(!p2.comments[0]._creator); - assert.equal(p1.comments[0]._creator.email,'fan1@learnboost.com'); - assert.equal(p2.comments[1]._creator.email,'fan1@learnboost.com'); + assert.equal(p1.comments[0]._creator.email, 'fan1@learnboost.com'); + assert.equal(p2.comments[1]._creator.email, 'fan1@learnboost.com'); assert.equal(p1.comments[0]._creator.isInit('name'), false); assert.equal(p2.comments[1]._creator.isInit('name'), false); - assert.equal(p1.comments[0].content,'bejeah!'); - assert.equal(p2.comments[1].content,'world'); + assert.equal(p1.comments[0].content, 'bejeah!'); + assert.equal(p2.comments[1].content, 'world'); assert.ok(!p1.comments[1]._creator); assert.ok(!p2.comments[0]._creator); - assert.equal(p1.comments[1].content,'chickfila'); - assert.equal(p2.comments[0].content,'hello'); + assert.equal(p1.comments[1].content, 'chickfila'); + assert.equal(p2.comments[0].content, 'hello'); done(); }); @@ -1546,13 +1729,13 @@ describe('model: populate:', function() { }); it('populating multiple children of a sub-array at a time', function(done) { - var db = start() - , User = db.model('RefUser', users) - , BlogPost = db.model('RefBlogPost', posts) - , Inner = new Schema({ - user: { type: ObjectId, ref: 'RefUser' } - , post: { type: ObjectId, ref: 'RefBlogPost' } - }); + var db = start(), + User = db.model('RefUser', users), + BlogPost = db.model('RefBlogPost', posts), + Inner = new Schema({ + user: {type: ObjectId, ref: 'RefUser'}, + post: {type: ObjectId, ref: 'RefBlogPost'} + }); db.model('PopMultiChildrenOfSubDocInner', Inner); var M = db.model('PopMultiChildrenOfSubDoc', new Schema({ @@ -1560,34 +1743,34 @@ describe('model: populate:', function() { })); User.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' - , gender : 'male' + name: 'Fan 1', + email: 'fan1@learnboost.com', + gender: 'male' }, { - name : 'Fan 2' - , email : 'fan2@learnboost.com' - , gender : 'female' + name: 'Fan 2', + email: 'fan2@learnboost.com', + gender: 'female' }, function(err, fan1, fan2) { assert.ifError(err); BlogPost.create({ - title : 'woot' + title: 'woot' }, { - title : 'yay' + title: 'yay' }, function(err, post1, post2) { assert.ifError(err); M.create({ kids: [ - { user: fan1, post: post1, y: 5 } - , { user: fan2, post: post2, y: 8 } - ] - , x: 4 + {user: fan1, post: post1, y: 5}, + {user: fan2, post: post2, y: 8} + ], + x: 4 }, function(err, m1) { assert.ifError(err); M.findById(m1) - .populate('kids.user', "name") - .populate('kids.post', "title", { title: "woot" }) + .populate('kids.user', 'name') + .populate('kids.post', 'title', {title: 'woot'}) .exec(function(err, o) { db.close(); assert.ifError(err); @@ -1595,10 +1778,10 @@ describe('model: populate:', function() { var k1 = o.kids[0]; var k2 = o.kids[1]; assert.strictEqual(true, !k2.post); - assert.strictEqual(k1.user.name, "Fan 1"); + assert.strictEqual(k1.user.name, 'Fan 1'); assert.strictEqual(k1.user.email, undefined); - assert.strictEqual(k1.post.title, "woot"); - assert.strictEqual(k2.user.name, "Fan 2"); + assert.strictEqual(k1.post.title, 'woot'); + assert.strictEqual(k2.user.name, 'Fan 2'); done(); }); @@ -1608,63 +1791,63 @@ describe('model: populate:', function() { }); it('passing sort options to the populate method', function(done) { - var db = start() - , P = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + P = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.create( - { name: 'aaron', age: 10 }, - { name: 'fan2', age: 8 }, - { name: 'someone else', age: 3 }, - { name: 'val', age: 3 }, - function(err, fan1, fan2, fan3, fan4) { - assert.ifError(err); - - P.create({ fans: [fan4, fan2, fan3, fan1] }, function(err, post) { + {name: 'aaron', age: 10}, + {name: 'fan2', age: 8}, + {name: 'someone else', age: 3}, + {name: 'val', age: 3}, + function(err, fan1, fan2, fan3, fan4) { assert.ifError(err); - P.findById(post) - .populate('fans', null, null, { sort: { age: 1, name: 1 } }) - .exec(function(err, post) { + P.create({fans: [fan4, fan2, fan3, fan1]}, function(err, post) { assert.ifError(err); - assert.equal(post.fans.length, 4); - assert.equal(post.fans[0].name, 'someone else'); - assert.equal(post.fans[1].name, 'val'); - assert.equal(post.fans[2].name, 'fan2'); - assert.equal(post.fans[3].name, 'aaron'); - P.findById(post) - .populate('fans', 'name', null, { sort: {'name':-1} }) + .populate('fans', null, null, {sort: {age: 1, name: 1}}) .exec(function(err, post) { assert.ifError(err); assert.equal(post.fans.length, 4); - assert.equal(post.fans[3].name,'aaron'); - assert.strictEqual(undefined, post.fans[3].age); - assert.equal(post.fans[2].name,'fan2'); - assert.strictEqual(undefined, post.fans[2].age); - assert.equal(post.fans[1].name,'someone else'); - assert.strictEqual(undefined, post.fans[1].age); - assert.equal(post.fans[0].name, 'val'); - assert.strictEqual(undefined, post.fans[0].age); + assert.equal(post.fans[0].name, 'someone else'); + assert.equal(post.fans[1].name, 'val'); + assert.equal(post.fans[2].name, 'fan2'); + assert.equal(post.fans[3].name, 'aaron'); P.findById(post) - .populate('fans', 'age', { age: { $gt: 3 }}, { sort: {'name': 'desc'} }) + .populate('fans', 'name', null, {sort: {'name': -1}}) .exec(function(err, post) { - db.close(); assert.ifError(err); - assert.equal(post.fans.length,2); - assert.equal(post.fans[1].age.valueOf(),10); - assert.equal(post.fans[0].age.valueOf(),8); + assert.equal(post.fans.length, 4); + assert.equal(post.fans[3].name, 'aaron'); + assert.strictEqual(undefined, post.fans[3].age); + assert.equal(post.fans[2].name, 'fan2'); + assert.strictEqual(undefined, post.fans[2].age); + assert.equal(post.fans[1].name, 'someone else'); + assert.strictEqual(undefined, post.fans[1].age); + assert.equal(post.fans[0].name, 'val'); + assert.strictEqual(undefined, post.fans[0].age); + + P.findById(post) + .populate('fans', 'age', {age: {$gt: 3}}, {sort: {'name': 'desc'}}) + .exec(function(err, post) { + db.close(); + assert.ifError(err); + + assert.equal(post.fans.length, 2); + assert.equal(post.fans[1].age.valueOf(), 10); + assert.equal(post.fans[0].age.valueOf(), 8); - done(); + done(); + }); }); }); }); }); - }); }); it('limit should apply to each returned doc, not in aggregate (gh-1490)', function(done) { @@ -1674,19 +1857,19 @@ describe('model: populate:', function() { }); var name = 'b' + random(); var sJ = new Schema({ - b : [{ type: Schema.Types.ObjectId, ref: name }] + b: [{type: Schema.Types.ObjectId, ref: name}] }); var B = db.model(name, sB); var J = db.model('j' + random(), sJ); - var b1 = new B({ name : 'thing1'}); - var b2 = new B({ name : 'thing2'}); - var b3 = new B({ name : 'thing3'}); - var b4 = new B({ name : 'thing4'}); - var b5 = new B({ name : 'thing5'}); + var b1 = new B({name: 'thing1'}); + var b2 = new B({name: 'thing2'}); + var b3 = new B({name: 'thing3'}); + var b4 = new B({name: 'thing4'}); + var b5 = new B({name: 'thing5'}); - var j1 = new J({ b : [b1.id, b2.id, b5.id]}); - var j2 = new J({ b : [b3.id, b4.id, b5.id]}); + var j1 = new J({b: [b1.id, b2.id, b5.id]}); + var j2 = new J({b: [b3.id, b4.id, b5.id]}); var count = 7; @@ -1699,12 +1882,14 @@ describe('model: populate:', function() { j2.save(cb); function cb(err) { - if (err) throw err; + if (err) { + throw err; + } --count || next(); } function next() { - J.find().populate({ path: 'b', options : { limit : 2 } }).exec(function(err, j) { + J.find().populate({path: 'b', options: {limit: 2}}).exec(function(err, j) { assert.equal(j.length, 2); assert.equal(j[0].b.length, 2); assert.equal(j[1].b.length, 2); @@ -1727,33 +1912,33 @@ describe('model: populate:', function() { var db = start(); var UserSchema = new Schema({ - _id: String - , name: String + _id: String, + name: String }); var NoteSchema = new Schema({ - author: { type: String, ref: 'UserWithStringId' } - , body: String + author: {type: String, ref: 'UserWithStringId'}, + body: String }); var User = db.model('UserWithStringId', UserSchema, random()); var Note = db.model('NoteWithStringId', NoteSchema, random()); - var alice = new User({_id: 'alice', name: "Alice"}); + var alice = new User({_id: 'alice', name: 'Alice'}); alice.save(function(err) { assert.ifError(err); - var note = new Note({author: 'alice', body: "Buy Milk"}); + var note = new Note({author: 'alice', body: 'Buy Milk'}); note.save(function(err) { assert.ifError(err); Note.findById(note.id).populate('author').exec(function(err, note) { db.close(); assert.ifError(err); - assert.equal(note.body,'Buy Milk'); + assert.equal(note.body, 'Buy Milk'); assert.ok(note.author); - assert.equal(note.author.name,'Alice'); + assert.equal(note.author.name, 'Alice'); done(); }); }); @@ -1764,33 +1949,33 @@ describe('model: populate:', function() { var db = start(); var UserSchema = new Schema({ - _id: Number - , name: String + _id: Number, + name: String }); var NoteSchema = new Schema({ - author: { type: Number, ref: 'UserWithNumberId' } - , body: String + author: {type: Number, ref: 'UserWithNumberId'}, + body: String }); var User = db.model('UserWithNumberId', UserSchema, random()); var Note = db.model('NoteWithNumberId', NoteSchema, random()); - var alice = new User({_id: 2359, name: "Alice"}); + var alice = new User({_id: 2359, name: 'Alice'}); alice.save(function(err) { assert.ifError(err); - var note = new Note({author: 2359, body: "Buy Milk"}); + var note = new Note({author: 2359, body: 'Buy Milk'}); note.save(function(err) { assert.ifError(err); Note.findById(note.id).populate('author').exec(function(err, note) { db.close(); assert.ifError(err); - assert.equal(note.body,'Buy Milk'); + assert.equal(note.body, 'Buy Milk'); assert.ok(note.author); - assert.equal(note.author.name,'Alice'); + assert.equal(note.author.name, 'Alice'); done(); }); }); @@ -1805,53 +1990,56 @@ describe('model: populate:', function() { }); var User = db.model('ObjectIdRefRequiredField', userSchema, random()); - var numSchema = new Schema({ _id: Number, val: Number }); + var numSchema = new Schema({_id: Number, val: Number}); var Num = db.model('NumberRefRequired', numSchema, random()); - var strSchema = new Schema({ _id: String, val: String }); + var strSchema = new Schema({_id: String, val: String}); var Str = db.model('StringRefRequired', strSchema, random()); var commentSchema = new Schema({ - user: {type: ObjectId, ref: 'ObjectIdRefRequiredField', required: true} - , num: {type: Number, ref: 'NumberRefRequired', required: true} - , str: {type: String, ref: 'StringRefRequired', required: true} - , text: String + user: {type: ObjectId, ref: 'ObjectIdRefRequiredField', required: true}, + num: {type: Number, ref: 'NumberRefRequired', required: true}, + str: {type: String, ref: 'StringRefRequired', required: true}, + text: String }); var Comment = db.model('CommentWithRequiredField', commentSchema); var pending = 3; - var string = new Str({ _id: 'my string', val: 'hello' }); - var number = new Num({ _id: 1995, val: 234 }); - var user = new User({ email: 'test' }); + var string = new Str({_id: 'my string', val: 'hello'}); + var number = new Num({_id: 1995, val: 234}); + var user = new User({email: 'test'}); string.save(next); number.save(next); user.save(next); function next(err) { - assert.strictEqual(null, err); - if (--pending) return; + assert.strictEqual(err, null); + if (--pending) { + return; + } var comment = new Comment({ text: 'test' }); comment.save(function(err) { - assert.equal('CommentWithRequiredField validation failed', err && err.message); + assert.ok(err); + assert.ok(err.message.indexOf('CommentWithRequiredField validation failed') === 0, err.message); assert.ok('num' in err.errors); assert.ok('str' in err.errors); assert.ok('user' in err.errors); - assert.equal(err.errors.num.kind,'required'); - assert.equal(err.errors.str.kind,'required'); - assert.equal(err.errors.user.kind,'required'); + assert.equal(err.errors.num.kind, 'required'); + assert.equal(err.errors.str.kind, 'required'); + assert.equal(err.errors.user.kind, 'required'); comment.user = user; comment.num = 1995; comment.str = 'my string'; comment.save(function(err, comment) { - assert.strictEqual(null, err); + assert.strictEqual(err, null); Comment .findById(comment.id) @@ -1875,50 +2063,50 @@ describe('model: populate:', function() { }); it('populate works with schemas with both id and _id defined', function(done) { - var db = start() - , S1 = new Schema({ id: String }) - , S2 = new Schema({ things: [{ type: ObjectId, ref: '_idAndid' }]}); + var db = start(), + S1 = new Schema({id: String}), + S2 = new Schema({things: [{type: ObjectId, ref: '_idAndid'}]}); var M1 = db.model('_idAndid', S1); var M2 = db.model('populateWorksWith_idAndidSchemas', S2); M1.create( - { id: "The Tiger That Isn't" } - , { id: "Users Guide To The Universe" } - , function(err, a, b) { - assert.ifError(err); - - var m2 = new M2({ things: [a, b]}); - m2.save(function(err) { + {id: 'The Tiger That Isn\'t'} + , {id: 'Users Guide To The Universe'} + , function(err, a, b) { assert.ifError(err); - M2.findById(m2).populate('things').exec(function(err, doc) { - db.close(); + + var m2 = new M2({things: [a, b]}); + m2.save(function(err) { assert.ifError(err); - assert.equal(doc.things.length,2); - assert.equal(doc.things[0].id,"The Tiger That Isn't"); - assert.equal(doc.things[1].id,"Users Guide To The Universe"); - done(); + M2.findById(m2).populate('things').exec(function(err, doc) { + db.close(); + assert.ifError(err); + assert.equal(doc.things.length, 2); + assert.equal(doc.things[0].id, 'The Tiger That Isn\'t'); + assert.equal(doc.things[1].id, 'Users Guide To The Universe'); + done(); + }); }); }); - }); }); it('Update works with populated arrays (gh-602)', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); - User.create({name:'aphex'},{name:'twin'}, function(err, u1, u2) { + User.create({name: 'aphex'}, {name: 'twin'}, function(err, u1, u2) { assert.ifError(err); BlogPost.create({ - title: 'Woot' - , fans: [] + title: 'Woot', + fans: [] }, function(err, post) { assert.ifError(err); - var update = { fans: [u1, u2] }; - BlogPost.update({ _id: post }, update, function(err) { + var update = {fans: [u1, u2]}; + BlogPost.update({_id: post}, update, function(err) { assert.ifError(err); // the original update doc should not be modified @@ -1930,7 +2118,7 @@ describe('model: populate:', function() { BlogPost.findById(post, function(err, post) { db.close(); assert.ifError(err); - assert.equal(post.fans.length,2); + assert.equal(post.fans.length, 2); assert.ok(post.fans[0] instanceof DocObjectId); assert.ok(post.fans[1] instanceof DocObjectId); done(); @@ -1941,9 +2129,9 @@ describe('model: populate:', function() { }); it('toJSON should also be called for refs (gh-675)', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefUser', users); User.prototype._toJSON = User.prototype.toJSON; User.prototype.toJSON = function() { @@ -1960,14 +2148,14 @@ describe('model: populate:', function() { }; User.create({ - name : 'Jerem' - , email : 'jerem@jolicloud.com' + name: 'Jerem', + email: 'jerem@jolicloud.com' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'Ping Pong' - , _creator : creator + title: 'Ping Pong', + _creator: creator }, function(err, post) { assert.ifError(err); @@ -1980,7 +2168,7 @@ describe('model: populate:', function() { var json = post.toJSON(); assert.equal(true, json.was_in_to_json); - assert.equal(json._creator.was_in_to_json,true); + assert.equal(json._creator.was_in_to_json, true); done(); }); }); @@ -1991,33 +2179,33 @@ describe('model: populate:', function() { var db = start(); var UserSchema = new Schema({ - _id: Buffer - , name: String + _id: Buffer, + name: String }); var NoteSchema = new Schema({ - author: { type: Buffer, ref: 'UserWithBufferId' } - , body: String + author: {type: Buffer, ref: 'UserWithBufferId'}, + body: String }); var User = db.model('UserWithBufferId', UserSchema, random()); var Note = db.model('NoteWithBufferId', NoteSchema, random()); - var alice = new User({_id: new mongoose.Types.Buffer('YWxpY2U=', 'base64'), name: "Alice"}); + var alice = new User({_id: new mongoose.Types.Buffer('YWxpY2U=', 'base64'), name: 'Alice'}); alice.save(function(err) { assert.ifError(err); - var note = new Note({author: 'alice', body: "Buy Milk"}); + var note = new Note({author: 'alice', body: 'Buy Milk'}); note.save(function(err) { assert.ifError(err); Note.findById(note.id).populate('author').exec(function(err, note) { db.close(); assert.ifError(err); - assert.equal(note.body,'Buy Milk'); + assert.equal(note.body, 'Buy Milk'); assert.ok(note.author); - assert.equal(note.author.name,'Alice'); + assert.equal(note.author.name, 'Alice'); done(); }); }); @@ -2028,24 +2216,24 @@ describe('model: populate:', function() { var db = start(); var UserSchema = new Schema({ - _id: Buffer - , name: String + _id: Buffer, + name: String }); var NoteSchema = new Schema({ - author: { type: Buffer, ref: 'UserWithBufferId', required: true } - , body: String + author: {type: Buffer, ref: 'UserWithBufferId', required: true}, + body: String }); var User = db.model('UserWithBufferId', UserSchema, random()); var Note = db.model('NoteWithBufferId', NoteSchema, random()); - var alice = new User({_id: new mongoose.Types.Buffer('YWxpY2U=', 'base64'), name: "Alice"}); + var alice = new User({_id: new mongoose.Types.Buffer('YWxpY2U=', 'base64'), name: 'Alice'}); alice.save(function(err) { assert.ifError(err); - var note = new Note({author: 'alice', body: "Buy Milk"}); + var note = new Note({author: 'alice', body: 'Buy Milk'}); note.save(function(err) { assert.ifError(err); @@ -2062,19 +2250,19 @@ describe('model: populate:', function() { }); it('populating with custom model selection (gh-773)', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts) - , User = db.model('RefAlternateUser', users); + var db = start(), + BlogPost = db.model('RefBlogPost', posts), + User = db.model('RefAlternateUser', users); User.create({ - name : 'Daniel' - , email : 'daniel.baulig@gmx.de' + name: 'Daniel', + email: 'daniel.baulig@gmx.de' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'woot' - , _creator : creator + title: 'woot', + _creator: creator }, function(err, post) { assert.ifError(err); @@ -2087,7 +2275,7 @@ describe('model: populate:', function() { assert.ok(post._creator instanceof User); assert.equal(post._creator.isInit('name'), false); - assert.equal(post._creator.email,'daniel.baulig@gmx.de'); + assert.equal(post._creator.email, 'daniel.baulig@gmx.de'); done(); }); @@ -2098,16 +2286,22 @@ describe('model: populate:', function() { describe('specifying a custom model without specifying a ref in schema', function() { it('with String _id', function(done) { var db = start(); - var A = db.model('A', { name: String, _id: String }); - var B = db.model('B', { other: String }); - A.create({ name: 'hello', _id: 'first' }, function(err, a) { - if (err) return done(err); - B.create({ other: a._id }, function(err, b) { - if (err) return done(err); - B.findById(b._id).populate({ path: 'other', model: 'A' }).exec(function(err, b) { + var A = db.model('A', {name: String, _id: String}); + var B = db.model('B', {other: String}); + A.create({name: 'hello', _id: 'first'}, function(err, a) { + if (err) { + return done(err); + } + B.create({other: a._id}, function(err, b) { + if (err) { + return done(err); + } + B.findById(b._id).populate({path: 'other', model: 'A'}).exec(function(err, b) { db.close(); - if (err) return done(err); - assert.equal('hello', b.other.name); + if (err) { + return done(err); + } + assert.equal(b.other.name, 'hello'); done(); }); }); @@ -2115,16 +2309,22 @@ describe('model: populate:', function() { }); it('with Number _id', function(done) { var db = start(); - var A = db.model('A', { name: String, _id: Number }); - var B = db.model('B', { other: Number }); - A.create({ name: 'hello', _id: 3 }, function(err, a) { - if (err) return done(err); - B.create({ other: a._id }, function(err, b) { - if (err) return done(err); - B.findById(b._id).populate({ path: 'other', model: 'A' }).exec(function(err, b) { + var A = db.model('A', {name: String, _id: Number}); + var B = db.model('B', {other: Number}); + A.create({name: 'hello', _id: 3}, function(err, a) { + if (err) { + return done(err); + } + B.create({other: a._id}, function(err, b) { + if (err) { + return done(err); + } + B.findById(b._id).populate({path: 'other', model: 'A'}).exec(function(err, b) { db.close(); - if (err) return done(err); - assert.equal('hello', b.other.name); + if (err) { + return done(err); + } + assert.equal(b.other.name, 'hello'); done(); }); }); @@ -2132,16 +2332,22 @@ describe('model: populate:', function() { }); it('with Buffer _id', function(done) { var db = start(); - var A = db.model('A', { name: String, _id: Buffer }); - var B = db.model('B', { other: Buffer }); - A.create({ name: 'hello', _id: new Buffer('x') }, function(err, a) { - if (err) return done(err); - B.create({ other: a._id }, function(err, b) { - if (err) return done(err); - B.findById(b._id).populate({ path: 'other', model: 'A' }).exec(function(err, b) { + var A = db.model('A', {name: String, _id: Buffer}); + var B = db.model('B', {other: Buffer}); + A.create({name: 'hello', _id: new Buffer('x')}, function(err, a) { + if (err) { + return done(err); + } + B.create({other: a._id}, function(err, b) { + if (err) { + return done(err); + } + B.findById(b._id).populate({path: 'other', model: 'A'}).exec(function(err, b) { db.close(); - if (err) return done(err); - assert.equal('hello', b.other.name); + if (err) { + return done(err); + } + assert.equal(b.other.name, 'hello'); done(); }); }); @@ -2149,16 +2355,22 @@ describe('model: populate:', function() { }); it('with ObjectId _id', function(done) { var db = start(); - var A = db.model('A', { name: String }); - var B = db.model('B', { other: Schema.ObjectId }); - A.create({ name: 'hello' }, function(err, a) { - if (err) return done(err); - B.create({ other: a._id }, function(err, b) { - if (err) return done(err); - B.findById(b._id).populate({ path: 'other', model: 'A' }).exec(function(err, b) { + var A = db.model('A', {name: String}); + var B = db.model('B', {other: Schema.ObjectId}); + A.create({name: 'hello'}, function(err, a) { + if (err) { + return done(err); + } + B.create({other: a._id}, function(err, b) { + if (err) { + return done(err); + } + B.findById(b._id).populate({path: 'other', model: 'A'}).exec(function(err, b) { db.close(); - if (err) return done(err); - assert.equal('hello', b.other.name); + if (err) { + return done(err); + } + assert.equal(b.other.name, 'hello'); done(); }); }); @@ -2176,23 +2388,23 @@ describe('model: populate:', function() { User = db.model('RefAlternateUser'); User.create({ - name : 'use an object' - , email : 'fo-real@objects.r.fun' + name: 'use an object', + email: 'fo-real@objects.r.fun' } - , { name: 'yup' } - , { name: 'not here' } - , function(err, fan1, fan2, fan3) { - assert.ifError(err); + , {name: 'yup'} + , {name: 'not here'} + , function(err, fan1, fan2, fan3) { + assert.ifError(err); - B.create({ - title: 'woot' - , fans: [fan1, fan2, fan3] - }, function(err, post_) { - assert.ifError(err); - post = post_; - done(); - }); - }); + B.create({ + title: 'woot', + fans: [fan1, fan2, fan3] + }, function(err, post_) { + assert.ifError(err); + post = post_; + done(); + }); + }); }); after(function(done) { @@ -2202,30 +2414,29 @@ describe('model: populate:', function() { it('works', function(done) { B.findById(post._id) .populate({ - path: 'fans' - , select: 'name' - , model: 'RefAlternateUser' - , match: { name: /u/ } - , options: { sort: {'name': -1} } + path: 'fans', + select: 'name', + model: 'RefAlternateUser', + match: {name: /u/}, + options: {sort: {name: -1}} }) .exec(function(err, post) { assert.ifError(err); assert.ok(Array.isArray(post.fans)); - assert.equal(2, post.fans.length); + assert.equal(post.fans.length, 2); assert.ok(post.fans[0] instanceof User); assert.ok(post.fans[1] instanceof User); assert.equal(post.fans[0].isInit('name'), true); assert.equal(post.fans[1].isInit('name'), true); assert.equal(post.fans[0].isInit('email'), false); assert.equal(post.fans[1].isInit('email'), false); - assert.equal(post.fans[0].name,'yup'); - assert.equal(post.fans[1].name,'use an object'); + assert.equal(post.fans[0].name, 'yup'); + assert.equal(post.fans[1].name, 'use an object'); done(); }); }); - }); describe('Model.populate()', function() { @@ -2240,13 +2451,13 @@ describe('model: populate:', function() { _id = new mongoose.Types.ObjectId; User.create({ - name : 'Phoenix' - , email : 'phx@az.com' - , blogposts: [_id] + name: 'Phoenix', + email: 'phx@az.com', + blogposts: [_id] }, { - name : 'Newark' - , email : 'ewr@nj.com' - , blogposts: [_id] + name: 'Newark', + email: 'ewr@nj.com', + blogposts: [_id] }, function(err, u1, u2) { assert.ifError(err); @@ -2254,13 +2465,13 @@ describe('model: populate:', function() { user2 = u2; B.create({ - title : 'the how and why' - , _creator : user1 - , fans: [user1, user2] + title: 'the how and why', + _creator: user1, + fans: [user1, user2] }, { - title : 'green eggs and ham' - , _creator : user2 - , fans: [user2, user1] + title: 'green eggs and ham', + _creator: user2, + fans: [user2, user1] }, function(err, p1, p2) { assert.ifError(err); post1 = p1; @@ -2289,12 +2500,12 @@ describe('model: populate:', function() { describe('of individual document', function() { it('works', function(done) { B.findById(post1._id, function(error, post1) { - var ret = utils.populate({ path: '_creator', model: 'RefAlternateUser' }); + var ret = utils.populate({path: '_creator', model: 'RefAlternateUser'}); B.populate(post1, ret, function(err, post) { assert.ifError(err); assert.ok(post); assert.ok(post._creator instanceof User); - assert.equal('Phoenix', post._creator.name); + assert.equal(post._creator.name, 'Phoenix'); done(); }); }); @@ -2306,33 +2517,29 @@ describe('model: populate:', function() { it('works', function(done) { B.findById(post1._id, function(err, doc) { assert.ifError(err); - B.populate(doc, [{ path: '_creator', model: 'RefAlternateUser' }, { path: 'fans', model: 'RefAlternateUser' }], function(err, post) { + B.populate(doc, [{path: '_creator', model: 'RefAlternateUser'}, {path: 'fans', model: 'RefAlternateUser'}], function(err, post) { assert.ifError(err); assert.ok(post); assert.ok(post._creator instanceof User); assert.equal('Phoenix', post._creator.name); - assert.equal(2, post.fans.length); + assert.equal(post.fans.length, 2); assert.equal(post.fans[0].name, user1.name); assert.equal(post.fans[1].name, user2.name); assert.equal(String(post._creator._id), String(post.populated('_creator'))); assert.ok(Array.isArray(post.populated('fans'))); - B.populate(doc, [{ path: '_creator', model: 'RefAlternateUser' }, { path: 'fans', model: 'RefAlternateUser' }], function(err, post) { + B.populate(doc, [{path: '_creator', model: 'RefAlternateUser'}, {path: 'fans', model: 'RefAlternateUser'}], function(err, post) { assert.ifError(err); assert.ok(post); assert.ok(post._creator instanceof User); - assert.equal('Phoenix', post._creator.name); - assert.equal(2, post.fans.length); + assert.equal(post._creator.name, 'Phoenix'); + assert.equal(post.fans.length, 2); assert.equal(post.fans[0].name, user1.name); assert.equal(post.fans[1].name, user2.name); assert.ok(Array.isArray(post.populated('fans'))); - assert.equal( - String(post.fans[0]._id) - , String(post.populated('fans')[0])); - assert.equal( - String(post.fans[1]._id) - , String(post.populated('fans')[1])); + assert.equal(String(post.fans[0]._id), String(post.populated('fans')[0])); + assert.equal(String(post.fans[1]._id), String(post.populated('fans')[1])); done(); }); @@ -2344,12 +2551,12 @@ describe('model: populate:', function() { it('works', function(done) { B.findById(post1._id, function(err, doc) { assert.ifError(err); - B.populate(doc, [{ path: '_creator', model: 'RefAlternateUser' }, { path: 'fans', model: 'RefAlternateUser' }], function(err, post) { + B.populate(doc, [{path: '_creator', model: 'RefAlternateUser'}, {path: 'fans', model: 'RefAlternateUser'}], function(err, post) { assert.ifError(err); assert.ok(post); assert.ok(post._creator instanceof User); - assert.equal('Phoenix', post._creator.name); - assert.equal(2, post.fans.length); + assert.equal(post._creator.name, 'Phoenix'); + assert.equal(post.fans.length, 2); assert.equal(post.fans[0].name, user1.name); assert.equal(post.fans[1].name, user2.name); @@ -2360,21 +2567,21 @@ describe('model: populate:', function() { doc.markModified('_creator'); doc.markModified('fans'); - B.populate(doc, [{ path: '_creator', model: 'RefAlternateUser' }, { path: 'fans', model: 'RefAlternateUser' }], function(err, post) { + B.populate(doc, [{path: '_creator', model: 'RefAlternateUser'}, {path: 'fans', model: 'RefAlternateUser'}], function(err, post) { assert.ifError(err); assert.ok(post); assert.ok(post._creator instanceof User); - assert.equal('Phoenix', post._creator.name); - assert.equal(2, post.fans.length); + assert.equal(post._creator.name, 'Phoenix'); + assert.equal(post.fans.length, 2); assert.equal(post.fans[0].name, user1.name); assert.equal(post.fans[1].name, user2.name); assert.ok(Array.isArray(post.populated('fans'))); assert.equal( String(post.fans[0]._id) - , String(post.populated('fans')[0])); + , String(post.populated('fans')[0])); assert.equal( String(post.fans[1]._id) - , String(post.populated('fans')[1])); + , String(post.populated('fans')[1])); done(); }); @@ -2390,41 +2597,40 @@ describe('model: populate:', function() { assert.ifError(error); B.findById(post2._id, function(error, post2) { assert.ifError(error); - var ret = utils.populate({ path: '_creator', model: 'RefAlternateUser' }); + var ret = utils.populate({path: '_creator', model: 'RefAlternateUser'}); B.populate([post1, post2], ret, function(err, posts) { assert.ifError(err); assert.ok(posts); - assert.equal(2, posts.length); + assert.equal(posts.length, 2); var p1 = posts[0]; var p2 = posts[1]; assert.ok(p1._creator instanceof User); - assert.equal('Phoenix', p1._creator.name); + assert.equal(p1._creator.name, 'Phoenix'); assert.ok(p2._creator instanceof User); - assert.equal('Newark', p2._creator.name); + assert.equal(p2._creator.name, 'Newark'); done(); }); }); }); }); }); - }); describe('populating combined with lean (gh-1260)', function() { it('with findOne', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts + random()) - , User = db.model('RefUser', users + random()); + var db = start(), + BlogPost = db.model('RefBlogPost', posts + random()), + User = db.model('RefUser', users + random()); User.create({ - name : 'Guillermo' - , email : 'rauchg@gmail.com' + name: 'Guillermo', + email: 'rauchg@gmail.com' }, function(err, creator) { assert.ifError(err); BlogPost.create({ - title : 'woot' - , _creator : creator + title: 'woot', + _creator: creator }, function(err, post) { assert.ifError(err); @@ -2439,7 +2645,7 @@ describe('model: populate:', function() { assert.ok(utils.isObject(post._creator)); assert.equal(post._creator.name, 'Guillermo'); assert.equal(post._creator.email, 'rauchg@gmail.com'); - assert.equal('undefined', typeof post._creator.update); + assert.equal(typeof post._creator.update, 'undefined'); done(); }); }); @@ -2447,48 +2653,48 @@ describe('model: populate:', function() { }); it('with find', function(done) { - var db = start() - , BlogPost = db.model('RefBlogPost', posts + random()) - , User = db.model('RefUser', users + random()); + var db = start(), + BlogPost = db.model('RefBlogPost', posts + random()), + User = db.model('RefUser', users + random()); User.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' + name: 'Fan 1', + email: 'fan1@learnboost.com' }, { - name : 'Fan 2' - , email : 'fan2@learnboost.com' + name: 'Fan 2', + email: 'fan2@learnboost.com' }, function(err, fan1, fan2) { assert.ifError(err); BlogPost.create({ - title : 'Woot' - , fans : [fan1, fan2] + title: 'Woot', + fans: [fan1, fan2] }, { - title : 'Woot2' - , fans : [fan2, fan1] + title: 'Woot2', + fans: [fan2, fan1] }, function(err, post1, post2) { assert.ifError(err); BlogPost - .find({ _id: { $in: [post1._id, post2._id ] } }) + .find({_id: {$in: [post1._id, post2._id]}}) .populate('fans') .lean() .exec(function(err, blogposts) { assert.ifError(err); - assert.equal(blogposts[0].fans[0].name,'Fan 1'); - assert.equal(blogposts[0].fans[0].email,'fan1@learnboost.com'); - assert.equal('undefined', typeof blogposts[0].fans[0].update); - assert.equal(blogposts[0].fans[1].name,'Fan 2'); - assert.equal(blogposts[0].fans[1].email,'fan2@learnboost.com'); - assert.equal('undefined', typeof blogposts[0].fans[1].update); - - assert.equal(blogposts[1].fans[0].name,'Fan 2'); - assert.equal(blogposts[1].fans[0].email,'fan2@learnboost.com'); - assert.equal('undefined', typeof blogposts[1].fans[0].update); - assert.equal(blogposts[1].fans[1].name,'Fan 1'); - assert.equal(blogposts[1].fans[1].email,'fan1@learnboost.com'); - assert.equal('undefined', typeof blogposts[1].fans[1].update); + assert.equal(blogposts[0].fans[0].name, 'Fan 1'); + assert.equal(blogposts[0].fans[0].email, 'fan1@learnboost.com'); + assert.equal(typeof blogposts[0].fans[0].update, 'undefined'); + assert.equal(blogposts[0].fans[1].name, 'Fan 2'); + assert.equal(blogposts[0].fans[1].email, 'fan2@learnboost.com'); + assert.equal(typeof blogposts[0].fans[1].update, 'undefined'); + + assert.equal(blogposts[1].fans[0].name, 'Fan 2'); + assert.equal(blogposts[1].fans[0].email, 'fan2@learnboost.com'); + assert.equal(typeof blogposts[1].fans[0].update, 'undefined'); + assert.equal(blogposts[1].fans[1].name, 'Fan 1'); + assert.equal(blogposts[1].fans[1].email, 'fan1@learnboost.com'); + assert.equal(typeof blogposts[1].fans[1].update, 'undefined'); db.close(done); }); }); @@ -2509,24 +2715,24 @@ describe('model: populate:', function() { U = db.model('RefUser', users + random()); U.create({ - name : 'Fan 1' - , email : 'fan1@learnboost.com' + name: 'Fan 1', + email: 'fan1@learnboost.com' }, { - name : 'Fan 2' - , email : 'fan2@learnboost.com' + name: 'Fan 2', + email: 'fan2@learnboost.com' }, function(err, fan1, fan2) { assert.ifError(err); u1 = fan1; u2 = fan2; B.create({ - title : 'Woot' - , fans : [fan1, fan2] - , _creator: fan1 + title: 'Woot', + fans: [fan1, fan2], + _creator: fan1 }, { - title : 'Woot2' - , fans : [fan2, fan1] - , _creator: fan2 + title: 'Woot2', + fans: [fan2, fan1], + _creator: fan2 }, function(err, post) { assert.ifError(err); b1 = post; @@ -2543,7 +2749,7 @@ describe('model: populate:', function() { B.findById(b1).populate('fans _creator').exec(function(err, doc) { assert.ifError(err); assert.ok(Array.isArray(doc.populated('fans'))); - assert.equal(2, doc.populated('fans').length); + assert.equal(doc.populated('fans').length, 2); assert.equal(doc.populated('fans')[0], String(u1._id)); assert.equal(doc.populated('fans')[1], String(u2._id)); assert.equal(doc.populated('_creator'), String(u1._id)); @@ -2554,20 +2760,20 @@ describe('model: populate:', function() { it('with find', function(done) { B.find().sort('title').populate('fans _creator').exec(function(err, docs) { assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); var doc1 = docs[0]; var doc2 = docs[1]; assert.ok(Array.isArray(doc1.populated('fans'))); - assert.equal(2, doc1.populated('fans').length); + assert.equal(doc1.populated('fans').length, 2); assert.equal(doc1.populated('fans')[0], String(u1._id)); assert.equal(doc1.populated('fans')[1], String(u2._id)); assert.equal(doc1.populated('_creator'), String(u1._id)); assert.ok(Array.isArray(doc2.populated('fans'))); - assert.equal(2, doc2.populated('fans').length); + assert.equal(doc2.populated('fans').length, 2); assert.equal(doc2.populated('fans')[0], String(u2._id)); assert.equal(doc2.populated('fans')[1], String(u1._id)); assert.equal(doc2.populated('_creator'), String(u2._id)); @@ -2586,23 +2792,23 @@ describe('model: populate:', function() { }), 'comments_' + random()); U = db.model('User', Schema({ - name: 'string' - , comments: [{ type: Schema.ObjectId, ref: 'Comment' }] - , comment: { type: Schema.ObjectId, ref: 'Comment' } + name: 'string', + comments: [{type: Schema.ObjectId, ref: 'Comment'}], + comment: {type: Schema.ObjectId, ref: 'Comment'} }), 'users_' + random()); - C.create({ body: 'comment 1', title: '1' }, { body: 'comment 2', title: 2 }, function(err, c1_, c2_) { + C.create({body: 'comment 1', title: '1'}, {body: 'comment 2', title: 2}, function(err, c1_, c2_) { assert.ifError(err); c1 = c1_; c2 = c2_; U.create( - { name: 'u1', comments: [c1, c2], comment: c1 } - , { name: 'u2', comment: c2 } - , function(err) { - assert.ifError(err); - done(); - }); + {name: 'u1', comments: [c1, c2], comment: c1} + , {name: 'u2', comment: c2} + , function(err) { + assert.ifError(err); + done(); + }); }); }); @@ -2612,35 +2818,35 @@ describe('model: populate:', function() { describe('in a subdocument', function() { it('works', function(done) { - U.find({name:'u1'}).populate('comments', { _id: 0 }).exec(function(err, docs) { + U.find({name: 'u1'}).populate('comments', {_id: 0}).exec(function(err, docs) { assert.ifError(err); var doc = docs[0]; assert.ok(Array.isArray(doc.comments), 'comments should be an array: ' + JSON.stringify(doc)); - assert.equal(2, doc.comments.length, 'invalid comments length for ' + JSON.stringify(doc)); + assert.equal(doc.comments.length, 2, 'invalid comments length for ' + JSON.stringify(doc)); doc.comments.forEach(function(d) { - assert.equal(undefined, d._id); - assert.equal(-1, Object.keys(d._doc).indexOf('_id')); + assert.equal(d._id, undefined); + assert.equal(Object.keys(d._doc).indexOf('_id'), -1); assert.ok(d.body.length); - assert.equal('number', typeof d._doc.__v); + assert.equal(typeof d._doc.__v, 'number'); }); - U.findOne({name:'u1'}).populate('comments', 'title -_id').exec(function(err, doc) { + U.findOne({name: 'u1'}).populate('comments', 'title -_id').exec(function(err, doc) { assert.ifError(err); - assert.equal(2, doc.comments.length); + assert.equal(doc.comments.length, 2); doc.comments.forEach(function(d) { - assert.equal(undefined, d._id); - assert.equal(-1, Object.keys(d._doc).indexOf('_id')); + assert.equal(d._id, undefined); + assert.equal(Object.keys(d._doc).indexOf('_id'), -1); assert.ok(d.title.length); - assert.equal(undefined, d.body); + assert.equal(d.body, undefined); assert.equal(typeof d._doc.__v, 'undefined'); }); - U.findOne({name:'u1'}).populate('comments', '-_id').exec(function(err, doc) { + U.findOne({name: 'u1'}).populate('comments', '-_id').exec(function(err, doc) { assert.ifError(err); - assert.equal(2, doc.comments.length); + assert.equal(doc.comments.length, 2); doc.comments.forEach(function(d) { - assert.equal(undefined, d._id); - assert.equal(-1, Object.keys(d._doc).indexOf('_id')); + assert.equal(d._id, undefined); + assert.equal(Object.keys(d._doc).indexOf('_id'), -1); assert.ok(d.title.length); assert.ok(d.body.length); assert.equal(typeof d._doc.__v, 'number'); @@ -2652,24 +2858,24 @@ describe('model: populate:', function() { }); it('with lean', function(done) { - U.find({name:'u1'}).lean().populate({ path: 'comments', select: { _id: 0 }, options: { lean: true }}).exec(function(err, docs) { + U.find({name: 'u1'}).lean().populate({path: 'comments', select: {_id: 0}, options: {lean: true}}).exec(function(err, docs) { assert.ifError(err); var doc = docs[0]; - assert.equal(2, doc.comments.length); + assert.equal(doc.comments.length, 2); doc.comments.forEach(function(d) { assert.ok(!('_id' in d)); assert.ok(d.body.length); - assert.equal('number', typeof d.__v); + assert.equal(typeof d.__v, 'number'); }); - U.findOne({name:'u1'}).lean().populate('comments', '-_id', null, { lean: true}).exec(function(err, doc) { + U.findOne({name: 'u1'}).lean().populate('comments', '-_id', null, {lean: true}).exec(function(err, doc) { assert.ifError(err); - assert.equal(2, doc.comments.length); + assert.equal(doc.comments.length, 2); doc.comments.forEach(function(d) { assert.ok(!('_id' in d)); assert.ok(d.body.length); - assert.equal('number', typeof d.__v); + assert.equal(typeof d.__v, 'number'); }); done(); }); @@ -2679,63 +2885,24 @@ describe('model: populate:', function() { describe('of documents being populated', function() { it('still works (gh-1441)', function(done) { - U.find() - .select('-_id comment name') - .populate('comment', { _id: 0 }).exec(function(err, docs) { - - assert.ifError(err); - assert.equal(2, docs.length); - - docs.forEach(function(doc) { - assert.ok(doc.comment && doc.comment.body); - if ('u1' == doc.name) { - assert.equal('comment 1', doc.comment.body); - } else { - assert.equal('comment 2', doc.comment.body); - } - }); - - done(); + .select('-_id comment name') + .populate('comment', {_id: 0}).exec(function(err, docs) { + assert.ifError(err); + assert.equal(docs.length, 2); + + docs.forEach(function(doc) { + assert.ok(doc.comment && doc.comment.body); + if (doc.name === 'u1') { + assert.equal(doc.comment.body, 'comment 1'); + } else { + assert.equal(doc.comment.body, 'comment 2'); + } }); - }); - }); - }); - - it('maps results back to correct document (gh-1444)', function(done) { - var db = start(); - - var articleSchema = new Schema({ - body: String, - mediaAttach: {type: Schema.ObjectId, ref : '1444-Media'}, - author: String - }); - var Article = db.model('1444-Article', articleSchema); - - var mediaSchema = new Schema({ - filename: String - }); - var Media = db.model('1444-Media', mediaSchema); - - Media.create({ filename: 'one' }, function(err, media) { - assert.ifError(err); - - Article.create( - {body: 'body1', author: 'a'} - , {body: 'body2', author: 'a', mediaAttach: media._id} - , {body: 'body3', author: 'a'}, function(err) { - if (err) return done(err); - - Article.find().populate('mediaAttach').exec(function(err, docs) { - db.close(); - assert.ifError(err); - var a2 = docs.filter(function(d) { return 'body2' == d.body;})[0]; - assert.equal(a2.mediaAttach.id, media.id); - - done(); - }); + done(); }); + }); }); }); @@ -2760,15 +2927,15 @@ describe('model: populate:', function() { } }, items: [ - { - id: { - type: Number, - refPath: 'items.type' - }, - type: { - type: String - } + { + id: { + type: Number, + refPath: 'items.type' + }, + type: { + type: String } + } ] }); @@ -2789,15 +2956,15 @@ describe('model: populate:', function() { var review = { _id: 0, text: 'Test', - item: { id: 1, type: 'dynrefItem1' }, - items: [{ id: 1, type: 'dynrefItem1' }, { id: 2, type: 'dynrefItem2' }] + item: {id: 1, type: 'dynrefItem1'}, + items: [{id: 1, type: 'dynrefItem1'}, {id: 2, type: 'dynrefItem2'}] }; - Item1.create({ _id: 1, name: 'Val' }, function(err) { + Item1.create({_id: 1, name: 'Val'}, function(err) { if (err) { return done(err); } - Item2.create({ _id: 2, otherName: 'Val' }, function(err) { + Item2.create({_id: 2, otherName: 'Val'}, function(err) { if (err) { return done(err); } @@ -2818,9 +2985,9 @@ describe('model: populate:', function() { it('Simple populate', function(done) { Review.find({}).populate('item.id').exec(function(err, results) { assert.ifError(err); - assert.equal(1, results.length); + assert.equal(results.length, 1); var result = results[0]; - assert.equal('Val', result.item.id.name); + assert.equal(result.item.id.name, 'Val'); done(); }); }); @@ -2828,14 +2995,185 @@ describe('model: populate:', function() { it('Array populate', function(done) { Review.find({}).populate('items.id').exec(function(err, results) { assert.ifError(err); - assert.equal(1, results.length); + assert.equal(results.length, 1); var result = results[0]; - assert.equal(2, result.items.length); - assert.equal('Val', result.items[0].id.name); - assert.equal('Val', result.items[1].id.otherName); + assert.equal(result.items.length, 2); + assert.equal(result.items[0].id.name, 'Val'); + assert.equal(result.items[1].id.otherName, 'Val'); done(); }); }); + + it('with nonexistant refPath (gh-4637)', function(done) { + var baseballSchema = mongoose.Schema({ + seam: String + }); + var Baseball = db.model('Baseball', baseballSchema); + + var ballSchema = mongoose.Schema({ + league: String, + kind: String, + ball: { + type: mongoose.Schema.Types.ObjectId, + refPath: 'balls.kind' + } + }); + + var basketSchema = mongoose.Schema({ + balls: [ballSchema] + }); + var Basket = db.model('Basket', basketSchema); + + new Baseball({seam: 'yarn'}). + save(). + then(function(baseball) { + return new Basket({ + balls: [ + { + league: 'MLB', + kind: 'Baseball', + ball: baseball._id + }, + { + league: 'NBA' + } + ] + }).save(); + }). + then(function(basket) { + return basket.populate('balls.ball').execPopulate(); + }). + then(function(basket) { + assert.equal(basket.balls[0].ball.seam, 'yarn'); + assert.ok(!basket.balls[1].kind); + assert.ok(!basket.balls[1].ball); + done(); + }). + catch(done); + }); + + it('array with empty refPath (gh-5377)', function(done) { + var modelASchema = new mongoose.Schema({ + name: String + }); + var ModelA = db.model('gh5377_a', modelASchema); + + var modelBSchema = new mongoose.Schema({ + name: String + }); + var ModelB = db.model('gh5377_b', modelBSchema); + + var ChildSchema = new mongoose.Schema({ + name: String, + toy: { + kind: { + type: String, + enum: ['gh5377_a', 'gh5377_b'] + }, + value: { + type: ObjectId, + refPath: 'children.toy.kind' + } + } + }); + + var ParentSchema = new mongoose.Schema({ + children: [ChildSchema] + }); + var Parent = db.model('gh5377', ParentSchema); + + ModelA.create({ name: 'model-A' }, function(error, toyA) { + assert.ifError(error); + ModelB.create({ name: 'model-B' }, function(error, toyB) { + assert.ifError(error); + Parent.create({ + children: [ + { + name: 'Child 1', + toy: { kind: 'gh5377_a', value: toyA._id } + }, + { + name: 'Child 2' + }, + { + name: 'Child 3', + toy: { kind: 'gh5377_b', value: toyB._id } + } + ] + }, function(error, doc) { + assert.ifError(error); + test(doc._id); + }); + }); + }); + + function test(id) { + Parent.findById(id, function(error, doc) { + assert.ifError(error); + doc.populate('children.toy.value').execPopulate().then(function(doc) { + assert.equal(doc.children[0].toy.value.name, 'model-A'); + assert.equal(doc.children[1].toy.value, null); + assert.equal(doc.children[2].toy.value.name, 'model-B'); + done(); + }).catch(done); + }); + } + }); + + it('with non-arrays (gh-5114)', function(done) { + var LocationSchema = new Schema({ + name: String + }); + var UserSchema = new Schema({ + name: String, + locationRef: String, + locationIds: { + type: [{ + location: { + type: mongoose.Schema.Types.ObjectId, + refPath: 'locationRef' + } + }] + } + }); + + var Locations = db.model('gh5114', LocationSchema); + var Users = db.model('gh5114_0', UserSchema); + + var location1Id = new mongoose.Types.ObjectId(); + var location2Id = new mongoose.Types.ObjectId(); + + var location1 = { + _id: location1Id, + name: 'loc1' + }; + var location2 = { + _id: location2Id, + name: 'loc2' + }; + var user = { + locationRef: 'gh5114', + locationIds: [ + { location: location1Id }, + { location: location2Id } + ] + }; + + Locations.create([location1, location2]). + then(function() { + return Users.create(user); + }). + then(function() { + return Users.findOne().populate('locationIds.location'); + }). + then(function(doc) { + assert.equal(doc.locationIds.length, 2); + assert.equal(doc.locationIds[0].location.name, 'loc1'); + assert.equal(doc.locationIds[1].location.name, 'loc2'); + done(); + }). + catch(done); + }); }); describe('leaves Documents within Mixed properties alone (gh-1471)', function() { @@ -2845,7 +3183,7 @@ describe('model: populate:', function() { before(function() { db = start(); - Cat = db.model('cats', new Schema({ name: String })); + Cat = db.model('cats', new Schema({name: String})); var litterSchema = new Schema({name: String, cats: {}, o: {}, a: []}); Litter = db.model('litters', litterSchema); }); @@ -2855,23 +3193,27 @@ describe('model: populate:', function() { }); it('when saving new docs', function(done) { - Cat.create({name:'new1'},{name:'new2'},{name:'new3'}, function(err, a, b, c) { - if (err) return done(err); + Cat.create({name: 'new1'}, {name: 'new2'}, {name: 'new3'}, function(err, a, b, c) { + if (err) { + return done(err); + } Litter.create({ - name: 'new' - , cats:[a] - , o: b - , a: [c] + name: 'new', + cats: [a], + o: b, + a: [c] }, confirm(done)); }); }); it('when saving existing docs 5T5', function(done) { - Cat.create({name:'ex1'},{name:'ex2'},{name:'ex3'}, function(err, a, b, c) { - if (err) return done(err); + Cat.create({name: 'ex1'}, {name: 'ex2'}, {name: 'ex3'}, function(err, a, b, c) { + if (err) { + return done(err); + } - Litter.create({name:'existing'}, function(err, doc) { + Litter.create({name: 'existing'}, function(err, doc) { doc.cats = [a]; doc.o = b; doc.a = [c]; @@ -2882,9 +3224,13 @@ describe('model: populate:', function() { function confirm(done) { return function(err, litter) { - if (err) return done(err); + if (err) { + return done(err); + } Litter.findById(litter).lean().exec(function(err, doc) { - if (err) return done(err); + if (err) { + return done(err); + } assert.ok(doc.o._id); assert.ok(doc.cats[0]); assert.ok(doc.cats[0]._id); @@ -2896,12 +3242,135 @@ describe('model: populate:', function() { } }); - describe('gh-2252', function() { - it('handles skip', function(done) { - var db = start(); + describe('github issues', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + it('populating an array of refs, slicing, and fetching many (gh-5737)', function(done) { + var BlogPost = db.model('gh5737_0', new Schema({ + title: String, + fans: [{ type: ObjectId, ref: 'gh5737' }] + })); + var User = db.model('gh5737', new Schema({ name: String })); + + User.create([{ name: 'Fan 1' }, { name: 'Fan 2' }], function(error, fans) { + assert.ifError(error); + var posts = [ + { title: 'Test 1', fans: [fans[0]._id, fans[1]._id] }, + { title: 'Test 2', fans: [fans[1]._id, fans[0]._id] } + ]; + BlogPost.create(posts, function(error) { + assert.ifError(error); + BlogPost. + find({}). + slice('fans', [0, 5]). + populate('fans'). + exec(function(err, blogposts) { + assert.ifError(err); + + assert.ok(blogposts.length === 2); + + var test1, test2; + if (blogposts[0].title === 'Test 1') { + test1 = blogposts[0]; + test2 = blogposts[1]; + } else { + test1 = blogposts[1]; + test2 = blogposts[0]; + } + + assert.ok(test1.title === 'Test 1'); + assert.ok(test2.title === 'Test 2'); + + assert.equal(test1.fans[0].name, 'Fan 1'); + assert.equal(test1.fans[1].name, 'Fan 2'); + + assert.equal(test2.fans[0].name, 'Fan 2'); + assert.equal(test2.fans[1].name, 'Fan 1'); + done(); + }); + }); + }); + }); + + it('populate + slice (gh-5737a)', function(done) { + var BlogPost = db.model('gh5737b', new Schema({ + title: String, + user: { type: ObjectId, ref: 'gh5737a' }, + fans: [{ type: ObjectId}] + })); + var User = db.model('gh5737a', new Schema({ name: String })); + + User.create([{ name: 'Fan 1' }], function(error, fans) { + assert.ifError(error); + var posts = [ + { title: 'Test 1', user: fans[0]._id, fans: [fans[0]._id] } + ]; + BlogPost.create(posts, function(error) { + assert.ifError(error); + BlogPost. + find({}). + slice('fans', [0, 2]). + populate('user'). + exec(function(err, blogposts) { + assert.ifError(error); + + assert.equal(blogposts[0].user.name, 'Fan 1'); + assert.equal(blogposts[0].title, 'Test 1'); + done(); + }); + }); + }); + }); + + it('maps results back to correct document (gh-1444)', function(done) { + var articleSchema = new Schema({ + body: String, + mediaAttach: {type: Schema.ObjectId, ref: '1444-Media'}, + author: String + }); + var Article = db.model('1444-Article', articleSchema); + + var mediaSchema = new Schema({ + filename: String + }); + var Media = db.model('1444-Media', mediaSchema); + + Media.create({filename: 'one'}, function(err, media) { + assert.ifError(err); + + Article.create( + {body: 'body1', author: 'a'} + , {body: 'body2', author: 'a', mediaAttach: media._id} + , {body: 'body3', author: 'a'}, function(err) { + if (err) { + return done(err); + } + + Article.find().populate('mediaAttach').exec(function(err, docs) { + assert.ifError(err); + + var a2 = docs.filter(function(d) { + return d.body === 'body2'; + })[0]; + assert.equal(a2.mediaAttach.id, media.id); + + done(); + }); + }); + }); + }); + it('handles skip', function(done) { var movieSchema = new Schema({}); - var categorySchema = new Schema({ movies: [{ type: ObjectId, ref: 'gh-2252-1' }] }); + var categorySchema = new Schema({movies: [{type: ObjectId, ref: 'gh-2252-1'}]}); var Movie = db.model('gh-2252-1', movieSchema); var Category = db.model('gh-2252-2', categorySchema); @@ -2911,94 +3380,2676 @@ describe('model: populate:', function() { Movie.find({}, function(error, docs) { assert.ifError(error); assert.equal(docs.length, 3); - Category.create({ movies: [docs[0]._id, docs[1]._id, docs[2]._id] }, function(error) { + Category.create({movies: [docs[0]._id, docs[1]._id, docs[2]._id]}, function(error) { assert.ifError(error); - Category.findOne({}).populate({ path: 'movies', options: { limit: 2, skip: 1 } }).exec(function(error, category) { + Category.findOne({}).populate({path: 'movies', options: {limit: 2, skip: 1}}).exec(function(error, category) { assert.ifError(error); - assert.equal(2, category.movies.length); - db.close(done); + assert.equal(category.movies.length, 2); + done(); }); }); }); }); }); - }); - it('handles slice (gh-1934)', function(done) { - var db = start(); - - var movieSchema = new Schema({ title: String, actors: [String] }); - var categorySchema = new Schema({ movies: [{ type: ObjectId, ref: 'gh-1934-1' }] }); - - var Movie = db.model('gh-1934-1', movieSchema); - var Category = db.model('gh-1934-2', categorySchema); - var movies = [ - { title: 'Rush', actors: ['Chris Hemsworth', 'Daniel Bruhl'] }, - { title: 'Pacific Rim', actors: ['Charlie Hunnam', 'Idris Elba'] }, - { title: 'Man of Steel', actors: ['Henry Cavill', 'Amy Adams'] } - ]; - Movie.create(movies[0], movies[1], movies[2], function(error, m1, m2, m3) { - assert.ifError(error); - Category.create({ movies: [m1._id, m2._id, m3._id] }, function(error) { + it('handles slice (gh-1934)', function(done) { + var movieSchema = new Schema({title: String, actors: [String]}); + var categorySchema = new Schema({movies: [{type: ObjectId, ref: 'gh-1934-1'}]}); + + var Movie = db.model('gh-1934-1', movieSchema); + var Category = db.model('gh-1934-2', categorySchema); + var movies = [ + {title: 'Rush', actors: ['Chris Hemsworth', 'Daniel Bruhl']}, + {title: 'Pacific Rim', actors: ['Charlie Hunnam', 'Idris Elba']}, + {title: 'Man of Steel', actors: ['Henry Cavill', 'Amy Adams']} + ]; + Movie.create(movies[0], movies[1], movies[2], function(error, m1, m2, m3) { assert.ifError(error); - Category.findOne({}).populate({ path: 'movies', options: { slice: { actors: 1 } } }).exec(function(error, category) { + Category.create({movies: [m1._id, m2._id, m3._id]}, function(error) { assert.ifError(error); - assert.equal(category.movies.length, 3); - assert.equal(category.movies[0].actors.length, 1); - assert.equal(category.movies[1].actors.length, 1); - assert.equal(category.movies[2].actors.length, 1); - done(); + Category.findOne({}).populate({path: 'movies', options: {slice: {actors: 1}}}).exec(function(error, category) { + assert.ifError(error); + assert.equal(category.movies.length, 3); + assert.equal(category.movies[0].actors.length, 1); + assert.equal(category.movies[1].actors.length, 1); + assert.equal(category.movies[2].actors.length, 1); + done(); + }); }); }); }); - }); - it('handles toObject() (gh-3279)', function(done) { - var db = start(); + it('fails if sorting with a doc array subprop (gh-2202)', function(done) { + var childSchema = new Schema({ name: String }); + var Child = db.model('gh2202', childSchema); - var teamSchema = new Schema({ - members:[{ - user: {type: ObjectId, ref: 'gh3279'}, - role: String - }] - }); + var parentSchema = new Schema({ + children1: [{ + child: { + type: mongoose.Schema.Types.ObjectId, + ref: 'gh2202' + }, + test: Number + }], + children2: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'gh2202' + }] + }); + var Parent = db.model('gh2202_0', parentSchema); - var calls = 0; - teamSchema.set('toJSON', { - transform: function(doc, ret) { - ++calls; - return ret; - } + Child.create([{ name: 'test1' }, { name: 'test2' }], function(error, c) { + assert.ifError(error); + var doc = { + children1: [ + { child: c[0]._id, test: 1 }, + { child: c[1]._id, test: 2 } + ], + children2: [c[0]._id, c[1]._id] + }; + Parent.create(doc, function(error, doc) { + assert.ifError(error); + Parent.findById(doc).populate('children2').exec(function(error, doc) { + assert.ifError(error); + assert.equal(doc.children2[0].name, 'test1'); + Parent.findById(doc). + populate({ path: 'children1.child', options: { sort: '-name' } }). + exec(function(error) { + assert.notEqual(error.message.indexOf('subproperty of a document array'), + -1); + done(); + }); + }); + }); + }); }); + it('handles toObject() (gh-3279)', function(done) { + var teamSchema = new Schema({ + members: [{ + user: {type: ObjectId, ref: 'gh3279'}, + role: String + }] + }); + + var calls = 0; + teamSchema.set('toJSON', { + transform: function(doc, ret) { + ++calls; + return ret; + } + }); - var Team = db.model('gh3279_1', teamSchema); - var userSchema = new Schema({ - username: String + var Team = db.model('gh3279_1', teamSchema); + + var userSchema = new Schema({ + username: String + }); + + userSchema.set('toJSON', { + transform: function(doc, ret) { + return ret; + } + }); + + var User = db.model('gh3279', userSchema); + + var user = new User({username: 'Test'}); + + user.save(function(err) { + assert.ifError(err); + var team = new Team({members: [{user: user}]}); + + team.save(function(err) { + assert.ifError(err); + team.populate('members.user', function() { + team.toJSON(); + assert.equal(calls, 1); + done(); + }); + }); + }); }); - userSchema.set('toJSON', { - transform: function(doc, ret) { - return ret; - } + it('populate option (gh-2321)', function(done) { + var User = db.model('User', {name: String}); + var Group = db.model('Group', { + users: [{type: mongoose.Schema.Types.ObjectId, ref: 'User'}], + name: String + }); + + User.create({name: 'Val'}, function(error, user) { + assert.ifError(error); + Group.create({users: [user._id], name: 'test'}, function(error, group) { + assert.ifError(error); + test(group._id); + }); + }); + + var test = function(id) { + var options = {populate: {path: 'users', model: 'User'}}; + Group.find({_id: id}, '-name', options, function(error, group) { + assert.ifError(error); + assert.ok(group[0].users[0]._id); + done(); + }); + }; }); - var User = db.model('gh3279', userSchema); + it('discriminator child schemas (gh-3878)', function(done) { + var options = { discriminatorKey: 'kind' }; + var activitySchema = new Schema({ title: { type: String } }, options); - var user = new User({ username: 'Test' }); + var dateActivitySchema = new Schema({ + postedBy: { type: Schema.Types.ObjectId, ref: 'gh3878', required: true } + }, options); - user.save(function(err) { - assert.ifError(err); - var team = new Team({ members: [{ user: user }] }); + var eventActivitySchema = new Schema({ test: String }, options); - team.save(function(err) { - assert.ifError(err); - team.populate('members.user', function() { - team.toJSON(); - assert.equal(calls, 1); + var User = db.model('gh3878', { name: String }); + var Activity = db.model('gh3878_0', activitySchema); + var DateActivity = Activity.discriminator('Date', dateActivitySchema); + var EventActivity = Activity.discriminator('Event', eventActivitySchema); + + User.create({ name: 'val' }, function(error, user) { + assert.ifError(error); + var dateActivity = { title: 'test', postedBy: user._id }; + DateActivity.create(dateActivity, function(error) { + assert.ifError(error); + var eventActivity = { + title: 'test2', + test: 'test' + }; + EventActivity.create(eventActivity, function(error) { + assert.ifError(error); + test(); + }); + }); + }); + + function test() { + Activity.find({}).populate('postedBy').exec(function(error, results) { + assert.ifError(error); + assert.equal(results.length, 2); + assert.equal(results[0].postedBy.name, 'val'); done(); }); + } + }); + + it('set to obj w/ same id doesnt mark modified (gh-3992)', function(done) { + var personSchema = new Schema({ + name: { type: String } + }); + var jobSchema = new Schema({ + title: String, + person: { type: Schema.Types.ObjectId, ref: 'gh3992' } + }); + + var Person = db.model('gh3992', personSchema); + var Job = db.model('gh3992_0', jobSchema); + + Person.create({ name: 'Val' }, function(error, person) { + assert.ifError(error); + var job = { title: 'Engineer', person: person._id }; + Job.create(job, function(error, job) { + assert.ifError(error); + Job.findById(job._id, function(error, job) { + assert.ifError(error); + job.person = person; + assert.ok(!job.isModified('person')); + done(); + }); + }); + }); + }); + + it('deep populate single -> array (gh-3904)', function(done) { + var personSchema = new Schema({ + name: { type: String } + }); + + var teamSchema = new Schema({ + name: { type: String }, + members: [{ type: Schema.Types.ObjectId, ref: 'gh3904' }] + }); + + var gameSchema = new Schema({ + team: { type: Schema.Types.ObjectId, ref: 'gh3904_0' }, + opponent: { type: Schema.Types.ObjectId, ref: 'gh3904_0' } + }); + + var Person = db.model('gh3904', personSchema); + var Team = db.model('gh3904_0', teamSchema); + var Game = db.model('gh3904_1', gameSchema); + + var people = [ + { name: 'Shaq' }, + { name: 'Kobe' }, + { name: 'Horry' }, + { name: 'Duncan' }, + { name: 'Robinson' }, + { name: 'Johnson' } + ]; + + Person.create(people, function(error, people) { + assert.ifError(error); + var lakers = { + name: 'Lakers', + members: [people[0]._id, people[1]._id, people[2]._id] + }; + var spurs = { + name: 'Spurs', + members: [people[3]._id, people[4]._id, people[5]._id] + }; + var teams = [lakers, spurs]; + Team.create(teams, function(error, teams) { + assert.ifError(error); + var game = { team: teams[0]._id, opponent: teams[1]._id }; + Game.create(game, function(error, game) { + assert.ifError(error); + test(game._id); + }); + }); + }); + + function test(id) { + var query = Game.findById(id).populate({ + path: 'team', + select: 'name members', + populate: { path: 'members', select: 'name' } + }); + query.exec(function(error, doc) { + assert.ifError(error); + var arr = _.map(doc.toObject().team.members, function(v) { + return v.name; + }); + assert.deepEqual(arr, ['Shaq', 'Kobe', 'Horry']); + done(); + }); + } + }); + + it('deep populate array -> array (gh-3954)', function(done) { + var personSchema = new Schema({ + name: { type: String } + }); + + var teamSchema = new Schema({ + name: { type: String }, + members: [{ type: Schema.Types.ObjectId, ref: 'gh3954' }] + }); + + var gameSchema = new Schema({ + teams: [{ type: Schema.Types.ObjectId, ref: 'gh3954_0' }] + }); + + var Person = db.model('gh3954', personSchema); + var Team = db.model('gh3954_0', teamSchema); + var Game = db.model('gh3954_1', gameSchema); + + var people = [ + { name: 'Shaq' }, + { name: 'Kobe' }, + { name: 'Horry' }, + { name: 'Duncan' }, + { name: 'Robinson' }, + { name: 'Johnson' } + ]; + + Person.create(people, function(error, people) { + assert.ifError(error); + var lakers = { + name: 'Lakers', + members: [people[0]._id, people[1]._id, people[2]._id] + }; + var spurs = { + name: 'Spurs', + members: [people[3]._id, people[4]._id, people[5]._id] + }; + var teams = [lakers, spurs]; + Team.create(teams, function(error, teams) { + assert.ifError(error); + var game = { + teams: [teams[0]._id, teams[1]._id] + }; + Game.create(game, function(error, game) { + assert.ifError(error); + test(game._id); + }); + }); + }); + + function test(id) { + var query = Game.findById(id).populate({ + path: 'teams', + select: 'name members', + populate: { path: 'members', select: 'name' } + }); + query.exec(function(error, doc) { + assert.ifError(error); + var players = doc.toObject().teams[0].members. + concat(doc.toObject().teams[1].members); + var arr = _.map(players, function(v) { + return v.name; + }); + assert.deepEqual(arr, + ['Shaq', 'Kobe', 'Horry', 'Duncan', 'Robinson', 'Johnson']); + done(); + }); + } + }); + + it('4 level population (gh-3973)', function(done) { + var level4Schema = new Schema({ + name: { type: String } + }); + + var level3Schema = new Schema({ + name: { type: String }, + level4: [{ type: Schema.Types.ObjectId, ref: 'level_4' }] + }); + + var level2Schema = new Schema({ + name: { type: String }, + level3: [{ type: Schema.Types.ObjectId, ref: 'level_3' }] + }); + + var level1Schema = new Schema({ + name: { type: String }, + level2: [{ type: Schema.Types.ObjectId, ref: 'level_2' }] + }); + + var level4 = db.model('level_4', level4Schema); + var level3 = db.model('level_3', level3Schema); + var level2 = db.model('level_2', level2Schema); + var level1 = db.model('level_1', level1Schema); + + var l4docs = [{ name: 'level 4' }]; + + level4.create(l4docs, function(error, l4) { + assert.ifError(error); + var l3docs = [{ name: 'level 3', level4: l4[0]._id }]; + level3.create(l3docs, function(error, l3) { + assert.ifError(error); + var l2docs = [{ name: 'level 2', level3: l3[0]._id }]; + level2.create(l2docs, function(error, l2) { + assert.ifError(error); + var l1docs = [{ name: 'level 1', level2: l2[0]._id }]; + level1.create(l1docs, function(error, l1) { + assert.ifError(error); + var opts = { + path: 'level2', + populate: { + path: 'level3', + populate: { + path: 'level4' + } + } + }; + level1.findById(l1[0]._id).populate(opts).exec(function(error, obj) { + assert.ifError(error); + assert.equal(obj.level2[0].level3[0].level4[0].name, 'level 4'); + done(); + }); + }); + }); + }); + }); + }); + + it('deep populate two paths (gh-3974)', function(done) { + var level3Schema = new Schema({ + name: { type: String } + }); + + var level2Schema = new Schema({ + name: { type: String }, + level31: [{ type: Schema.Types.ObjectId, ref: 'gh3974' }], + level32: [{ type: Schema.Types.ObjectId, ref: 'gh3974' }] + }); + + var level1Schema = new Schema({ + name: { type: String }, + level2: [{ type: Schema.Types.ObjectId, ref: 'gh3974_0' }] + }); + + var level3 = db.model('gh3974', level3Schema); + var level2 = db.model('gh3974_0', level2Schema); + var level1 = db.model('gh3974_1', level1Schema); + + var l3 = [ + { name: 'level 3/1' }, + { name: 'level 3/2' } + ]; + level3.create(l3, function(error, l3) { + assert.ifError(error); + var l2 = [ + { name: 'level 2', level31: l3[0]._id, level32: l3[1]._id } + ]; + level2.create(l2, function(error, l2) { + assert.ifError(error); + var l1 = [{ name: 'level 1', level2: l2[0]._id }]; + level1.create(l1, function(error, l1) { + assert.ifError(error); + level1.findById(l1[0]._id). + populate({ + path: 'level2', + populate: [{ + path: 'level31' + }] + }). + populate({ + path: 'level2', + populate: [{ + path: 'level32' + }] + }). + exec(function(error, obj) { + assert.ifError(error); + assert.equal(obj.level2[0].level31[0].name, 'level 3/1'); + assert.equal(obj.level2[0].level32[0].name, 'level 3/2'); + done(); + }); + }); + }); + }); + }); + + it('out-of-order discriminators (gh-4073)', function(done) { + var UserSchema = new Schema({ + name: String + }); + + var CommentSchema = new Schema({ + content: String + }); + + var BlogPostSchema = new Schema({ + title: String + }); + + var EventSchema = new Schema({ + name: String, + createdAt: { type: Date, default: Date.now } + }); + + var UserEventSchema = new Schema({ + user: { type: ObjectId, ref: 'gh4073_0' } + }); + + var CommentEventSchema = new Schema({ + comment: { type: ObjectId, ref: 'gh4073_1' } + }); + + var BlogPostEventSchema = new Schema({ + blogpost: { type: ObjectId, ref: 'gh4073_2' } + }); + + var User = db.model('gh4073_0', UserSchema); + var Comment = db.model('gh4073_1', CommentSchema); + var BlogPost = db.model('gh4073_2', BlogPostSchema); + + var Event = db.model('gh4073_3', EventSchema); + var UserEvent = Event.discriminator('User4073', UserEventSchema); + var CommentEvent = Event.discriminator('Comment4073', + CommentEventSchema); + var BlogPostEvent = Event.discriminator('BlogPost4073', BlogPostEventSchema); + + var u1 = new User({ name: 'user 1' }); + var u2 = new User({ name: 'user 2' }); + var u3 = new User({ name: 'user 3' }); + var c1 = new Comment({ content: 'comment 1' }); + var c2 = new Comment({ content: 'comment 2' }); + var c3 = new Comment({ content: 'comment 3' }); + var b1 = new BlogPost({ title: 'blog post 1' }); + var b2 = new BlogPost({ title: 'blog post 2' }); + var b3 = new BlogPost({ title: 'blog post 3' }); + var ue1 = new UserEvent({ user: u1 }); + var ue2 = new UserEvent({ user: u2 }); + var ue3 = new UserEvent({ user: u3 }); + var ce1 = new CommentEvent({ comment: c1 }); + var ce2 = new CommentEvent({ comment: c2 }); + var ce3 = new CommentEvent({ comment: c3 }); + var be1 = new BlogPostEvent({ blogpost: b1 }); + var be2 = new BlogPostEvent({ blogpost: b2 }); + var be3 = new BlogPostEvent({ blogpost: b3 }); + + async.series( + [ + u1.save.bind(u1), + u2.save.bind(u2), + u3.save.bind(u3), + + c1.save.bind(c1), + c2.save.bind(c2), + c3.save.bind(c3), + + b1.save.bind(b1), + b2.save.bind(b2), + b3.save.bind(b3), + + ce1.save.bind(ce1), + ue1.save.bind(ue1), + be1.save.bind(be1), + + ce2.save.bind(ce2), + ue2.save.bind(ue2), + be2.save.bind(be2), + + ce3.save.bind(ce3), + ue3.save.bind(ue3), + be3.save.bind(be3), + + function(next) { + Event. + find({}). + populate('user comment blogpost'). + exec(function(err, docs) { + docs.forEach(function(doc) { + if (doc.__t === 'User4073') { + assert.ok(doc.user.name.indexOf('user') !== -1); + } else if (doc.__t === 'Comment4073') { + assert.ok(doc.comment.content.indexOf('comment') !== -1); + } else if (doc.__t === 'BlogPost4073') { + assert.ok(doc.blogpost.title.indexOf('blog post') !== -1); + } else { + assert.ok(false); + } + }); + next(); + }); + } + ], + done + ); + }); + + it('dynref bug (gh-4104)', function(done) { + var PersonSchema = new Schema({ + name: { type: String } + }); + + var AnimalSchema = new Schema({ + name: { type: String } + }); + + var ThingSchema = new Schema({ + createdByModel: { type: String }, + createdBy: { + type: mongoose.Schema.Types.ObjectId, refPath: 'createdByModel' + } + }); + + var Thing = db.model('Thing4104', ThingSchema); + var Person = db.model('Person4104', PersonSchema); + var Animal = db.model('Animal4104', AnimalSchema); + + Person.create({ name: 'Val' }, function(error, person) { + assert.ifError(error); + Animal.create({ name: 'Air Bud' }, function(error, animal) { + assert.ifError(error); + var obj1 = { createdByModel: 'Person4104', createdBy: person._id }; + var obj2 = { createdByModel: 'Animal4104', createdBy: animal._id }; + Thing.create(obj1, obj2, function(error) { + assert.ifError(error); + Thing.find({}).populate('createdBy').exec(function(error, things) { + assert.ifError(error); + assert.ok(things[0].createdBy.name); + assert.ok(things[1].createdBy.name); + done(); + }); + }); + }); + }); + }); + + it('returned array has toObject() (gh-4656)', function(done) { + var demoWrapperSchema = new Schema({ + demo: [{ + type: String, + ref: 'gh4656' + }] + }); + var demoSchema = new Schema({ name: String }); + + var Demo = db.model('gh4656', demoSchema); + var DemoWrapper = db.model('gh4656_0', demoWrapperSchema); + + Demo.create({ name: 'test' }). + then(function(demo) { return DemoWrapper.create({ demo: [demo._id] }); }). + then(function(wrapper) { return DemoWrapper.findById(wrapper._id); }). + then(function(doc) { return doc.populate('demo').execPopulate(); }). + then(function(res) { + assert.equal(res.demo.toObject()[0].name, 'test'); + done(); + }). + catch(done); + }); + + it('empty array (gh-4284)', function(done) { + var PersonSchema = new Schema({ + name: { type: String } + }); + + var BandSchema = new Schema({ + people: [{ + type: mongoose.Schema.Types.ObjectId + }] + }); + + var Person = db.model('gh4284_b', PersonSchema); + var Band = db.model('gh4284_b0', BandSchema); + + var band = { people: [new mongoose.Types.ObjectId()] }; + Band.create(band, function(error, band) { + assert.ifError(error); + var opts = { path: 'people', model: Person }; + Band.findById(band).populate(opts).exec(function(error, band) { + assert.ifError(error); + assert.equal(band.people.length, 0); + done(); + }); + }); + }); + + it('empty populate string is a no-op (gh-4702)', function(done) { + var BandSchema = new Schema({ + people: [{ + type: mongoose.Schema.Types.ObjectId + }] + }); + + var Band = db.model('gh4702', BandSchema); + + var band = { people: [new mongoose.Types.ObjectId()] }; + Band.create(band, function(error, band) { + assert.ifError(error); + Band.findById(band).populate('').exec(function(error, band) { + assert.ifError(error); + assert.equal(band.people.length, 1); + done(); + }); + }); + }); + + it('checks field name correctly with nested arrays (gh-4365)', function(done) { + var UserSchema = new mongoose.Schema({ + name: { + type: String, + default: '' + } + }); + db.model('gh4365_0', UserSchema); + + var GroupSchema = new mongoose.Schema({ + name: String, + members: [String] + }); + + var OrganizationSchema = new mongoose.Schema({ + members: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'gh4365_0' + }], + groups: [GroupSchema] + }); + var OrganizationModel = db.model('gh4365_1', OrganizationSchema); + + var org = { + members: [], + groups: [] + }; + OrganizationModel.create(org, function(error) { + assert.ifError(error); + OrganizationModel. + findOne({}). + populate('members', 'name'). + exec(function(error, org) { + assert.ifError(error); + org.groups.push({ name: 'Team Rocket' }); + org.save(function(error) { + assert.ifError(error); + org.groups[0].members.push('Jessie'); + assert.equal(org.groups[0].members[0], 'Jessie'); + org.save(function(error) { + assert.ifError(error); + assert.equal(org.groups[0].members[0], 'Jessie'); + done(); + }); + }); + }); + }); + }); + + describe('populate virtuals (gh-2562)', function() { + it('basic populate virtuals', function(done) { + var PersonSchema = new Schema({ + name: String, + band: String + }); + + var BandSchema = new Schema({ + name: String + }); + BandSchema.virtual('members', { + ref: 'gh2562', + localField: 'name', + foreignField: 'band' + }); + + var Person = db.model('gh2562', PersonSchema); + var Band = db.model('gh2562_0', BandSchema); + + var people = _.map(['Axl Rose', 'Slash'], function(v) { + return { name: v, band: 'Guns N\' Roses' }; + }); + Person.create(people, function(error) { + assert.ifError(error); + Band.create({ name: 'Guns N\' Roses' }, function(error) { + assert.ifError(error); + var query = { name: 'Guns N\' Roses' }; + Band.findOne(query).populate('members').exec(function(error, gnr) { + assert.ifError(error); + assert.equal(gnr.members.length, 2); + done(); + }); + }); + }); + }); + + it('multiple source docs', function(done) { + var PersonSchema = new Schema({ + name: String, + band: String + }); + + var BandSchema = new Schema({ + name: String + }); + BandSchema.virtual('members', { + ref: 'gh2562_a0', + localField: 'name', + foreignField: 'band' + }); + + var Person = db.model('gh2562_a0', PersonSchema); + var Band = db.model('gh2562_a1', BandSchema); + + var people = _.map(['Axl Rose', 'Slash'], function(v) { + return { name: v, band: 'Guns N\' Roses' }; + }); + people = people.concat(_.map(['Vince Neil', 'Nikki Sixx'], function(v) { + return { name: v, band: 'Motley Crue' }; + })); + Person.create(people, function(error) { + assert.ifError(error); + var bands = [ + { name: 'Guns N\' Roses' }, + { name: 'Motley Crue' } + ]; + Band.create(bands, function(error) { + assert.ifError(error); + Band. + find({}). + sort({ name: 1 }). + populate({ path: 'members', options: { sort: { name: 1 } } }). + exec(function(error, bands) { + assert.ifError(error); + + assert.equal(bands.length, 2); + assert.equal(bands[0].name, 'Guns N\' Roses'); + assert.equal(bands[0].members.length, 2); + assert.deepEqual(_.map(bands[0].members, 'name'), + ['Axl Rose', 'Slash']); + + assert.equal(bands[1].name, 'Motley Crue'); + assert.equal(bands[1].members.length, 2); + assert.deepEqual(_.map(bands[1].members, 'name'), + ['Nikki Sixx', 'Vince Neil']); + done(); + }); + }); + }); + }); + + it('source array', function(done) { + var PersonSchema = new Schema({ + name: String + }); + + var BandSchema = new Schema({ + name: String, + people: [String] + }); + BandSchema.virtual('members', { + ref: 'gh2562_b0', + localField: 'people', + foreignField: 'name' + }); + + var Person = db.model('gh2562_b0', PersonSchema); + var Band = db.model('gh2562_b1', BandSchema); + + var bands = [ + { name: 'Guns N\' Roses', people: ['Axl Rose', 'Slash'] }, + { name: 'Motley Crue', people: ['Vince Neil', 'Nikki Sixx'] } + ]; + var people = [ + { name: 'Axl Rose' }, + { name: 'Slash' }, + { name: 'Vince Neil' }, + { name: 'Nikki Sixx' } + ]; + + Person.create(people, function(error) { + assert.ifError(error); + Band.insertMany(bands, function(error) { + assert.ifError(error); + Band. + find({}). + sort({ name: 1 }). + populate({ path: 'members', options: { sort: { name: 1 } } }). + exec(function(error, bands) { + assert.ifError(error); + + assert.equal(bands.length, 2); + assert.equal(bands[0].name, 'Guns N\' Roses'); + assert.equal(bands[0].members.length, 2); + assert.deepEqual(_.map(bands[0].members, 'name'), + ['Axl Rose', 'Slash']); + + assert.equal(bands[1].name, 'Motley Crue'); + assert.equal(bands[1].members.length, 2); + assert.deepEqual(_.map(bands[1].members, 'name'), + ['Nikki Sixx', 'Vince Neil']); + + done(); + }); + }); + }); + }); + + it('multiple paths (gh-4234)', function(done) { + var PersonSchema = new Schema({ + name: String, + authored: [Number], + favorites: [Number] + }); + + var BlogPostSchema = new Schema({ + _id: Number, + title: String + }); + BlogPostSchema.virtual('authors', { + ref: 'gh4234', + localField: '_id', + foreignField: 'authored' + }); + BlogPostSchema.virtual('favoritedBy', { + ref: 'gh4234', + localField: '_id', + foreignField: 'favorites' + }); + + var Person = db.model('gh4234', PersonSchema); + var BlogPost = db.model('gh4234_0', BlogPostSchema); + + var blogPosts = [{ _id: 0, title: 'Bacon is Great' }]; + var people = [{ name: 'Val', authored: [0], favorites: [0] }]; + + Person.create(people, function(error) { + assert.ifError(error); + BlogPost.create(blogPosts, function(error) { + assert.ifError(error); + BlogPost. + findOne({ _id: 0 }). + populate('authors favoritedBy'). + exec(function(error, post) { + assert.ifError(error); + assert.equal(post.authors.length, 1); + assert.equal(post.authors[0].name, 'Val'); + assert.equal(post.favoritedBy.length, 1); + assert.equal(post.favoritedBy[0].name, 'Val'); + done(); + }); + }); + }); + }); + + it('in embedded array (gh-4928)', function(done) { + var PersonSchema = new Schema({ + name: String, + authored: [Number] + }); + + var BlogPostSchema = new Schema({ + _id: Number, + title: String + }); + BlogPostSchema.virtual('author', { + ref: 'gh4928', + localField: '_id', + foreignField: 'authored', + justOne: true + }); + + var CollectionSchema = new Schema({ + blogPosts: [BlogPostSchema] + }); + + var Person = db.model('gh4928', PersonSchema); + var Collection = db.model('gh4928_0', CollectionSchema); + + Person.create({ name: 'Val', authored: 1 }). + then(function() { + return Collection.create({ + blogPosts: [{ _id: 1, title: 'Test' }] + }); + }). + then(function(c) { + return Collection.findById(c._id).populate('blogPosts.author'); + }). + then(function(c) { + assert.equal(c.blogPosts[0].author.name, 'Val'); + done(); + }). + catch(done); + }); + + it('justOne option (gh-4263)', function(done) { + var PersonSchema = new Schema({ + name: String, + authored: [Number] + }); + + var BlogPostSchema = new Schema({ + _id: Number, + title: String + }); + BlogPostSchema.virtual('author', { + ref: 'gh4263', + localField: '_id', + foreignField: 'authored', + justOne: true + }); + + var Person = db.model('gh4263', PersonSchema); + var BlogPost = db.model('gh4263_0', BlogPostSchema); + + var blogPosts = [{ _id: 0, title: 'Bacon is Great' }]; + var people = [ + { name: 'Val', authored: [0] }, + { name: 'Test', authored: [0] } + ]; + + Person.create(people, function(error) { + assert.ifError(error); + BlogPost.create(blogPosts, function(error) { + assert.ifError(error); + BlogPost. + findOne({ _id: 0 }). + populate('author'). + exec(function(error, post) { + assert.ifError(error); + assert.strictEqual(Array.isArray(post.author), false); + assert.ok(post.author.name.match(/^(Val|Test)$/)); + done(); + }); + }); + }); + }); + + it('with no results and justOne (gh-4284)', function(done) { + var PersonSchema = new Schema({ + name: String, + authored: [Number] + }); + + var BlogPostSchema = new Schema({ + _id: Number, + title: String + }); + BlogPostSchema.virtual('author', { + ref: 'gh4284', + localField: '_id', + foreignField: 'authored', + justOne: true + }); + + var Person = db.model('gh4284', PersonSchema); + var BlogPost = db.model('gh4284_0', BlogPostSchema); + + var blogPosts = [ + { _id: 0, title: 'Bacon is Great' }, + { _id: 1, title: 'Bacon is OK' } + ]; + var people = [ + { name: 'Val', authored: [0] } + ]; + + Person.create(people, function(error) { + assert.ifError(error); + BlogPost.create(blogPosts, function(error) { + assert.ifError(error); + BlogPost. + find({}). + sort({ title: 1 }). + populate('author'). + exec(function(error, posts) { + assert.ifError(error); + assert.equal(posts[0].author.name, 'Val'); + assert.strictEqual(posts[1].author, null); + done(); + }); + }); + }); + }); + + it('with multiple results and justOne (gh-4329)', function(done) { + var UserSchema = new Schema({ + openId: { + type: String, + unique: true + } + }); + var TaskSchema = new Schema({ + openId: { + type: String + } + }); + + TaskSchema.virtual('user', { + ref: 'gh4329', + localField: 'openId', + foreignField: 'openId', + justOne: true + }); + + var User = db.model('gh4329', UserSchema); + var Task = db.model('gh4329_0', TaskSchema); + + User.create({ openId: 'user1' }, { openId: 'user2' }, function(error) { + assert.ifError(error); + Task.create({ openId: 'user1' }, { openId: 'user2' }, function(error) { + assert.ifError(error); + Task. + find(). + sort({ openId: 1 }). + populate('user'). + exec(function(error, tasks) { + assert.ifError(error); + + assert.ok(tasks[0].user); + assert.ok(tasks[1].user); + var users = tasks.map(function(task) { + return task.user.openId; + }); + assert.deepEqual(users, ['user1', 'user2']); + done(); + }); + }); + }); + }); + + it('hydrates properly (gh-4618)', function(done) { + var ASchema = new Schema({ + name: { type: String } + }); + + var BSchema = new Schema({ + name: { type: String }, + a_id: { type: ObjectId } + }, { + toObject: { virtuals: true }, + toJSON: { virtuals: true } + }); + + BSchema.virtual('a', { + ref: 'gh4618', + localField: 'a_id', + foreignField: '_id' + }); + + var A = db.model('gh4618', ASchema); + var B = db.model('gh4618_0', BSchema); + + A.create({ name: 'test' }). + then(function(a) { + return B.create({ name: 'test2', a_id: a._id }); + }). + then(function(b) { return B.findById(b).populate('a').exec(); }). + then(function(b) { + assert.equal(b.toObject().a[0].name, 'test'); + done(); + }). + catch(done); + }); + + it('with functions for localField and foreignField (gh-5704)', function(done) { + var ASchema = new Schema({ + name: String + }); + + var BSchema = new Schema({ + name: String, + localField: String, + firstId: ObjectId, + secondId: ObjectId + }, { + toObject: { virtuals: true }, + toJSON: { virtuals: true } + }); + + BSchema.virtual('a', { + ref: 'gh5704', + localField: function() { return this.localField; }, + foreignField: function() { return '_id'; }, + justOne: true + }); + + var A = db.model('gh5704', ASchema); + var B = db.model('gh5704_0', BSchema); + + A.create([{ name: 'test1' }, { name: 'test2' }]). + then(function(arr) { + return B.create([ + { + name: 'b1', + localField: 'firstId', + firstId: arr[0]._id, + secondId: arr[1]._id + }, + { + name: 'b2', + localField: 'secondId', + firstId: arr[0]._id, + secondId: arr[1]._id + } + ]); + }). + then(function() { + return B.find().populate('a').sort([['name', 1]]).exec(); + }). + then(function(bs) { + assert.equal(bs[0].a.name, 'test1'); + assert.equal(bs[1].a.name, 'test2'); + done(); + }). + catch(done); + }); + + it('with functions for ref (gh-5602)', function(done) { + var ASchema = new Schema({ + name: String + }); + + var BSchema = new Schema({ + referencedModel: String, + aId: ObjectId + }); + + BSchema.virtual('a', { + ref: function() { return this.referencedModel; }, + localField: 'aId', + foreignField: '_id', + justOne: true + }); + + var A1 = db.model('gh5602_1', ASchema); + var A2 = db.model('gh5602_2', ASchema); + var B = db.model('gh5602_0', BSchema); + + A1.create({ name: 'a1' }). + then(function(a1) { + return A2.create({ name: 'a2' }).then(function(res) { + return [a1].concat(res); + }); + }). + then(function(as) { + return B.create([ + { name: 'test1', referencedModel: 'gh5602_1', aId: as[0]._id }, + { name: 'test2', referencedModel: 'gh5602_2', aId: as[1]._id } + ]); + }). + then(function() { + return B.find().populate('a').sort([['name', 1]]); + }). + then(function(bs) { + assert.equal(bs.length, 2); + var names = bs.map(function(b) { return b.a.name; }).sort(); + assert.deepEqual(names, ['a1', 'a2']); + done(); + }). + catch(done); + }); + + it('with no results (gh-4284)', function(done) { + var PersonSchema = new Schema({ + name: String, + authored: [Number] + }); + + var BlogPostSchema = new Schema({ + _id: Number, + title: String + }); + BlogPostSchema.virtual('authors', { + ref: 'gh4284_a', + localField: '_id', + foreignField: 'authored' + }); + + var Person = db.model('gh4284_a', PersonSchema); + var BlogPost = db.model('gh4284_a0', BlogPostSchema); + + var blogPosts = [ + { _id: 0, title: 'Bacon is Great' }, + { _id: 1, title: 'Bacon is OK' }, + { _id: 2, title: 'Bacon is not great' } + ]; + var people = [ + { name: 'Val', authored: [0] }, + { name: 'Test', authored: [0, 1] } + ]; + + Person.create(people, function(error) { + assert.ifError(error); + BlogPost.create(blogPosts, function(error) { + assert.ifError(error); + BlogPost. + find({}). + sort({ _id: 1 }). + populate('authors'). + exec(function(error, posts) { + assert.ifError(error); + var arr = posts[0].toObject({ virtuals: true }).authors. + map(function(v) { + return v.name; + }). + sort(); + assert.deepEqual(arr, ['Test', 'Val']); + assert.equal(posts[1].authors.length, 1); + assert.equal(posts[1].authors[0].name, 'Test'); + assert.equal(posts[2].authors.length, 0); + done(); + }); + }); + }); + }); + + it('deep populate virtual -> conventional (gh-4261)', function(done) { + var PersonSchema = new Schema({ + name: String + }); + + PersonSchema.virtual('blogPosts', { + ref: 'gh4261', + localField: '_id', + foreignField: 'author' + }); + + var BlogPostSchema = new Schema({ + title: String, + author: { type: ObjectId }, + comments: [{ author: { type: ObjectId, ref: 'gh4261' } }] + }); + + var Person = db.model('gh4261', PersonSchema); + var BlogPost = db.model('gh4261_0', BlogPostSchema); + + var people = [ + { name: 'Val' }, + { name: 'Test' } + ]; + + Person.create(people, function(error, people) { + assert.ifError(error); + var post = { + title: 'Test1', + author: people[0]._id, + comments: [{ author: people[1]._id }] + }; + BlogPost.create(post, function(error) { + assert.ifError(error); + Person.findById(people[0]._id). + populate({ + path: 'blogPosts', + model: BlogPost, + populate: { + path: 'comments.author', + model: Person + } + }). + exec(function(error, person) { + assert.ifError(error); + assert.equal(person.blogPosts[0].comments[0].author.name, + 'Test'); + done(); + }); + }); + }); + }); + + it('deep populate virtual -> virtual (gh-4278)', function(done) { + var ASchema = new Schema({ + name: String + }); + ASchema.virtual('bs', { + ref: 'gh4278_1', + localField: '_id', + foreignField: 'a' + }); + + var BSchema = new Schema({ + a: mongoose.Schema.Types.ObjectId, + name: String + }); + BSchema.virtual('cs', { + ref: 'gh4278_2', + localField: '_id', + foreignField: 'b' + }); + + var CSchema = new Schema({ + b: mongoose.Schema.Types.ObjectId, + name: String + }); + + var A = db.model('gh4278_0', ASchema); + var B = db.model('gh4278_1', BSchema); + var C = db.model('gh4278_2', CSchema); + + A.create({ name: 'A1' }, function(error, a) { + assert.ifError(error); + B.create({ name: 'B1', a: a._id }, function(error, b) { + assert.ifError(error); + C.create({ name: 'C1', b: b._id }, function(error) { + assert.ifError(error); + var options = { + path: 'bs', + populate: { + path: 'cs' + } + }; + A.findById(a).populate(options).exec(function(error, res) { + assert.ifError(error); + assert.equal(res.bs.length, 1); + assert.equal(res.bs[0].name, 'B1'); + assert.equal(res.bs[0].cs.length, 1); + assert.equal(res.bs[0].cs[0].name, 'C1'); + done(); + }); + }); + }); + }); + }); + + it('source array (gh-4585)', function(done) { + var tagSchema = new mongoose.Schema({ + name: String, + tagId: { type:String, unique:true } + }); + + var blogPostSchema = new mongoose.Schema({ + name : String, + body: String, + tags : [String] + }); + + blogPostSchema.virtual('tagsDocuments', { + ref: 'gh4585', // model + localField: 'tags', + foreignField: 'tagId' + }); + + var Tag = db.model('gh4585', tagSchema); + var BlogPost = db.model('gh4585_0', blogPostSchema); + + var tags = [ + { + name : 'angular.js', + tagId : 'angular' + }, + { + name : 'node.js', + tagId : 'node' + }, + { + name : 'javascript', + tagId : 'javascript' + } + ]; + + Tag.create(tags). + then(function() { + return BlogPost.create({ + title: 'test', + tags: ['angular', 'javascript'] + }); + }). + then(function(post) { + return BlogPost.findById(post._id).populate('tagsDocuments'); + }). + then(function(doc) { + assert.equal(doc.tags[0], 'angular'); + assert.equal(doc.tags[1], 'javascript'); + assert.equal(doc.tagsDocuments[0].tagId, 'angular'); + assert.equal(doc.tagsDocuments[1].tagId, 'javascript'); + done(); + }). + catch(done); + }); + + it('lean with single result and no justOne (gh-4288)', function(done) { + var PersonSchema = new Schema({ + name: String, + authored: [Number] + }); + + var BlogPostSchema = new Schema({ + _id: Number, + title: String + }); + BlogPostSchema.virtual('authors', { + ref: true, + localField: '_id', + foreignField: 'authored', + justOne: false + }); + + var Person = db.model('gh4288', PersonSchema); + var BlogPost = db.model('gh4288_0', BlogPostSchema); + + var blogPosts = [ + { _id: 0, title: 'Bacon is Great' } + ]; + var people = [ + { name: 'Val', authored: [0] } + ]; + + Person.create(people, function(error) { + assert.ifError(error); + BlogPost.create(blogPosts, function(error) { + assert.ifError(error); + BlogPost. + findOne({}). + lean(). + populate({ path: 'authors', model: Person }). + exec(function(error, post) { + assert.ifError(error); + assert.equal(post.authors.length, 1); + assert.equal(post.authors[0].name, 'Val'); + done(); + }); + }); + }); + }); + + it('gh-4923', function(done) { + var ClusterSchema = new Schema({ + name: String + }); + var Cluster = db.model('gh4923', ClusterSchema); + + var ZoneSchema = new Schema({ + name: String, + clusters: { + type: [ObjectId], + ref: 'gh4923' + } + }); + var Zone = db.model('gh4923_1', ZoneSchema); + + var DocSchema = new Schema({ + activity: [{ + cluster: { + type: ObjectId, + ref: 'gh4923' + }, + intensity: Number + }] + }); + DocSchema.virtual('activity.zones', { + ref: 'gh4923_1', + localField: 'activity.cluster', + foreignField: 'clusters' + }); + DocSchema.set('toObject', {virtuals: true}); + DocSchema.set('toJSON', {virtuals: true}); + var Doc = db.model('gh4923_2', DocSchema); + + Cluster.create([{ name: 'c1' }, { name: 'c2' }, { name: 'c3' }]). + then(function(c) { + return Zone.create([ + { name: 'z1', clusters: [c[0]._id, c[1]._id, c[2]._id] }, + { name: 'z2', clusters: [c[0]._id, c[2]._id] } + ]).then(function() { return c; }); + }). + then(function(c) { + return Doc.create({ + activity: [ + { cluster: c[0]._id, intensity: 1 }, + { cluster: c[1]._id, intensity: 2 } + ] + }); + }). + then(function() { + return Doc. + findOne({}). + populate('activity.cluster'). + populate('activity.zones', 'name clusters'). + exec(function(error, res) { + assert.ifError(error); + // Fails if this `.toObject()` is omitted, issue #4926 + res = res.toObject({ virtuals: true }); + var compare = function(a, b) { + if (a.name < b.name) { + return -1; + } else if (b.name < a.name) { + return 1; + } + return 0; + }; + res.activity[0].zones.sort(compare); + res.activity[1].zones.sort(compare); + assert.equal(res.activity[0].zones[0].name, 'z1'); + assert.equal(res.activity[1].zones[0].name, 'z1'); + done(); + }); + }). + catch(done); + }); + + it('supports setting default options in schema (gh-4741)', function(done) { + var sessionSchema = new Schema({ + date: { type: Date }, + user: { type: Schema.ObjectId, ref: 'User' } + }); + + var userSchema = new Schema({ + name: String + }); + + userSchema.virtual('sessions', { + ref: 'gh4741', + localField: '_id', + foreignField: 'user', + options: { sort: { date: -1 }, limit: 2 } + }); + + var Session = db.model('gh4741', sessionSchema); + var User = db.model('gh4741_0', userSchema); + + User.create({ name: 'Val' }). + then(function(user) { + return Session.create([ + { date: '2011-06-01', user: user._id }, + { date: '2011-06-02', user: user._id }, + { date: '2011-06-03', user: user._id } + ]); + }). + then(function(sessions) { + return User.findById(sessions[0].user).populate('sessions'); + }). + then(function(user) { + assert.equal(user.sessions.length, 2); + assert.equal(user.sessions[0].date.valueOf(), + new Date('2011-06-03').valueOf()); + assert.equal(user.sessions[1].date.valueOf(), + new Date('2011-06-02').valueOf()); + done(); + }). + catch(done); + }); + + it('handles populate with 0 args (gh-5036)', function(done) { + var userSchema = new Schema({ + name: String + }); + + var User = db.model('gh5036', userSchema); + + User.findOne().populate().exec(function(error) { + assert.ifError(error); + done(); + }); + }); + + it('auto select populated fields (gh-5669) (gh-5685)', function(done) { + var ProductSchema = new mongoose.Schema({ + name: { + type: String + }, + categories: { + type: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'gh5669' + }], + select: false + } + }); + + var CategorySchema = new Schema({ name: String }); + var Product = db.model('gh5669_0', ProductSchema); + var Category = db.model('gh5669', CategorySchema); + + Category.create({ name: 'Books' }, function(error, doc) { + assert.ifError(error); + var product = { + name: 'Professional AngularJS', + categories: [doc._id] + }; + Product.create(product, function(error, product) { + assert.ifError(error); + Product.findById(product._id).populate('categories').exec(function(error, product) { + assert.ifError(error); + assert.equal(product.categories.length, 1); + assert.equal(product.categories[0].name, 'Books'); + Product.findById(product._id).populate('categories').select({ categories: 0 }).exec(function(error, product) { + assert.ifError(error); + assert.ok(!product.categories); + Product.findById(product._id).select({ name: 0 }).populate('categories').exec(function(error, product) { + assert.ifError(error); + assert.equal(product.categories.length, 1); + assert.equal(product.categories[0].name, 'Books'); + assert.ok(!product.name); + done(); + }); + }); + }); + }); + }); + }); + + it('handles populating with discriminators that may not have a ref (gh-4817)', function(done) { + var imagesSchema = new mongoose.Schema({ + name: { + type: String, + required: true + } + }); + var Image = db.model('gh4817', imagesSchema, 'images'); + + var fieldSchema = new mongoose.Schema({ + name: { + type: String, + required: true + } + }); + var Field = db.model('gh4817_0', fieldSchema, 'fields'); + + var imageFieldSchema = new mongoose.Schema({ + value: { + type: mongoose.Schema.Types.ObjectId, + ref: 'gh4817', + default: null + } + }); + var FieldImage = Field.discriminator('gh4817_1', imageFieldSchema); + + var textFieldSchema = new mongoose.Schema({ + value: { + type: Schema.Types.Mixed, + required: true, + default: {} + } + }); + var FieldText = Field.discriminator('gh4817_2', textFieldSchema); + + var objectSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + fields: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'gh4817_0' + }] + }); + var ObjectModel = db.model('gh4817_3', objectSchema, 'objects'); + + Image.create({ name: 'testimg' }). + then(function(image) { + return FieldImage.create({ name: 'test', value: image._id }); + }). + then(function(fieldImage) { + return FieldText.create({ name: 'test', value: 'test' }). + then(function(fieldText) { + return [fieldImage, fieldText]; + }); + }). + then(function(fields) { + return ObjectModel.create({ fields: fields, name: 'test' }); + }). + then(function(obj) { + return ObjectModel.findOne({ _id: obj._id }).populate({ + path: 'fields', + populate: { + path: 'value' + } + }); + }). + then(function(obj) { + assert.equal(obj.fields.length, 2); + assert.equal(obj.fields[0].value.name, 'testimg'); + assert.equal(obj.fields[1].value, 'test'); + done(); + }). + catch(done); + }); + + it('populate with no ref using Model.populate (gh-4843)', function(done) { + var schema = new Schema({ + parent: mongoose.Schema.Types.ObjectId, + name: String + }); + + var Person = db.model('gh4843', schema); + + Person.create({ name: 'Anakin' }). + then(function(parent) { + return Person.create({ name: 'Luke', parent: parent._id }); + }). + then(function(luke) { + return Person.findById(luke._id); + }). + then(function(luke) { + return Person.populate(luke, { path: 'parent', model: 'gh4843' }); + }). + then(function(luke) { + assert.equal(luke.parent.name, 'Anakin'); + done(); + }). + catch(done); + }); + + it('nested populate, virtual -> normal (gh-4631)', function(done) { + var PersonSchema = new Schema({ + name: String + }); + + PersonSchema.virtual('blogPosts', { + ref: 'gh4631_0', + localField: '_id', + foreignField: 'author' + }); + + var BlogPostSchema = new Schema({ + title: String, + author: { type: ObjectId }, + comments: [{ author: { type: ObjectId, ref: 'gh4631' } }] + }); + + var Person = db.model('gh4631', PersonSchema); + var BlogPost = db.model('gh4631_0', BlogPostSchema); + + var people = [ + { name: 'Val' }, + { name: 'Test' } + ]; + + Person.create(people, function(error, people) { + assert.ifError(error); + var post = { + title: 'Test1', + author: people[0]._id, + comments: [{ author: people[1]._id }] + }; + BlogPost.create(post, function(error) { + assert.ifError(error); + + Person.findById(people[0]._id). + populate({ + path: 'blogPosts', + model: BlogPost, + populate: { + path: 'author', + model: Person + } + }). + exec(function(error, doc) { + assert.ifError(error); + assert.equal(doc.blogPosts.length, 1); + assert.equal(doc.blogPosts[0].author.name, 'Val'); + done(); + }); + }); + }); + }); + + it('populate with Decimal128 as ref (gh-4759)', function(done) { + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + done(); + return; + } + + test(); + }); + + function test() { + var parentSchema = new Schema({ + name: String, + child: { + type: 'Decimal128', + ref: 'gh4759' + } + }); + + var childSchema = new Schema({ + _id: 'Decimal128', + name: String + }); + + var Child = db.model('gh4759', childSchema); + var Parent = db.model('gh4759_0', parentSchema); + + var decimal128 = childSchema.path('_id').cast('1.337e+3'); + Child.create({ name: 'Luke', _id: '1.337e+3' }). + then(function() { + return Parent.create({ name: 'Anakin', child: decimal128.bytes }); + }). + then(function(parent) { + return Parent.findById(parent._id).populate('child'); + }). + then(function(parent) { + assert.equal(parent.child.name, 'Luke'); + assert.equal(parent.child._id.toString(), '1337'); + done(); + }). + catch(done); + } + }); + + it('handles circular virtual -> regular (gh-5128)', function(done) { + var ASchema = new Schema({ + title: { type: String, required: true, trim : true } + }); + + ASchema.virtual('brefs', { + ref: 'gh5128_0', + localField: '_id', + foreignField: 'arefs' + }); + + var BSchema = new Schema({ + arefs: [{ type: ObjectId, required: true, ref: 'gh5128' }] + }); + + var a = db.model('gh5128', ASchema); + var b = db.model('gh5128_0', BSchema); + + var id1 = new mongoose.Types.ObjectId(); + + a.create({ _id: id1, title: 'test' }). + then(function() { return b.create({ arefs: [id1] }); }). + then(function() { + return a.findOne({ _id: id1 }).populate([{ + path: 'brefs', // this gets populated + model: 'gh5128_0', + populate: [{ + path: 'arefs', // <---- this is returned as [ObjectId], not populated + model: 'gh5128' + }] + }]); + }). + then(function(doc) { + assert.equal(doc.brefs[0].arefs[0].title, 'test'); + done(); + }). + catch(done); + }); + + it('handles nested virtuals (gh-4851)', function(done) { + var AuthorSchema = new Schema({ name: String }); + + var BookSchema = new Schema({ + title: String, + author: { id: mongoose.Schema.Types.ObjectId } + }); + + BookSchema.virtual('author.doc', { + ref: 'Author', + foreignField: '_id', + localField: 'author.id', + justOne: true + }); + + var Author = db.model('Author', AuthorSchema); + var Book = db.model('Book', BookSchema); + + Author.create({ name: 'Val' }). + then(function(author) { + return Book.create({ + title: 'Professional AngularJS', + author: { id: author._id } + }); + }). + then(function(book) { + return Book.findById(book).populate('author.doc'); + }). + then(function(doc) { + assert.equal(doc.author.doc.name, 'Val'); + doc = doc.toObject({ virtuals: true }); + assert.equal(doc.author.doc.name, 'Val'); + done(); + }). + catch(done); + }); + + it('nested virtuals if top-level prop doesnt exist (gh-5431)', function(done) { + var personSchema = new mongoose.Schema({ + name: String, + band: String + }); + var bandSchema = new mongoose.Schema({ + name: String, + data: { + field: String + } + }); + bandSchema.virtual('data.members', { + ref: 'gh5431', + localField: 'name', + foreignField: 'band', + justOne: false + }); + + bandSchema.set('toObject', { virtuals: true }); + + var Person = db.model('gh5431', personSchema); + var Band = db.model('gh5431_0', bandSchema); + + Band.create({ name: 'Motley Crue', data: {} }). + then(function() { + return Person.create({ name: 'Vince Neil', band: 'Motley Crue' }); + }). + then(function() { + return Band.findOne({}).populate('data.members'); + }). + then(function(band) { + assert.equal(band.data.members.length, 1); + assert.equal(band.data.members[0].name, 'Vince Neil'); + done(); + }). + catch(done); + }); + + it('nested virtuals + doc.populate() (gh-5240)', function(done) { + var parentSchema = new Schema({ name: String }); + var childSchema = new Schema({ + parentId: mongoose.Schema.Types.ObjectId + }); + childSchema.virtual('parent', { + ref: 'gh5240', + localField: 'parentId', + foreignField: '_id', + justOne: true + }); + var teamSchema = new Schema({ people: [childSchema] }); + + var Parent = db.model('gh5240', parentSchema); + var Team = db.model('gh5240_0', teamSchema); + + Parent.create({ name: 'Darth Vader' }). + then(function(doc) { + return Team.create({ people: [{ parentId: doc._id }] }); + }). + then(function(team) { + return Team.findById(team._id); + }). + then(function(team) { + return team.populate('people.parent').execPopulate(); + }). + then(function(team) { + team = team.toObject({ virtuals: true }); + assert.equal(team.people[0].parent.name, 'Darth Vader'); + done(); + }). + catch(done); + }); + + it('no ref + cursor (gh-5334)', function(done) { + var parentSchema = new Schema({ + name: String, + child: mongoose.Schema.Types.ObjectId + }); + var childSchema = new Schema({ + name: String + }); + + var Parent = db.model('gh5334_0', parentSchema); + var Child = db.model('gh5334', childSchema); + + Child.create({ name: 'Luke' }, function(error, child) { + assert.ifError(error); + Parent.create({ name: 'Vader', child: child._id }, function(error) { + assert.ifError(error); + Parent.find().populate({ path: 'child', model: 'gh5334' }).cursor().next(function(error, doc) { + assert.ifError(error); + assert.equal(doc.child.name, 'Luke'); + done(); + }); + }); + }); + }); + + it('retains limit when using cursor (gh-5468)', function(done) { + var refSchema = new mongoose.Schema({ + _id: Number, + name: String + }, { versionKey: null }); + var Ref = db.model('gh5468', refSchema); + + var testSchema = new mongoose.Schema({ + _id: Number, + prevnxt: [{ type: Number, ref: 'gh5468' }] + }); + var Test = db.model('gh5468_0', testSchema); + + var docs = [1, 2, 3, 4, 5, 6].map(function(i) { + return { _id: i }; + }); + Ref.create(docs, function(error) { + assert.ifError(error); + var docs = [ + { _id: 1, prevnxt: [1, 2, 3] }, + { _id: 2, prevnxt: [4, 5, 6] } + ]; + Test.create(docs, function(error) { + assert.ifError(error); + + var cursor = Test. + find(). + populate({ path: 'prevnxt', options: { limit: 2 } }). + cursor(); + + cursor.on('data', function(doc) { + assert.equal(doc.prevnxt.length, 2); + }); + cursor.on('error', done); + cursor.on('end', function() { + done(); + }); + }); + }); + }); + + it('virtuals + doc.populate() (gh-5311)', function(done) { + var parentSchema = new Schema({ name: String }); + var childSchema = new Schema({ + parentId: mongoose.Schema.Types.ObjectId + }); + childSchema.virtual('parent', { + ref: 'gh5311', + localField: 'parentId', + foreignField: '_id', + justOne: true + }); + + var Parent = db.model('gh5311', parentSchema); + var Child = db.model('gh5311_0', childSchema); + + Parent.create({ name: 'Darth Vader' }). + then(function(doc) { + return Child.create({ parentId: doc._id }); + }). + then(function(c) { + return Child.findById(c._id); + }). + then(function(c) { + return c.populate('parent').execPopulate(); + }). + then(function(c) { + c = c.toObject({ virtuals: true }); + + assert.equal(c.parent.name, 'Darth Vader'); + done(); + }). + catch(done); + }); + + it('empty virtual with Model.populate (gh-5331)', function(done) { + var myModelSchema = new Schema({ + virtualRefKey: {type: String, ref: 'gh5331'} + }); + myModelSchema.set('toJSON', {virtuals:true}); + myModelSchema.virtual('populatedVirtualRef', { + ref: 'gh5331', + localField: 'virtualRefKey', + foreignField: 'handle' + }); + + var otherModelSchema = new Schema({ + handle: String + }); + + var MyModel = db.model('gh5331_0', myModelSchema); + db.model('gh5331', otherModelSchema); + + MyModel.create({ virtualRefKey: 'test' }, function(error, doc) { + assert.ifError(error); + MyModel.populate(doc, 'populatedVirtualRef', function(error, doc) { + assert.ifError(error); + assert.ok(doc.populatedVirtualRef); + assert.ok(Array.isArray(doc.populatedVirtualRef)); + done(); + }); + }); + }); + + it('virtual populate in single nested doc (gh-4715)', function(done) { + var someModelSchema = new mongoose.Schema({ + name: String + }); + + var SomeModel = db.model('gh4715', someModelSchema); + + var schema0 = new mongoose.Schema({ + name1: String + }); + + schema0.virtual('detail', { + ref: 'gh4715', + localField: '_id', + foreignField: '_id', + justOne: true + }); + + var schemaMain = new mongoose.Schema({ + name: String, + obj: schema0 + }); + + var ModelMain = db.model('gh4715_0', schemaMain); + + ModelMain.create({ name: 'Test', obj: {} }). + then(function(m) { + return SomeModel.create({ _id: m.obj._id, name: 'test' }); + }). + then(function() { + return ModelMain.findOne().populate('obj.detail'); + }). + then(function(m) { + assert.equal(m.obj.detail.name, 'test'); + done(); + }). + catch(done); + }); + + it('populate with missing schema (gh-5364)', function(done) { + var Foo = db.model('gh5364', new mongoose.Schema({ + bar: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Bar' + } + })); + + Foo.create({ bar: new mongoose.Types.ObjectId() }, function(error) { + assert.ifError(error); + Foo.find().populate('bar').exec(function(error) { + assert.ok(error); + assert.equal(error.name, 'MissingSchemaError'); + done(); + }); + }); + }); + + it('populate with missing schema (gh-5460)', function(done) { + var refSchema = new mongoose.Schema({ + name: String + }); + + db.model('gh5460', refSchema); + + var schema = new mongoose.Schema({ + ref: { type: mongoose.Schema.Types.ObjectId, ref: 'gh5460' } + }); + + var Model = db.model('gh5460_0', schema); + + var q = Model.find().read('secondaryPreferred').populate('ref'); + assert.equal(q._mongooseOptions.populate['ref'].options.readPreference.mode, + 'secondaryPreferred'); + done(); + }); + + it('virtuals with justOne false and foreign field not found (gh-5336)', function(done) { + var BandSchema = new mongoose.Schema({ + name: String, + active: Boolean + }); + + var Band = db.model('gh5336', BandSchema); + + var PersonSchema = new mongoose.Schema({ + name: String, + bands: [String] + }); + + PersonSchema.virtual('bandDetails', { + ref: 'gh5336', + localField: 'bands', + foreignField: 'name', + justOne: false + }); + var Person = db.model('gh5336_0', PersonSchema); + + var band = new Band({name: 'The Beatles', active: false}); + var person = new Person({ + name: 'George Harrison', + bands: ['The Beatles'] + }); + + person.save(). + then(function() { return band.save(); }). + then(function() { + return Person.findOne({ name: 'George Harrison' }); + }). + then(function(person) { + return person.populate({ + path: 'bandDetails', + match: { active: { $eq: true } } + }).execPopulate(); + }). + then(function(person) { + person = person.toObject({ virtuals: true }); + assert.deepEqual(person.bandDetails, []); + done(); + }). + catch(done); + }); + + it('virtuals with justOne true and foreign field not found (gh-5336)', function(done) { + var BandSchema = new mongoose.Schema({ + name: String, + active: Boolean + }); + + var Band = db.model('gh5336_10', BandSchema); + + var PersonSchema = new mongoose.Schema({ + name: String, + bands: [String] + }); + + PersonSchema.virtual('bandDetails', { + ref: 'gh5336_10', + localField: 'bands', + foreignField: 'name', + justOne: true + }); + var Person = db.model('gh5336_11', PersonSchema); + + var band = new Band({name: 'The Beatles', active: false}); + var person = new Person({ + name: 'George Harrison', + bands: ['The Beatles'] + }); + + person.save(). + then(function() { return band.save(); }). + then(function() { + return Person.findOne({ name: 'George Harrison' }); + }). + then(function(person) { + return person.populate({ + path: 'bandDetails', + match: { active: { $eq: true } } + }).execPopulate(); + }). + then(function(person) { + person = person.toObject({ virtuals: true }); + assert.strictEqual(person.bandDetails, null); + done(); + }). + catch(done); + }); + + it('select foreignField automatically (gh-4959)', function(done) { + var childSchema = new mongoose.Schema({ + name: String, + parentId: mongoose.Schema.Types.ObjectId + }); + + var Child = db.model('gh4959', childSchema); + + var parentSchema = new mongoose.Schema({ + name: String + }); + + parentSchema.virtual('detail', { + ref: 'gh4959', + localField: '_id', + foreignField: 'parentId', + justOne: true + }); + + var Parent = db.model('gh4959_0', parentSchema); + + Parent.create({ name: 'Test' }). + then(function(m) { + return Child.create({ name: 'test', parentId: m._id }); + }). + then(function() { + return Parent.find().populate({ path: 'detail', select: 'name' }); + }). + then(function(res) { + var m = res[0]; + assert.equal(m.detail.name, 'test'); + assert.ok(m.detail.parentId); + done(); + }). + catch(done); + }); + + it('does not set `populated()` until populate is done (gh-5564)', function() { + var userSchema = new mongoose.Schema({}); + var User = db.model('gh5564', userSchema); + + var testSchema = new mongoose.Schema({ + users: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'gh5564' + }] + }); + var Test = db.model('gh5564_0', testSchema); + + return User.create({}). + then(function(user) { + return Test.create({ users: [user._id] }); + }). + then(function(test) { + var promise = test.populate('users').execPopulate(); + assert.ok(!test.populated('users')); + return promise; + }). + then(function(test) { + assert.ok(test.populated('users')); + assert.ok(test.users[0]._id); + assert.equal(test.users.length, 1); + assert.equal(test.populated('users').length, 1); + }); + }); + + it('virtual populate toJSON output (gh-5542)', function(done) { + var AuthorSchema = new mongoose.Schema({ + name: String + }, { + toObject: { virtuals: true }, + toJSON: { virtuals: true } + }); + + var BookSchema = new mongoose.Schema({ + title: String, + author: { type: ObjectId, ref: 'gh5542' } + }); + + AuthorSchema.virtual('books', { + ref: 'gh5542_0', + localField: '_id', + foreignField: 'author', + justOne: true + }); + + var Author = db.model('gh5542', AuthorSchema); + var Book = db.model('gh5542_0', BookSchema); + + var author = new Author({ name: 'Bob' }); + author.save(). + then(function(author) { + var book = new Book({ name: 'Book', author: author._id }); + return book.save(); + }). + then(function() { + return Author.findOne({}) + .populate({ path: 'books', select: 'title' }) + .exec(); + }). + then(function(author) { + var json = author.toJSON(); + assert.deepEqual(Object.getOwnPropertyNames(json.books), + ['_id', 'author']); + done(); + }). + catch(done); + }); + + it('works if foreignField parent is selected (gh-5037)', function(done) { + var childSchema = new mongoose.Schema({ + name: String, + parent: { + id: mongoose.Schema.Types.ObjectId, + name: String + } + }); + + var Child = db.model('gh5037', childSchema); + + var parentSchema = new mongoose.Schema({ + name: String + }); + + parentSchema.virtual('detail', { + ref: 'gh5037', + localField: '_id', + foreignField: 'parent.id', + justOne: true + }); + + var Parent = db.model('gh5037_0', parentSchema); + + Parent.create({ name: 'Test' }). + then(function(m) { + return Child.create({ + name: 'test', + parent: { + id: m._id, + name: 'test2' + } + }); + }). + then(function() { + return Parent.find().populate({ + path: 'detail', + select: 'name parent' + }); + }). + then(function(res) { + var m = res[0]; + assert.equal(m.detail.name, 'test'); + assert.ok(m.detail.parent.id); + assert.equal(m.detail.parent.name, 'test2'); + done(); + }). + catch(done); + }); + + it('subPopulate under discriminators race condition (gh-5858)', function() { + var options = { discriminatorKey: 'kind' }; + var activitySchema = new Schema({ title: { type: String } }, options); + + var dateActivitySchema = new Schema({ + postedBy: { + type: Schema.Types.ObjectId, + ref: 'gh5858', + required: true + } + }, options); + + var eventActivitySchema = new Schema({ test: String }, options); + + var logSchema = new Schema({ + seq: Number, + activity: { + type: Schema.Types.ObjectId, + refPath: 'kind', + required: true + }, + kind: String + }, options); + + var User = db.model('gh5858', { name: String }); + var Activity = db.model('gh5858_0', activitySchema); + var DateActivity = Activity.discriminator('gh5858_1', dateActivitySchema); + var EventActivity = Activity.discriminator('gh5858_2', eventActivitySchema); + var Log = db.model('gh5858_3', logSchema); + + var dateActivity; + var eventActivity; + return User.create({ name: 'val' }). + then(function(user) { + return DateActivity.create({ title: 'test', postedBy: user._id }); + }). + then(function(_dateActivity) { + dateActivity = _dateActivity; + return EventActivity.create({ title: 'test' }); + }). + then(function(_eventActivity) { + eventActivity = _eventActivity; + return Log.create([ + { seq: 1, activity: eventActivity._id, kind: 'gh5858_2' }, + { seq: 2, activity: dateActivity._id, kind: 'gh5858_1' } + ]); + }). + then(function() { + return Log.find({}). + populate({ + path: 'activity', + populate: { path: 'postedBy' } + }). + sort({ seq:-1 }); + }). + then(function(results) { + assert.equal(results.length, 2); + assert.equal(results[0].activity.kind, 'gh5858_1' ); + assert.equal(results[0].activity.postedBy.name, 'val'); + assert.equal(results[1].activity.kind, 'gh5858_2' ); + assert.equal(results[1].activity.postedBy, null); + }); + }); + + it('populating nested discriminator path (gh-5970)', function() { + var Author = db.model('gh5970', new mongoose.Schema({ + firstName: { + type: String, + required: true + }, + lastName: { + type: String, + required: true + } + })); + + var ItemSchema = new mongoose.Schema({ + title: { + type: String, + required: true + } + }, {discriminatorKey: 'type'}); + + var ItemBookSchema = new mongoose.Schema({ + author: { + type: mongoose.Schema.ObjectId, + ref: 'gh5970' + } + }); + + var ItemEBookSchema = new mongoose.Schema({ + author: { + type: mongoose.Schema.ObjectId, + ref: 'gh5970' + }, + url: { + type: String + } + }); + + var BundleSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + items: [{ + type: ItemSchema, + required: false + }] + }); + + BundleSchema.path('items').discriminator('Book', ItemBookSchema); + BundleSchema.path('items').discriminator('EBook', ItemEBookSchema); + + var Bundle = db.model('gh5970_0', BundleSchema); + + return Author.create({firstName: 'David', lastName: 'Flanagan'}). + then(function(author) { + return Bundle.create({ + name: 'Javascript Book Collection', items: [ + {type: 'Book', title: 'JavaScript: The Definitive Guide', author: author}, + { + type: 'EBook', + title: 'JavaScript: The Definitive Guide Ebook', + url: 'https://google.com', + author: author + } + ] + }); + }). + then(function(bundle) { + return Bundle.findById(bundle._id).populate('items.author').lean(); + }). + then(function(bundle) { + assert.equal(bundle.items[0].author.firstName, 'David'); + assert.equal(bundle.items[1].author.firstName, 'David'); + }); + }); + + it('specify model in populate (gh-4264)', function(done) { + var PersonSchema = new Schema({ + name: String, + authored: [Number] + }); + + var BlogPostSchema = new Schema({ + _id: Number, + title: String + }); + BlogPostSchema.virtual('authors', { + ref: true, + localField: '_id', + foreignField: 'authored' + }); + + var Person = db.model('gh4264', PersonSchema); + var BlogPost = db.model('gh4264_0', BlogPostSchema); + + var blogPosts = [{ _id: 0, title: 'Bacon is Great' }]; + var people = [ + { name: 'Val', authored: [0] } + ]; + + Person.create(people, function(error) { + assert.ifError(error); + BlogPost.create(blogPosts, function(error) { + assert.ifError(error); + BlogPost. + findOne({ _id: 0 }). + populate({ path: 'authors', model: Person }). + exec(function(error, post) { + assert.ifError(error); + assert.equal(post.authors.length, 1); + assert.equal(post.authors[0].name, 'Val'); + done(); + }); + }); + }); }); }); }); diff --git a/test/model.query.casting.test.js b/test/model.query.casting.test.js index 5150843ac98..e9584588b55 100644 --- a/test/model.query.casting.test.js +++ b/test/model.query.casting.test.js @@ -4,84 +4,89 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , SchemaType = mongoose.SchemaType - , CastError = SchemaType.CastError - , ObjectId = Schema.Types.ObjectId - , DocumentObjectId = mongoose.Types.ObjectId; +var assert = require('power-assert'); +var random = require('../lib/utils').random; +var start = require('./common'); -/** - * Setup. - */ - -var Comments = new Schema; +var mongoose = start.mongoose; -Comments.add({ - title : String - , date : Date - , body : String - , comments : [Comments] -}); - -var BlogPostB = new Schema({ - title : { $type: String }, - author : String, - slug : String, - date : Date, - meta : { - date : Date, - visitors : Number - }, - published : Boolean, - mixed : {}, - numbers : [{ $type: Number }], - tags : [String], - sigs : [Buffer], - owners : [ObjectId], - comments : [Comments], - def : { $type: String, default: 'kandinsky' } -}, { typeKey: '$type' }); - -var modelName = 'model.query.casting.blogpost'; -mongoose.model(modelName, BlogPostB); -var collection = 'blogposts_' + random(); - -var geoSchemaArray = new Schema({ loc: { type: [Number], index: '2d'}}); -var geoSchemaObject = new Schema({ loc: { long: Number, lat: Number }}); -geoSchemaObject.index({'loc': '2d'}); +var CastError = mongoose.SchemaType.CastError; +var DocumentObjectId = mongoose.Types.ObjectId; +var ObjectId = mongoose.Schema.Types.ObjectId; +var Schema = mongoose.Schema; describe('model query casting', function() { + var Comments; + var BlogPostB; + var collection; + var geoSchemaArray; + var geoSchemaObject; + var modelName; + + before(function() { + Comments = new Schema; + + Comments.add({ + title: String, + date: Date, + body: String, + comments: [Comments] + }); + + BlogPostB = new Schema({ + title: {$type: String}, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number + }, + published: Boolean, + mixed: {}, + numbers: [{$type: Number}], + tags: [String], + sigs: [Buffer], + owners: [ObjectId], + comments: [Comments], + def: {$type: String, default: 'kandinsky'} + }, {typeKey: '$type'}); + + modelName = 'model.query.casting.blogpost'; + mongoose.model(modelName, BlogPostB); + collection = 'blogposts_' + random(); + + geoSchemaArray = new Schema({loc: {type: [Number], index: '2d'}}); + geoSchemaObject = new Schema({loc: {long: Number, lat: Number}}); + geoSchemaObject.index({loc: '2d'}); + }); it('works', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection) - , title = 'Loki ' + random(); + var db = start(), + BlogPostB = db.model(modelName, collection), + title = 'Loki ' + random(); - var post = new BlogPostB() - , id = post.get('_id').toString(); + var post = new BlogPostB(), + id = post.get('_id').toString(); post.set('title', title); post.save(function(err) { assert.ifError(err); - BlogPostB.findOne({ _id: id }, function(err, doc) { + BlogPostB.findOne({_id: id}, function(err, doc) { assert.ifError(err); - assert.equal(title, doc.get('title')); + assert.equal(doc.get('title'), title); db.close(done); }); }); }); it('returns cast errors', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); - BlogPostB.find({ date: 'invalid date' }, function(err) { + BlogPostB.find({date: 'invalid date'}, function(err) { assert.ok(err instanceof Error); assert.ok(err instanceof CastError); db.close(done); @@ -89,41 +94,41 @@ describe('model query casting', function() { }); it('casts $modifiers', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection) - , post = new BlogPostB({ - meta: { - visitors: -75 - } - }); + var db = start(), + BlogPostB = db.model(modelName, collection), + post = new BlogPostB({ + meta: { + visitors: -75 + } + }); post.save(function(err) { assert.ifError(err); - BlogPostB.find({ 'meta.visitors': { $gt: '-100', $lt: -50 } }, - function(err, found) { - assert.ifError(err); + BlogPostB.find({'meta.visitors': {$gt: '-100', $lt: -50}}, + function(err, found) { + assert.ifError(err); - assert.ok(found); - assert.equal(1, found.length); - assert.equal(found[0].get('_id').toString(), post.get('_id')); - assert.equal(found[0].get('meta.visitors').valueOf(), post.get('meta.visitors').valueOf()); - db.close(done); - }); + assert.ok(found); + assert.equal(found.length, 1); + assert.equal(found[0].get('_id').toString(), post.get('_id')); + assert.equal(found[0].get('meta.visitors').valueOf(), post.get('meta.visitors').valueOf()); + db.close(done); + }); }); }); it('casts $in values of arrays (gh-199)', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); - var post = new BlogPostB() - , id = post._id.toString(); + var post = new BlogPostB(), + id = post._id.toString(); post.save(function(err) { assert.ifError(err); - BlogPostB.findOne({ _id: { $in: [id] } }, function(err, doc) { + BlogPostB.findOne({_id: {$in: [id]}}, function(err, doc) { assert.ifError(err); assert.equal(doc._id.toString(), id); @@ -133,16 +138,16 @@ describe('model query casting', function() { }); it('casts $in values of arrays with single item instead of array (jrl-3238)', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); - var post = new BlogPostB() - , id = post._id.toString(); + var post = new BlogPostB(), + id = post._id.toString(); post.save(function(err) { assert.ifError(err); - BlogPostB.findOne({ _id: { $in: id } }, function(err, doc) { + BlogPostB.findOne({_id: {$in: id}}, function(err, doc) { assert.ifError(err); assert.equal(doc._id.toString(), id); @@ -153,24 +158,24 @@ describe('model query casting', function() { }); it('casts $nin values of arrays (gh-232)', function(done) { - var db = start() - , NinSchema = new Schema({ - num: Number - }); + var db = start(), + NinSchema = new Schema({ + num: Number + }); mongoose.model('Nin', NinSchema); var Nin = db.model('Nin', 'nins_' + random()); - Nin.create({ num: 1 }, function(err) { + Nin.create({num: 1}, function(err) { assert.ifError(err); - Nin.create({ num: 2 }, function(err) { + Nin.create({num: 2}, function(err) { assert.ifError(err); Nin.create({num: 3}, function(err) { assert.ifError(err); - Nin.find({ num: {$nin: [2]}}, function(err, found) { + Nin.find({num: {$nin: [2]}}, function(err, found) { assert.ifError(err); - assert.equal(2, found.length); + assert.equal(found.length, 2); db.close(done); }); }); @@ -179,8 +184,8 @@ describe('model query casting', function() { }); it('works when finding by Date (gh-204)', function(done) { - var db = start() - , P = db.model(modelName, collection); + var db = start(), + P = db.model(modelName, collection); var post = new P; @@ -189,7 +194,7 @@ describe('model query casting', function() { post.save(function(err) { assert.ifError(err); - P.findOne({ _id: post._id, 'meta.date': { $lte: Date.now() } }, function(err, doc) { + P.findOne({_id: post._id, 'meta.date': {$lte: Date.now()}}, function(err, doc) { assert.ifError(err); assert.equal(doc._id.toString(), post._id.toString()); @@ -207,13 +212,13 @@ describe('model query casting', function() { }); it('works with $type matching', function(done) { - var db = start() - , B = db.model(modelName, collection); + var db = start(); + var B = db.model(modelName, collection); - B.find({ title: { $type: "asd" }}, function(err) { - assert.equal(err.message,"$type parameter must be Number"); + B.find({title: {$type: {x:1}}}, function(err) { + assert.equal(err.message, '$type parameter must be number or string'); - B.find({ title: { $type: 2 }}, function(err, posts) { + B.find({title: {$type: 2}}, function(err, posts) { assert.ifError(err); assert.strictEqual(Array.isArray(posts), true); db.close(done); @@ -222,13 +227,13 @@ describe('model query casting', function() { }); it('works when finding Boolean with $in (gh-998)', function(done) { - var db = start() - , B = db.model(modelName, collection); + var db = start(), + B = db.model(modelName, collection); - var b = new B({ published: true }); + var b = new B({published: true}); b.save(function(err) { assert.ifError(err); - B.find({ _id: b._id, boolean: { $in: [null, true] }}, function(err, doc) { + B.find({_id: b._id, boolean: {$in: [null, true]}}, function(err, doc) { assert.ifError(err); assert.ok(doc); assert.equal(doc[0].id, b.id); @@ -238,10 +243,10 @@ describe('model query casting', function() { }); it('works when finding Boolean with $ne (gh-1093)', function(done) { - var db = start() - , B = db.model(modelName, collection + random()); + var db = start(), + B = db.model(modelName, collection + random()); - var b = new B({ published: false }); + var b = new B({published: false}); b.save(function(err) { assert.ifError(err); B.find().ne('published', true).exec(function(err, doc) { @@ -254,9 +259,9 @@ describe('model query casting', function() { }); it('properly casts $and (gh-1180)', function(done) { - var db = start() - , B = db.model(modelName, collection + random()) - , result = B.find({}).cast(B, {$and:[{date:'1987-03-17T20:00:00.000Z'}, {_id:'000000000000000000000000'}]}); + var db = start(), + B = db.model(modelName, collection + random()), + result = B.find({}).cast(B, {$and: [{date: '1987-03-17T20:00:00.000Z'}, {_id: '000000000000000000000000'}]}); assert.ok(result.$and[0].date instanceof Date); assert.ok(result.$and[1]._id instanceof DocumentObjectId); db.close(done); @@ -266,16 +271,18 @@ describe('model query casting', function() { this.slow(60); it('with arrays', function(done) { - var db = start() - , Test = db.model('Geo4', geoSchemaArray, "y" + random()); + var db = start(), + Test = db.model('Geo4', geoSchemaArray, 'y' + random()); Test.once('index', complete); - Test.create({ loc: [ 10, 20 ]}, { loc: [ 40, 90 ]}, complete); + Test.create({loc: [10, 20]}, {loc: [40, 90]}, complete); var pending = 2; function complete(err) { - if (complete.ran) return; + if (complete.ran) { + return; + } if (err) { db.close(); return done(complete.ran = err); @@ -284,23 +291,25 @@ describe('model query casting', function() { } function test() { - Test.find({ loc: { $near: ['30', '40'] }}, function(err, docs) { + Test.find({loc: {$near: ['30', '40']}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } }); it('with objects', function(done) { - var db = start() - , Test = db.model('Geo5', geoSchemaObject, "y" + random()); + var db = start(), + Test = db.model('Geo5', geoSchemaObject, 'y' + random()); var pending = 2; function complete(err) { - if (complete.ran) return; + if (complete.ran) { + return; + } if (err) { db.close(); return done(complete.ran = err); @@ -309,29 +318,31 @@ describe('model query casting', function() { } function test() { - Test.find({ loc: { $near: ['30', '40'], $maxDistance: 51 }}, function(err, docs) { + Test.find({loc: {$near: ['30', '40'], $maxDistance: 51}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } - Test.create({ loc: {long:10, lat:20 }}, { loc: {long:40, lat:90 }}, complete); + Test.create({loc: {long: 10, lat: 20}}, {loc: {long: 40, lat: 90}}, complete); Test.once('index', complete); }); it('with nested objects', function(done) { var db = start(); - var geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number }}}); + var geoSchemaObject = new Schema({loc: {nested: {long: Number, lat: Number}}}); geoSchemaObject.index({'loc.nested': '2d'}); - var Test = db.model('Geo52', geoSchemaObject, "y" + random()); + var Test = db.model('Geo52', geoSchemaObject, 'y' + random()); var pending = 2; function complete(err) { - if (complete.ran) return; + if (complete.ran) { + return; + } if (err) { db.close(); return done(complete.ran = err); @@ -340,19 +351,19 @@ describe('model query casting', function() { } function test() { - Test.find({ 'loc.nested': {$near: ['30', '40'], $maxDistance: '50' }}, function(err, docs) { + Test.find({'loc.nested': {$near: ['30', '40'], $maxDistance: '50'}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); } Test.once('index', complete); Test.create( - { loc: {nested:{long:10, lat:20 }}} - , { loc: {nested:{long:40, lat:90 }}} - , complete); + {loc: {nested: {long: 10, lat: 20}}}, + {loc: {nested: {long: 40, lat: 90}}}, + complete); }); }); @@ -360,48 +371,58 @@ describe('model query casting', function() { this.slow(70); it('with arrays', function(done) { - var db = start() - , Test = db.model('Geo4', geoSchemaArray, "y" + random()); + var db = start(), + Test = db.model('Geo4', geoSchemaArray, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: [ 10, 20 ]}, { loc: [ 40, 90 ]}, complete); + Test.create({loc: [10, 20]}, {loc: [40, 90]}, complete); function test() { - Test.find({ loc: { $nearSphere: ['30', '40'] }}, function(err, docs) { + Test.find({loc: {$nearSphere: ['30', '40']}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } }); it('with objects', function(done) { - var db = start() - , Test = db.model('Geo5', geoSchemaObject, "y" + random()); + var db = start(), + Test = db.model('Geo5', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {long:10, lat:20 }}, { loc: {long:40, lat:90 }}, complete); + Test.create({loc: {long: 10, lat: 20}}, {loc: {long: 40, lat: 90}}, complete); function test() { - Test.find({ loc: { $nearSphere: ['30', '40'], $maxDistance: 1 }}, function(err, docs) { + Test.find({loc: {$nearSphere: ['30', '40'], $maxDistance: 1}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } @@ -409,26 +430,31 @@ describe('model query casting', function() { it('with nested objects', function(done) { var db = start(); - var geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number }}}); + var geoSchemaObject = new Schema({loc: {nested: {long: Number, lat: Number}}}); geoSchemaObject.index({'loc.nested': '2d'}); - var Test = db.model('Geo52', geoSchemaObject, "y" + random()); + var Test = db.model('Geo52', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {nested:{long:10, lat:20 }}}, { loc: {nested:{long:40, lat:90 }}}, complete); + Test.create({loc: {nested: {long: 10, lat: 20}}}, {loc: {nested: {long: 40, lat: 90}}}, complete); function test() { - Test.find({ 'loc.nested': {$nearSphere: ['30', '40'], $maxDistance: 1 }}, function(err, docs) { + Test.find({'loc.nested': {$nearSphere: ['30', '40'], $maxDistance: 1}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } @@ -440,48 +466,58 @@ describe('model query casting', function() { describe('$centerSphere', function() { it('with arrays', function(done) { - var db = start() - , Test = db.model('Geo4', geoSchemaArray, "y" + random()); + var db = start(), + Test = db.model('Geo4', geoSchemaArray, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: [ 10, 20 ]}, { loc: [ 40, 90 ]}, complete); + Test.create({loc: [10, 20]}, {loc: [40, 90]}, complete); function test() { - Test.find({ loc: { $within: { $centerSphere: [['11', '20'], '0.4'] }}}, function(err, docs) { + Test.find({loc: {$within: {$centerSphere: [['11', '20'], '0.4']}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); } }); it('with objects', function(done) { - var db = start() - , Test = db.model('Geo5', geoSchemaObject, "y" + random()); + var db = start(), + Test = db.model('Geo5', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {long:10, lat:20 }}, { loc: {long:40, lat:90 }}, complete); + Test.create({loc: {long: 10, lat: 20}}, {loc: {long: 40, lat: 90}}, complete); function test() { - Test.find({ loc: { $within: { $centerSphere: [['11', '20'], '0.4'] }}}, function(err, docs) { + Test.find({loc: {$within: {$centerSphere: [['11', '20'], '0.4']}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); } @@ -489,26 +525,31 @@ describe('model query casting', function() { it('with nested objects', function(done) { var db = start(); - var geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number }}}); + var geoSchemaObject = new Schema({loc: {nested: {long: Number, lat: Number}}}); geoSchemaObject.index({'loc.nested': '2d'}); - var Test = db.model('Geo52', geoSchemaObject, "y" + random()); + var Test = db.model('Geo52', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {nested:{long:10, lat:20 }}}, { loc: {nested:{long:40, lat:90 }}}, complete); + Test.create({loc: {nested: {long: 10, lat: 20}}}, {loc: {nested: {long: 40, lat: 90}}}, complete); function test() { - Test.find({ 'loc.nested': { $within: { $centerSphere: [['11', '20'], '0.4'] }}}, function(err, docs) { + Test.find({'loc.nested': {$within: {$centerSphere: [['11', '20'], '0.4']}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); } @@ -517,48 +558,58 @@ describe('model query casting', function() { describe('$center', function() { it('with arrays', function(done) { - var db = start() - , Test = db.model('Geo4', geoSchemaArray, "y" + random()); + var db = start(), + Test = db.model('Geo4', geoSchemaArray, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: [ 10, 20 ]}, { loc: [ 40, 90 ]}, complete); + Test.create({loc: [10, 20]}, {loc: [40, 90]}, complete); function test() { - Test.find({ loc: { $within: { $center: [['11', '20'], '1'] }}}, function(err, docs) { + Test.find({loc: {$within: {$center: [['11', '20'], '1']}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); } }); it('with objects', function(done) { - var db = start() - , Test = db.model('Geo5', geoSchemaObject, "y" + random()); + var db = start(), + Test = db.model('Geo5', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {long:10, lat:20 }}, { loc: {long:40, lat:90 }}, complete); + Test.create({loc: {long: 10, lat: 20}}, {loc: {long: 40, lat: 90}}, complete); function test() { - Test.find({ loc: { $within: { $center: [['11', '20'], '1'] }}}, function(err, docs) { + Test.find({loc: {$within: {$center: [['11', '20'], '1']}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); } @@ -566,26 +617,31 @@ describe('model query casting', function() { it('with nested objects', function(done) { var db = start(); - var geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number }}}); + var geoSchemaObject = new Schema({loc: {nested: {long: Number, lat: Number}}}); geoSchemaObject.index({'loc.nested': '2d'}); - var Test = db.model('Geo52', geoSchemaObject, "y" + random()); + var Test = db.model('Geo52', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {nested:{long:10, lat:20 }}}, { loc: {nested:{long:40, lat:90 }}}, complete); + Test.create({loc: {nested: {long: 10, lat: 20}}}, {loc: {nested: {long: 40, lat: 90}}}, complete); function test() { - Test.find({ 'loc.nested': { $within: { $center: [['11', '20'], '1'] }}}, function(err, docs) { + Test.find({'loc.nested': {$within: {$center: [['11', '20'], '1']}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); } @@ -594,48 +650,58 @@ describe('model query casting', function() { describe('$polygon', function() { it('with arrays', function(done) { - var db = start() - , Test = db.model('Geo4', geoSchemaArray, "y" + random()); + var db = start(), + Test = db.model('Geo4', geoSchemaArray, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: [ 10, 20 ]}, { loc: [ 40, 90 ]}, complete); + Test.create({loc: [10, 20]}, {loc: [40, 90]}, complete); function test() { - Test.find({ loc: { $within: { $polygon: [['8', '1'], ['8', '100'],['50','100'],['50','1']] }}}, function(err, docs) { + Test.find({loc: {$within: {$polygon: [['8', '1'], ['8', '100'], ['50', '100'], ['50', '1']]}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } }); it('with objects', function(done) { - var db = start() - , Test = db.model('Geo5', geoSchemaObject, "y" + random()); + var db = start(), + Test = db.model('Geo5', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {long:10, lat:20 }}, { loc: {long:40, lat:90 }}, complete); + Test.create({loc: {long: 10, lat: 20}}, {loc: {long: 40, lat: 90}}, complete); function test() { - Test.find({ loc: { $within: { $polygon: [['8', '1'], ['8', '100'],['50','100'],['50','1']] }}}, function(err, docs) { + Test.find({loc: {$within: {$polygon: [['8', '1'], ['8', '100'], ['50', '100'], ['50', '1']]}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } @@ -643,26 +709,31 @@ describe('model query casting', function() { it('with nested objects', function(done) { var db = start(); - var geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number }}}); + var geoSchemaObject = new Schema({loc: {nested: {long: Number, lat: Number}}}); geoSchemaObject.index({'loc.nested': '2d'}); - var Test = db.model('Geo52', geoSchemaObject, "y" + random()); + var Test = db.model('Geo52', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {nested:{long:10, lat:20 }}}, { loc: {nested:{long:40, lat:90 }}}, complete); + Test.create({loc: {nested: {long: 10, lat: 20}}}, {loc: {nested: {long: 40, lat: 90}}}, complete); function test() { - Test.find({ 'loc.nested': { $within: { $polygon: [['8', '1'], ['8', '100'],['50','100'],['50','1']] }}}, function(err, docs) { + Test.find({'loc.nested': {$within: {$polygon: [['8', '1'], ['8', '100'], ['50', '100'], ['50', '1']]}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } @@ -671,49 +742,58 @@ describe('model query casting', function() { describe('$box', function() { it('with arrays', function(done) { - - var db = start() - , Test = db.model('Geo4', geoSchemaArray, "y" + random()); + var db = start(), + Test = db.model('Geo4', geoSchemaArray, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: [ 10, 20 ]}, { loc: [ 40, 90 ]}, complete); + Test.create({loc: [10, 20]}, {loc: [40, 90]}, complete); function test() { - Test.find({ loc: { $within: { $box: [['8', '1'], ['50','100']] }}}, function(err, docs) { + Test.find({loc: {$within: {$box: [['8', '1'], ['50', '100']]}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } }); it('with objects', function(done) { - var db = start() - , Test = db.model('Geo5', geoSchemaObject, "y" + random()); + var db = start(), + Test = db.model('Geo5', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {long:10, lat:20 }}, { loc: {long:40, lat:90 }}, complete); + Test.create({loc: {long: 10, lat: 20}}, {loc: {long: 40, lat: 90}}, complete); function test() { - Test.find({ loc: { $within: { $box: [['8', '1'], ['50','100']] }}}, function(err, docs) { + Test.find({loc: {$within: {$box: [['8', '1'], ['50', '100']]}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } @@ -721,25 +801,30 @@ describe('model query casting', function() { it('with nested objects', function(done) { var db = start(); - var geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number }}}); + var geoSchemaObject = new Schema({loc: {nested: {long: Number, lat: Number}}}); geoSchemaObject.index({'loc.nested': '2d'}); - var Test = db.model('Geo52', geoSchemaObject, "y" + random()); + var Test = db.model('Geo52', geoSchemaObject, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.err = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.err = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: {nested:{long:10, lat:20 }}}, { loc: {nested:{long:40, lat:90 }}}, complete); + Test.create({loc: {nested: {long: 10, lat: 20}}}, {loc: {nested: {long: 40, lat: 90}}}, complete); function test() { - Test.find({ 'loc.nested': { $within: { $box: [['8', '1'], ['50','100']] }}}, function(err, docs) { + Test.find({'loc.nested': {$within: {$box: [['8', '1'], ['50', '100']]}}}, function(err, docs) { assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); db.close(done); }); } @@ -754,30 +839,28 @@ describe('model query casting', function() { return 'img'; }; - var db = start() - , B = db.model(modelName, collection + random()) - , result = B.find({}).cast(B, { tags: {$regex:/a/, $options: opts}}); + var db = start(), + B = db.model(modelName, collection + random()), + result = B.find({}).cast(B, {tags: {$regex: /a/, $options: opts}}); - assert.equal('img', result.tags.$options); + assert.equal(result.tags.$options, 'img'); db.close(done); }); }); describe('$elemMatch', function() { it('should cast String to ObjectId in $elemMatch', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); var commentId = mongoose.Types.ObjectId(111); - var post = new BlogPostB({ - comments: [{ _id: commentId }] - }), id = post._id.toString(); + var post = new BlogPostB({comments: [{_id: commentId}]}), id = post._id.toString(); post.save(function(err) { assert.ifError(err); - BlogPostB.findOne({ _id: id, comments: { $elemMatch: { _id: commentId.toString() } } }, function(err, doc) { + BlogPostB.findOne({_id: id, comments: {$elemMatch: {_id: commentId.toString()}}}, function(err, doc) { assert.ifError(err); assert.equal(doc._id.toString(), id); @@ -787,19 +870,17 @@ describe('model query casting', function() { }); it('should cast String to ObjectId in $elemMatch inside $not', function(done) { - var db = start() - , BlogPostB = db.model(modelName, collection); + var db = start(), + BlogPostB = db.model(modelName, collection); var commentId = mongoose.Types.ObjectId(111); - var post = new BlogPostB({ - comments: [{ _id: commentId }] - }), id = post._id.toString(); + var post = new BlogPostB({comments: [{_id: commentId}]}), id = post._id.toString(); post.save(function(err) { assert.ifError(err); - BlogPostB.findOne({ _id: id, comments: { $not: { $elemMatch: { _id: commentId.toString() } } } }, function(err, doc) { + BlogPostB.findOne({_id: id, comments: {$not: {$elemMatch: {_id: commentId.toString()}}}}, function(err, doc) { assert.ifError(err); assert.equal(doc, null); @@ -807,12 +888,72 @@ describe('model query casting', function() { }); }); }); + + it('should cast subdoc _id typed as String to String in $elemMatch gh3719', function(done) { + var db = start(); + + var child = new Schema({ + _id: {type: String} + }, {_id: false}); + + var parent = new Schema({ + children: [child] + }); + + var Parent = db.model('gh3719-1', parent); + + Parent.create({children: [{ _id: 'foobar' }] }, function(error) { + assert.ifError(error); + test(); + }); + + function test() { + Parent.find({ + $and: [{children: {$elemMatch: {_id: 'foobar'}}}] + }, function(error, docs) { + assert.ifError(error); + + assert.equal(docs.length, 1); + db.close(done); + }); + } + }); + + it('should cast subdoc _id typed as String to String in $elemMatch inside $not gh3719', function(done) { + var db = start(); + + var child = new Schema({ + _id: {type: String} + }, {_id: false}); + + var parent = new Schema({ + children: [child] + }); + + var Parent = db.model('gh3719-2', parent); + + Parent.create({children: [{ _id: 'foobar' }] }, function(error) { + assert.ifError(error); + test(); + }); + + function test() { + Parent.find({ + $and: [{children: {$not: {$elemMatch: {_id: 'foobar'}}}}] + }, function(error, docs) { + assert.ifError(error); + + assert.equal(docs.length, 0); + db.close(done); + }); + } + }); }); it('works with $all (gh-3394)', function(done) { var db = start(); - var MyModel = db.model('gh3394', { tags: [ObjectId] }); + var MyModel = db.model('gh3394', {tags: [ObjectId]}); var doc = { tags: ['00000000000000000000000a', '00000000000000000000000b'] @@ -821,11 +962,236 @@ describe('model query casting', function() { MyModel.create(doc, function(error, savedDoc) { assert.ifError(error); assert.equal(typeof savedDoc.tags[0], 'object'); - MyModel.findOne({ tags: { $all: doc.tags } }, function(error, doc) { + MyModel.findOne({tags: {$all: doc.tags}}, function(error, doc) { + assert.ifError(error); + assert.ok(doc); + db.close(done); + }); + }); + }); + + it('date with $not + $type (gh-4632)', function(done) { + var db = start(); + + var MyModel = db.model('gh4632', { test: Date }); + + MyModel.find({ test: { $not: { $type: 9 } } }, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('setOnInsert with custom type (gh-5126)', function(done) { + var db = start(); + + function Point(key, options) { + mongoose.SchemaType.call(this, key, options, 'Point'); + } + + mongoose.Schema.Types.Point = Point; + Point.prototype = Object.create(mongoose.SchemaType.prototype); + + var called = 0; + Point.prototype.cast = function(point) { + ++called; + if (point.type !== 'Point') { + throw new Error('Woops'); + } + + return point; + }; + + var testSchema = new mongoose.Schema({ name: String, test: Point }); + var Test = db.model('gh5126', testSchema); + + var u = { + $setOnInsert: { + name: 'a', + test: { + type: 'Point' + } + } + }; + Test.findOneAndUpdate({ name: 'a' }, u). + exec(function(error) { assert.ifError(error); + assert.equal(called, 1); + done(); + }). + catch(done); + }); + + it('lowercase in query (gh-4569)', function(done) { + var db = start(); + + var contexts = []; + + var testSchema = new Schema({ + name: { type: String, lowercase: true }, + num: { + type: Number, + set: function(v) { + contexts.push(this); + return Math.floor(v); + } + } + }, { runSettersOnQuery: true }); + + var Test = db.model('gh-4569', testSchema); + Test.create({ name: 'val', num: 2.02 }). + then(function() { + assert.equal(contexts.length, 1); + assert.equal(contexts[0].constructor.name, 'model'); + return Test.findOne({ name: 'VAL' }); + }). + then(function(doc) { + assert.ok(doc); + assert.equal(doc.name, 'val'); + assert.equal(doc.num, 2); + }). + then(function() { + return Test.findOneAndUpdate({}, { num: 3.14 }, { new: true }); + }). + then(function(doc) { assert.ok(doc); + assert.equal(doc.name, 'val'); + assert.equal(doc.num, 3); + assert.equal(contexts.length, 2); + assert.equal(contexts[1].constructor.name, 'Query'); + }). + then(function() { done(); }). + catch(done); + }); + + it('runSettersOnQuery only once on find (gh-5434)', function(done) { + var db = start(); + + var vs = []; + var UserSchema = new mongoose.Schema({ + name: String, + foo: { + type: Number, + get: function(val) { + return val.toString(); + }, + set: function(val) { + vs.push(val); + return val; + } + } + }, { runSettersOnQuery: true }); + + var Test = db.model('gh5434', UserSchema); + + Test.find({ foo: '123' }).exec(function(error) { + assert.ifError(error); + assert.equal(vs.length, 1); + assert.strictEqual(vs[0], '123'); + + vs = []; + Test.find({ foo: '123' }, function(error) { + assert.ifError(error); + assert.equal(vs.length, 1); + assert.strictEqual(vs[0], '123'); + done(); + }); + }); + }); + + it('runSettersOnQuery as query option (gh-5350)', function(done) { + var db = start(); + + var contexts = []; + + var testSchema = new Schema({ + name: { type: String, lowercase: true }, + num: { + type: Number, + set: function(v) { + contexts.push(this); + return Math.floor(v); + } + } + }, { runSettersOnQuery: false }); + + var Test = db.model('gh5350', testSchema); + Test.create({ name: 'val', num: 2.02 }). + then(function() { + assert.equal(contexts.length, 1); + assert.equal(contexts[0].constructor.name, 'model'); + return Test.findOne({ name: 'VAL' }, { _id: 0 }, { + runSettersOnQuery: true + }); + }). + then(function(doc) { + assert.ok(doc); + assert.equal(doc.name, 'val'); + assert.equal(doc.num, 2); + }). + then(function() { done(); }). + catch(done); + }); + + it('_id = 0 (gh-4610)', function(done) { + var db = start(); + + var MyModel = db.model('gh4610', { _id: Number }); + + MyModel.create({ _id: 0 }, function(error) { + assert.ifError(error); + MyModel.findById({ _id: 0 }, function(error, doc) { + assert.ifError(error); + assert.ok(doc); + assert.equal(doc._id, 0); done(); }); }); }); + + it('minDistance (gh-4197)', function(done) { + var db = start(); + + var schema = new Schema({ + name: String, + loc: { + type: { type: String }, + coordinates: [Number] + } + }); + + schema.index({ loc: '2dsphere' }); + + var MyModel = db.model('gh4197', schema); + + MyModel.on('index', function(error) { + assert.ifError(error); + var docs = [ + { name: 'San Mateo Caltrain', loc: _geojsonPoint([-122.33, 37.57]) }, + { name: 'Squaw Valley', loc: _geojsonPoint([-120.24, 39.21]) }, + { name: 'Mammoth Lakes', loc: _geojsonPoint([-118.9, 37.61]) } + ]; + var RADIUS_OF_EARTH_IN_METERS = 6378100; + MyModel.create(docs, function(error) { + assert.ifError(error); + MyModel. + find(). + near('loc', { + center: [-122.33, 37.57], + minDistance: (1000 / RADIUS_OF_EARTH_IN_METERS).toString(), + maxDistance: (280000 / RADIUS_OF_EARTH_IN_METERS).toString(), + spherical: true + }). + exec(function(error, results) { + assert.ifError(error); + assert.equal(results.length, 1); + assert.equal(results[0].name, 'Squaw Valley'); + done(); + }); + }); + }); + }); }); + +function _geojsonPoint(coordinates) { + return { type: 'Point', coordinates: coordinates }; +} diff --git a/test/model.querying.test.js b/test/model.querying.test.js index ff9a61771d2..1dd9f2e376d 100644 --- a/test/model.querying.test.js +++ b/test/model.querying.test.js @@ -1,75 +1,82 @@ - /** * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ObjectId = Schema.Types.ObjectId - , MongooseBuffer = mongoose.Types.Buffer - , DocumentObjectId = mongoose.Types.ObjectId - , Query = require('../lib/query'); - -/** - * Setup. - */ - -var Comments = new Schema; - -Comments.add({ - title : String - , date : Date - , body : String - , comments : [Comments] -}); - -var BlogPostB = new Schema({ - title : String - , author : String - , slug : String - , date : Date - , meta : { - date : Date - , visitors : Number - } - , published : Boolean - , mixed : {} - , numbers : [Number] - , tags : [String] - , sigs : [Buffer] - , owners : [ObjectId] - , comments : [Comments] - , def : { type: String, default: 'kandinsky' } -}); - -mongoose.model('BlogPostB', BlogPostB); -var collection = 'blogposts_' + random(); - -var ModSchema = new Schema({ - num: Number - , str: String -}); -mongoose.model('Mod', ModSchema); - -var geoSchema = new Schema({ loc: { type: [Number], index: '2d'}}); +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema, + ObjectId = Schema.Types.ObjectId, + MongooseBuffer = mongoose.Types.Buffer, + DocumentObjectId = mongoose.Types.ObjectId, + Query = require('../lib/query'); describe('model: querying:', function() { + var Comments; + var BlogPostB; + var collection; + var ModSchema; + var geoSchema; + + before(function() { + Comments = new Schema; + + Comments.add({ + title: String, + date: Date, + body: String, + comments: [Comments] + }); + + BlogPostB = new Schema({ + title: String, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number + }, + published: Boolean, + mixed: {}, + numbers: [Number], + tags: [String], + sigs: [Buffer], + owners: [ObjectId], + comments: [Comments], + def: {type: String, default: 'kandinsky'} + }); + + mongoose.model('BlogPostB', BlogPostB); + collection = 'blogposts_' + random(); + + ModSchema = new Schema({ + num: Number, + str: String + }); + mongoose.model('Mod', ModSchema); + + geoSchema = new Schema({loc: {type: [Number], index: '2d'}}); + }); + var mongo26_or_greater = false; before(function(done) { start.mongodVersion(function(err, version) { - if (err) throw err; - mongo26_or_greater = 2 < version[0] || (2 == version[0] && 6 <= version[1]); - if (!mongo26_or_greater) console.log('not testing mongodb 2.6 features'); + if (err) { + throw err; + } + mongo26_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 6); + if (!mongo26_or_greater) { + console.log('not testing mongodb 2.6 features'); + } done(); }); }); it('find returns a Query', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); // query assert.ok(BlogPostB.find({}) instanceof Query); @@ -90,8 +97,8 @@ describe('model: querying:', function() { }); it('findOne returns a Query', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); // query assert.ok(BlogPostB.findOne({}) instanceof Query); @@ -112,8 +119,8 @@ describe('model: querying:', function() { }); it('an empty find does not hang', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); function fn() { db.close(done); @@ -123,13 +130,15 @@ describe('model: querying:', function() { }); it('a query is executed when a callback is passed', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , count = 5 - , q = { _id: new DocumentObjectId }; // make sure the query is fast + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + count = 5, + q = {_id: new DocumentObjectId}; // make sure the query is fast function fn() { - if (--count) return; + if (--count) { + return; + } db.close(done); } @@ -150,13 +159,15 @@ describe('model: querying:', function() { }); it('query is executed where a callback for findOne', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , count = 5 - , q = { _id: new DocumentObjectId }; // make sure the query is fast + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + count = 5, + q = {_id: new DocumentObjectId}; // make sure the query is fast function fn() { - if (--count) return; + if (--count) { + return; + } db.close(); done(); } @@ -179,20 +190,22 @@ describe('model: querying:', function() { describe('count', function() { it('returns a Query', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); assert.ok(BlogPostB.count({}) instanceof Query); db.close(); done(); }); it('Query executes when you pass a callback', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , pending = 2; + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + pending = 2; function fn() { - if (--pending) return; + if (--pending) { + return; + } db.close(); done(); } @@ -202,9 +215,9 @@ describe('model: querying:', function() { }); it('counts documents', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , title = 'Wooooot ' + random(); + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + title = 'Wooooot ' + random(); var post = new BlogPostB(); post.set('title', title); @@ -218,11 +231,11 @@ describe('model: querying:', function() { post.save(function(err) { assert.ifError(err); - BlogPostB.count({ title: title }, function(err, count) { + BlogPostB.count({title: title}, function(err, count) { assert.ifError(err); - assert.equal('number', typeof count); - assert.equal(2, count); + assert.equal(typeof count, 'number'); + assert.equal(count, 2); db.close(); done(); @@ -234,8 +247,8 @@ describe('model: querying:', function() { describe('distinct', function() { it('returns a Query', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); assert.ok(BlogPostB.distinct('title', {}) instanceof Query); db.close(); @@ -244,14 +257,14 @@ describe('model: querying:', function() { it('executes when you pass a callback', function(done) { var db = start(); - var Address = new Schema({ zip: String }); + var Address = new Schema({zip: String}); Address = db.model('Address', Address, 'addresses_' + random()); - Address.create({ zip: '10010'}, { zip: '10010'}, { zip: '99701'}, function(err) { + Address.create({zip: '10010'}, {zip: '10010'}, {zip: '99701'}, function(err) { assert.strictEqual(null, err); var query = Address.distinct('zip', {}, function(err, results) { assert.ifError(err); - assert.equal(2, results.length); + assert.equal(results.length, 2); assert.ok(results.indexOf('10010') > -1); assert.ok(results.indexOf('99701') > -1); db.close(done); @@ -262,13 +275,13 @@ describe('model: querying:', function() { it('permits excluding conditions gh-1541', function(done) { var db = start(); - var Address = new Schema({ zip: String }); + var Address = new Schema({zip: String}); Address = db.model('Address', Address, 'addresses_' + random()); - Address.create({ zip: '10010'}, { zip: '10010'}, { zip: '99701'}, function(err) { + Address.create({zip: '10010'}, {zip: '10010'}, {zip: '99701'}, function(err) { assert.ifError(err); Address.distinct('zip', function(err, results) { assert.ifError(err); - assert.equal(2, results.length); + assert.equal(results.length, 2); assert.ok(results.indexOf('10010') > -1); assert.ok(results.indexOf('99701') > -1); db.close(done); @@ -279,8 +292,8 @@ describe('model: querying:', function() { describe('update', function() { it('returns a Query', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); assert.ok(BlogPostB.update({}, {}) instanceof Query); assert.ok(BlogPostB.update({}, {}, {}) instanceof Query); @@ -289,12 +302,14 @@ describe('model: querying:', function() { }); it('Query executes when you pass a callback', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , count = 2; + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + count = 2; function fn() { - if (--count) return; + if (--count) { + return; + } db.close(); done(); } @@ -310,20 +325,20 @@ describe('model: querying:', function() { mixed: Schema.Types.Mixed }); - var query = Model.update({}, { mixed: {}, name: 'abc' }, - { minimize: true }); + var query = Model.update({}, {mixed: {}, name: 'abc'}, + {minimize: true}); assert.ok(!query._update.$set.mixed); - done(); + db.close(done); }); }); describe('findOne', function() { it('works', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , title = 'Wooooot ' + random(); + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + title = 'Wooooot ' + random(); var post = new BlogPostB(); post.set('title', title); @@ -331,10 +346,10 @@ describe('model: querying:', function() { post.save(function(err) { assert.ifError(err); - BlogPostB.findOne({ title: title }, function(err, doc) { + BlogPostB.findOne({title: title}, function(err, doc) { assert.ifError(err); assert.equal(title, doc.get('title')); - assert.equal(false, doc.isNew); + assert.equal(doc.isNew, false); db.close(); done(); @@ -343,18 +358,18 @@ describe('model: querying:', function() { }); it('casts $modifiers', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , post = new BlogPostB({ - meta: { - visitors: -10 - } - }); + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + post = new BlogPostB({ + meta: { + visitors: -10 + } + }); post.save(function(err) { assert.ifError(err); - var query = { 'meta.visitors': { $gt: '-20', $lt: -1 }}; + var query = {'meta.visitors': {$gt: '-20', $lt: -1}}; BlogPostB.findOne(query, function(err, found) { assert.ifError(err); assert.ok(found); @@ -368,8 +383,8 @@ describe('model: querying:', function() { }); it('querying if an array contains one of multiple members $in a set', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); var post = new BlogPostB(); @@ -380,11 +395,11 @@ describe('model: querying:', function() { BlogPostB.findOne({tags: {$in: ['football', 'baseball']}}, function(err, doc) { assert.ifError(err); - assert.equal(doc._id.toString(),post._id); + assert.equal(doc._id.toString(), post._id); - BlogPostB.findOne({ _id: post._id, tags: /otba/i }, function(err, doc) { + BlogPostB.findOne({_id: post._id, tags: /otba/i}, function(err, doc) { assert.ifError(err); - assert.equal(doc._id.toString(),post._id); + assert.equal(doc._id.toString(), post._id); db.close(); done(); }); @@ -393,15 +408,15 @@ describe('model: querying:', function() { }); it('querying if an array contains one of multiple members $in a set 2', function(done) { - var db = start() - , BlogPostA = db.model('BlogPostB', collection); + var db = start(), + BlogPostA = db.model('BlogPostB', collection); - var post = new BlogPostA({ tags: ['gooberOne'] }); + var post = new BlogPostA({tags: ['gooberOne']}); post.save(function(err) { assert.ifError(err); - var query = {tags: {$in:[ 'gooberOne' ]}}; + var query = {tags: {$in: ['gooberOne']}}; BlogPostA.findOne(query, function(err, returned) { cb(); @@ -411,7 +426,7 @@ describe('model: querying:', function() { }); }); - post.collection.insert({ meta: { visitors: 9898, a: null } }, {}, function(err, b) { + post.collection.insert({meta: {visitors: 9898, a: null}}, {}, function(err, b) { assert.ifError(err); BlogPostA.findOne({_id: b.ops[0]._id}, function(err, found) { @@ -422,24 +437,27 @@ describe('model: querying:', function() { }); var pending = 2; + function cb() { - if (--pending) return; + if (--pending) { + return; + } db.close(); done(); } }); it('querying via $where a string', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); - BlogPostB.create({ title: 'Steve Jobs', author: 'Steve Jobs'}, function(err, created) { + BlogPostB.create({title: 'Steve Jobs', author: 'Steve Jobs'}, function(err, created) { assert.ifError(err); - BlogPostB.findOne({ $where: "this.title && this.title === this.author" }, function(err, found) { + BlogPostB.findOne({$where: 'this.title && this.title === this.author'}, function(err, found) { assert.ifError(err); - assert.equal(found._id.toString(),created._id); + assert.equal(found._id.toString(), created._id); db.close(); done(); }); @@ -447,15 +465,17 @@ describe('model: querying:', function() { }); it('querying via $where a function', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); - BlogPostB.create({ author: 'Atari', slug: 'Atari'}, function(err, created) { + BlogPostB.create({author: 'Atari', slug: 'Atari'}, function(err, created) { assert.ifError(err); - BlogPostB.findOne({ $where: function() { - return (this.author && this.slug && this.author === this.slug); - } }, function(err, found) { + BlogPostB.findOne({ + $where: function() { + return (this.author && this.slug && this.author === this.slug); + } + }, function(err, found) { assert.ifError(err); assert.equal(found._id.toString(), created._id); @@ -466,21 +486,21 @@ describe('model: querying:', function() { }); it('based on nested fields', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , post = new BlogPostB({ - meta: { - visitors: 5678 - } - }); + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + post = new BlogPostB({ + meta: { + visitors: 5678 + } + }); post.save(function(err) { assert.ifError(err); - BlogPostB.findOne({ 'meta.visitors': 5678 }, function(err, found) { + BlogPostB.findOne({'meta.visitors': 5678}, function(err, found) { assert.ifError(err); assert.equal(found.get('meta.visitors') - .valueOf(), post.get('meta.visitors').valueOf()); + .valueOf(), post.get('meta.visitors').valueOf()); assert.equal(found.get('_id').toString(), post.get('_id')); db.close(); done(); @@ -489,10 +509,10 @@ describe('model: querying:', function() { }); it('based on embedded doc fields (gh-242, gh-463)', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); - BlogPostB.create({comments: [{title: 'i should be queryable'}], numbers: [1,2,33333], tags:['yes', 'no']}, function(err, created) { + BlogPostB.create({comments: [{title: 'i should be queryable'}], numbers: [1, 2, 33333], tags: ['yes', 'no']}, function(err, created) { assert.ifError(err); BlogPostB.findOne({'comments.title': 'i should be queryable'}, function(err, found) { assert.ifError(err); @@ -520,10 +540,10 @@ describe('model: querying:', function() { }); it('works with nested docs and string ids (gh-389)', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); - BlogPostB.create({comments: [{title: 'i should be queryable by _id'}, {title:'me too me too!'}]}, function(err, created) { + BlogPostB.create({comments: [{title: 'i should be queryable by _id'}, {title: 'me too me too!'}]}, function(err, created) { assert.ifError(err); var id = created.comments[1]._id.toString(); BlogPostB.findOne({'comments._id': id}, function(err, found) { @@ -537,11 +557,11 @@ describe('model: querying:', function() { }); it('using #all with nested #elemMatch', function(done) { - var db = start() - , P = db.model('BlogPostB', collection + '_nestedElemMatch'); + var db = start(), + P = db.model('BlogPostB', collection + '_nestedElemMatch'); - var post = new P({ title: "nested elemMatch" }); - post.comments.push({ title: 'comment A' }, { title: 'comment B' }, { title: 'comment C' }); + var post = new P({title: 'nested elemMatch'}); + post.comments.push({title: 'comment A'}, {title: 'comment B'}, {title: 'comment C'}); var id1 = post.comments[1]._id; var id2 = post.comments[2]._id; @@ -549,10 +569,10 @@ describe('model: querying:', function() { post.save(function(err) { assert.ifError(err); - var query0 = { $elemMatch: { _id: id1, title: 'comment B' }}; - var query1 = { $elemMatch: { _id: id2.toString(), title: 'comment C' }}; + var query0 = {$elemMatch: {_id: id1, title: 'comment B'}}; + var query1 = {$elemMatch: {_id: id2.toString(), title: 'comment C'}}; - P.findOne({ comments: { $all: [query0, query1] }}, function(err, p) { + P.findOne({comments: {$all: [query0, query1]}}, function(err, p) { db.close(); assert.ifError(err); assert.equal(p.id, post.id); @@ -562,21 +582,21 @@ describe('model: querying:', function() { }); it('using #or with nested #elemMatch', function(done) { - var db = start() - , P = db.model('BlogPostB', collection); + var db = start(), + P = db.model('BlogPostB', collection); - var post = new P({ title: "nested elemMatch" }); - post.comments.push({ title: 'comment D' }, { title: 'comment E' }, { title: 'comment F' }); + var post = new P({title: 'nested elemMatch'}); + post.comments.push({title: 'comment D'}, {title: 'comment E'}, {title: 'comment F'}); var id1 = post.comments[1]._id; post.save(function(err) { assert.ifError(err); - var query0 = { comments: { $elemMatch: { title: 'comment Z' }}}; - var query1 = { comments: { $elemMatch: { _id: id1.toString(), title: 'comment E' }}}; + var query0 = {comments: {$elemMatch: {title: 'comment Z'}}}; + var query1 = {comments: {$elemMatch: {_id: id1.toString(), title: 'comment E'}}}; - P.findOne({ $or: [query0, query1] }, function(err, p) { + P.findOne({$or: [query0, query1]}, function(err, p) { db.close(); assert.ifError(err); assert.equal(p.id, post.id); @@ -586,18 +606,20 @@ describe('model: querying:', function() { }); it('buffer $in array', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); - BlogPostB.create({sigs: [new Buffer([1, 2, 3]), - new Buffer([4, 5, 6]), - new Buffer([7, 8, 9])]}, function(err, created) { + BlogPostB.create({ + sigs: [new Buffer([1, 2, 3]), + new Buffer([4, 5, 6]), + new Buffer([7, 8, 9])] + }, function(err, created) { assert.ifError(err); BlogPostB.findOne({sigs: new Buffer([1, 2, 3])}, function(err, found) { assert.ifError(err); found.id; assert.equal(found._id.toString(), created._id); - var query = { sigs: { "$in" : [new Buffer([3, 3, 3]), new Buffer([4, 5, 6])] } }; + var query = {sigs: {'$in': [new Buffer([3, 3, 3]), new Buffer([4, 5, 6])]}}; BlogPostB.findOne(query, function(err) { assert.ifError(err); db.close(); @@ -608,17 +630,17 @@ describe('model: querying:', function() { }); it('regex with Array (gh-599)', function(done) { - var db = start() - , B = db.model('BlogPostB', random()); + var db = start(), + B = db.model('BlogPostB', random()); - B.create({ tags: 'wooof baaaark meeeeow'.split(' ') }, function(err) { + B.create({tags: 'wooof baaaark meeeeow'.split(' ')}, function(err) { assert.ifError(err); - B.findOne({ tags: /ooof$/ }, function(err, doc) { + B.findOne({tags: /ooof$/}, function(err, doc) { assert.ifError(err); assert.strictEqual(true, !!doc); assert.ok(!!~doc.tags.indexOf('meeeeow')); - B.findOne({ tags: {$regex: 'eow$' } }, function(err, doc) { + B.findOne({tags: {$regex: 'eow$'}}, function(err, doc) { db.close(); assert.ifError(err); assert.strictEqual(true, !!doc); @@ -630,13 +652,13 @@ describe('model: querying:', function() { }); it('regex with options', function(done) { - var db = start() - , B = db.model('BlogPostB', collection); + var db = start(), + B = db.model('BlogPostB', collection); - var post = new B({ title: '$option queries' }); + var post = new B({title: '$option queries'}); post.save(function(err) { assert.ifError(err); - B.findOne({ title: { $regex: ' QUERIES$', $options: 'i' }}, function(err, doc) { + B.findOne({title: {$regex: ' QUERIES$', $options: 'i'}}, function(err, doc) { db.close(); assert.strictEqual(null, err, err && err.stack); assert.equal(doc.id, post.id); @@ -646,14 +668,14 @@ describe('model: querying:', function() { }); it('works with $elemMatch and $in combo (gh-1100)', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , id1 = new DocumentObjectId - , id2 = new DocumentObjectId; + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + id1 = new DocumentObjectId, + id2 = new DocumentObjectId; BlogPostB.create({owners: [id1, id2]}, function(err, created) { assert.ifError(err); - BlogPostB.findOne({owners: {'$elemMatch': { $in: [id2.toString()] }}}, function(err, found) { + BlogPostB.findOne({owners: {'$elemMatch': {$in: [id2.toString()]}}}, function(err, found) { db.close(); assert.ifError(err); assert.ok(found); @@ -666,9 +688,9 @@ describe('model: querying:', function() { describe('findById', function() { it('handles undefined', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , title = 'Edwald ' + random(); + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + title = 'Edwald ' + random(); var post = new BlogPostB(); post.set('title', title); @@ -678,16 +700,16 @@ describe('model: querying:', function() { BlogPostB.findById(undefined, function(err, doc) { assert.ifError(err); - assert.equal(null, doc); - done(); + assert.equal(doc, null); + db.close(done); }); }); }); it('works', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , title = 'Edwald ' + random(); + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + title = 'Edwald ' + random(); var post = new BlogPostB(); post.set('title', title); @@ -700,8 +722,10 @@ describe('model: querying:', function() { BlogPostB.findById(post.get('_id'), function(err, doc) { assert.ifError(err); assert.ok(doc instanceof BlogPostB); - assert.equal(title, doc.get('title')); - if (--pending) return; + assert.equal(doc.get('title'), title); + if (--pending) { + return; + } db.close(); done(); }); @@ -709,8 +733,10 @@ describe('model: querying:', function() { BlogPostB.findById(post.get('_id').toHexString(), function(err, doc) { assert.ifError(err); assert.ok(doc instanceof BlogPostB); - assert.equal(title, doc.get('title')); - if (--pending) return; + assert.equal(doc.get('title'), title); + if (--pending) { + return; + } db.close(); done(); }); @@ -718,9 +744,9 @@ describe('model: querying:', function() { }); it('works with partial initialization', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , queries = 5; + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + queries = 5; var post = new BlogPostB(); @@ -735,65 +761,75 @@ describe('model: querying:', function() { BlogPostB.findById(post.get('_id'), function(err, doc) { assert.ifError(err); - assert.equal(true, doc.isInit('title')); - assert.equal(true, doc.isInit('slug')); - assert.equal(false, doc.isInit('date')); - assert.equal(true, doc.isInit('meta.visitors')); - assert.equal(53, doc.meta.visitors.valueOf()); - assert.equal(2, doc.tags.length); - if (--queries) return; + assert.equal(doc.isInit('title'), true); + assert.equal(doc.isInit('slug'), true); + assert.equal(doc.isInit('date'), false); + assert.equal(doc.isInit('meta.visitors'), true); + assert.equal(doc.meta.visitors.valueOf(), 53); + assert.equal(doc.tags.length, 2); + if (--queries) { + return; + } db.close(); done(); }); BlogPostB.findById(post.get('_id'), 'title', function(err, doc) { assert.ifError(err); - assert.equal(true, doc.isInit('title')); - assert.equal(false, doc.isInit('slug')); - assert.equal(false, doc.isInit('date')); - assert.equal(false, doc.isInit('meta.visitors')); - assert.equal(undefined, doc.meta.visitors); - assert.equal(undefined, doc.tags); - if (--queries) return; + assert.equal(doc.isInit('title'), true); + assert.equal(doc.isInit('slug'), false); + assert.equal(doc.isInit('date'), false); + assert.equal(doc.isInit('meta.visitors'), false); + assert.equal(doc.meta.visitors, undefined); + assert.equal(doc.tags, undefined); + if (--queries) { + return; + } db.close(); done(); }); BlogPostB.findById(post.get('_id'), '-slug', function(err, doc) { assert.ifError(err); - assert.equal(true, doc.isInit('title')); - assert.equal(false, doc.isInit('slug')); - assert.equal(false, doc.isInit('date')); - assert.equal(true, doc.isInit('meta.visitors')); - assert.equal(53, doc.meta.visitors); - assert.equal(2, doc.tags.length); - if (--queries) return; + assert.equal(doc.isInit('title'), true); + assert.equal(doc.isInit('slug'), false); + assert.equal(doc.isInit('date'), false); + assert.equal(doc.isInit('meta.visitors'), true); + assert.equal(doc.meta.visitors, 53); + assert.equal(doc.tags.length, 2); + if (--queries) { + return; + } db.close(); done(); }); - BlogPostB.findById(post.get('_id'), { title:1 }, function(err, doc) { + BlogPostB.findById(post.get('_id'), {title: 1}, function(err, doc) { assert.ifError(err); - assert.equal(true, doc.isInit('title')); - assert.equal(false, doc.isInit('slug')); - assert.equal(false, doc.isInit('date')); - assert.equal(false, doc.isInit('meta.visitors')); - assert.equal(undefined, doc.meta.visitors); - assert.equal(undefined, doc.tags); - if (--queries) return; + assert.equal(doc.isInit('title'), true); + assert.equal(doc.isInit('slug'), false); + assert.equal(doc.isInit('date'), false); + assert.equal(doc.isInit('meta.visitors'), false); + assert.equal(doc.meta.visitors, undefined); + assert.equal(doc.tags, undefined); + if (--queries) { + return; + } db.close(); done(); }); BlogPostB.findById(post.get('_id'), 'slug', function(err, doc) { assert.ifError(err); - assert.equal(false, doc.isInit('title')); - assert.equal(true, doc.isInit('slug')); - assert.equal(false, doc.isInit('date')); - assert.equal(false, doc.isInit('meta.visitors')); - assert.equal(undefined, doc.meta.visitors); - assert.equal(undefined, doc.tags); - if (--queries) return; + assert.equal(doc.isInit('title'), false); + assert.equal(doc.isInit('slug'), true); + assert.equal(doc.isInit('date'), false); + assert.equal(doc.isInit('meta.visitors'), false); + assert.equal(doc.meta.visitors, undefined); + assert.equal(doc.tags, undefined); + if (--queries) { + return; + } db.close(); done(); }); @@ -801,8 +837,8 @@ describe('model: querying:', function() { }); it('querying if an array contains at least a certain single member (gh-220)', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); var post = new BlogPostB(); @@ -813,7 +849,7 @@ describe('model: querying:', function() { BlogPostB.findOne({tags: 'cat'}, function(err, doc) { assert.ifError(err); - assert.equal(doc._id.toString(),post._id); + assert.equal(doc._id.toString(), post._id); db.close(); done(); }); @@ -822,29 +858,29 @@ describe('model: querying:', function() { it('where an array where the $slice operator', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); - BlogPostB.create({numbers: [500,600,700,800]}, function(err, created) { + BlogPostB.create({numbers: [500, 600, 700, 800]}, function(err, created) { assert.ifError(err); BlogPostB.findById(created._id, {numbers: {$slice: 2}}, function(err, found) { assert.ifError(err); assert.equal(found._id.toString(), created._id); - assert.equal(2, found.numbers.length); - assert.equal(500, found.numbers[0]); - assert.equal(600, found.numbers[1]); + assert.equal(found.numbers.length, 2); + assert.equal(found.numbers[0], 500); + assert.equal(found.numbers[1], 600); BlogPostB.findById(created._id, {numbers: {$slice: -2}}, function(err, found) { assert.ifError(err); assert.equal(found._id.toString(), created._id); - assert.equal(2, found.numbers.length); - assert.equal(700, found.numbers[0]); - assert.equal(800, found.numbers[1]); + assert.equal(found.numbers.length, 2); + assert.equal(found.numbers[0], 700); + assert.equal(found.numbers[1], 800); BlogPostB.findById(created._id, {numbers: {$slice: [1, 2]}}, function(err, found) { assert.ifError(err); assert.equal(found._id.toString(), created._id); - assert.equal(2, found.numbers.length); - assert.equal(600, found.numbers[0]); - assert.equal(700, found.numbers[1]); + assert.equal(found.numbers.length, 2); + assert.equal(found.numbers[0], 600); + assert.equal(found.numbers[1], 700); db.close(); done(); }); @@ -852,14 +888,13 @@ describe('model: querying:', function() { }); }); }); - }); describe('find', function() { it('works', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , title = 'Wooooot ' + random(); + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + title = 'Wooooot ' + random(); var post = new BlogPostB(); post.set('title', title); @@ -873,15 +908,15 @@ describe('model: querying:', function() { post.save(function(err) { assert.ifError(err); - BlogPostB.find({ title: title }, function(err, docs) { + BlogPostB.find({title: title}, function(err, docs) { assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); assert.equal(title, docs[0].get('title')); - assert.equal(false, docs[0].isNew); + assert.equal(docs[0].isNew, false); assert.equal(title, docs[1].get('title')); - assert.equal(false, docs[1].isNew); + assert.equal(docs[1].isNew, false); db.close(); done(); @@ -891,13 +926,13 @@ describe('model: querying:', function() { }); it('returns docs where an array that contains one specific member', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); BlogPostB.create({numbers: [100, 101, 102]}, function(err, created) { assert.ifError(err); BlogPostB.find({numbers: 100}, function(err, found) { assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); assert.equal(found[0]._id.toString(), created._id); db.close(); done(); @@ -908,8 +943,8 @@ describe('model: querying:', function() { it('works when comparing $ne with single value against an array', function(done) { var db = start(); var schema = new Schema({ - ids: [Schema.ObjectId] - , b: Schema.ObjectId + ids: [Schema.ObjectId], + b: Schema.ObjectId }); var NE = db.model('NE_Test', schema, 'nes__' + random()); @@ -919,40 +954,39 @@ describe('model: querying:', function() { var id3 = new DocumentObjectId; var id4 = new DocumentObjectId; - NE.create({ ids: [id1, id4], b: id3 }, function(err) { + NE.create({ids: [id1, id4], b: id3}, function(err) { assert.ifError(err); - NE.create({ ids: [id2, id4], b: id3 }, function(err) { + NE.create({ids: [id2, id4], b: id3}, function(err) { assert.ifError(err); - var query = NE.find({ 'b': id3.toString(), 'ids': { $ne: id1 }}); + var query = NE.find({'b': id3.toString(), 'ids': {$ne: id1}}); query.exec(function(err, nes1) { assert.ifError(err); - assert.equal(1, nes1.length); + assert.equal(nes1.length, 1); - NE.find({ b: { $ne: [1] }}, function(err) { - assert.equal("Cast to ObjectId failed for value \"1\" at path \"b\"", err.message); + NE.find({b: {$ne: [1]}}, function(err) { + assert.equal(err.message, 'Cast to ObjectId failed for value "[ 1 ]" at path "b" for model "NE_Test"'); - NE.find({ b: { $ne: 4 }}, function(err) { - assert.equal("Cast to ObjectId failed for value \"4\" at path \"b\"", err.message); + NE.find({b: {$ne: 4}}, function(err) { + assert.equal(err.message, 'Cast to ObjectId failed for value "4" at path "b" for model "NE_Test"'); - NE.find({ b: id3, ids: { $ne: id4 }}, function(err, nes4) { + NE.find({b: id3, ids: {$ne: id4}}, function(err, nes4) { db.close(); assert.ifError(err); - assert.equal(0, nes4.length); + assert.equal(nes4.length, 0); done(); }); }); }); }); - }); }); }); it('with partial initialization', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , queries = 4; + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + queries = 4; var post = new BlogPostB(); @@ -962,46 +996,54 @@ describe('model: querying:', function() { post.save(function(err) { assert.ifError(err); - BlogPostB.find({ _id: post.get('_id') }, function(err, docs) { + BlogPostB.find({_id: post.get('_id')}, function(err, docs) { assert.ifError(err); - assert.equal(true, docs[0].isInit('title')); - assert.equal(true, docs[0].isInit('slug')); - assert.equal(false, docs[0].isInit('date')); + assert.equal(docs[0].isInit('title'), true); + assert.equal(docs[0].isInit('slug'), true); + assert.equal(docs[0].isInit('date'), false); assert.strictEqual('kandinsky', docs[0].def); - if (--queries) return; + if (--queries) { + return; + } db.close(); done(); }); - BlogPostB.find({ _id: post.get('_id') }, 'title', function(err, docs) { + BlogPostB.find({_id: post.get('_id')}, 'title', function(err, docs) { assert.ifError(err); - assert.equal(true, docs[0].isInit('title')); - assert.equal(false, docs[0].isInit('slug')); - assert.equal(false, docs[0].isInit('date')); + assert.equal(docs[0].isInit('title'), true); + assert.equal(docs[0].isInit('slug'), false); + assert.equal(docs[0].isInit('date'), false); assert.strictEqual(undefined, docs[0].def); - if (--queries) return; + if (--queries) { + return; + } db.close(); done(); }); - BlogPostB.find({ _id: post.get('_id') }, { slug: 0, def: 0 }, function(err, docs) { + BlogPostB.find({_id: post.get('_id')}, {slug: 0, def: 0}, function(err, docs) { assert.ifError(err); - assert.equal(true, docs[0].isInit('title')); - assert.equal(false, docs[0].isInit('slug')); - assert.equal(false, docs[0].isInit('date')); + assert.equal(docs[0].isInit('title'), true); + assert.equal(docs[0].isInit('slug'), false); + assert.equal(docs[0].isInit('date'), false); assert.strictEqual(undefined, docs[0].def); - if (--queries) return; + if (--queries) { + return; + } db.close(); done(); }); - BlogPostB.find({ _id: post.get('_id') }, 'slug', function(err, docs) { + BlogPostB.find({_id: post.get('_id')}, 'slug', function(err, docs) { assert.ifError(err); - assert.equal(false, docs[0].isInit('title')); - assert.equal(true, docs[0].isInit('slug')); - assert.equal(false, docs[0].isInit('date')); + assert.equal(docs[0].isInit('title'), false); + assert.equal(docs[0].isInit('slug'), true); + assert.equal(docs[0].isInit('date'), false); assert.strictEqual(undefined, docs[0].def); - if (--queries) return; + if (--queries) { + return; + } db.close(); done(); }); @@ -1009,21 +1051,21 @@ describe('model: querying:', function() { }); it('where $exists', function(done) { - var db = start() - , ExistsSchema = new Schema({ - a: Number - , b: String - }); + var db = start(), + ExistsSchema = new Schema({ + a: Number, + b: String + }); mongoose.model('Exists', ExistsSchema); var Exists = db.model('Exists', 'exists_' + random()); - Exists.create({ a: 1}, function(err) { + Exists.create({a: 1}, function(err) { assert.ifError(err); Exists.create({b: 'hi'}, function(err) { assert.ifError(err); Exists.find({b: {$exists: true}}, function(err, docs) { assert.ifError(err); db.close(); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); }); @@ -1031,32 +1073,32 @@ describe('model: querying:', function() { }); it('works with $elemMatch (gh-1100)', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , id1 = new DocumentObjectId - , id2 = new DocumentObjectId; + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + id1 = new DocumentObjectId, + id2 = new DocumentObjectId; BlogPostB.create({owners: [id1, id2]}, function(err) { assert.ifError(err); - BlogPostB.find({owners: {'$elemMatch': { $in: [id2.toString()] }}}, function(err, found) { + BlogPostB.find({owners: {'$elemMatch': {$in: [id2.toString()]}}}, function(err, found) { db.close(); assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); done(); }); }); }); it('where $mod', function(done) { - var db = start() - , Mod = db.model('Mod', 'mods_' + random()); + var db = start(), + Mod = db.model('Mod', 'mods_' + random()); Mod.create({num: 1}, function(err, one) { assert.ifError(err); Mod.create({num: 2}, function(err) { assert.ifError(err); Mod.find({num: {$mod: [2, 1]}}, function(err, found) { assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); assert.equal(found[0]._id.toString(), one._id); db.close(); done(); @@ -1066,16 +1108,16 @@ describe('model: querying:', function() { }); it('where $not', function(done) { - var db = start() - , Mod = db.model('Mod', 'mods_' + random()); + var db = start(), + Mod = db.model('Mod', 'mods_' + random()); Mod.create({num: 1}, function(err) { assert.ifError(err); Mod.create({num: 2}, function(err, two) { assert.ifError(err); Mod.find({num: {$not: {$mod: [2, 1]}}}, function(err, found) { assert.ifError(err); - assert.equal(1, found.length); - assert.equal(found[0]._id.toString(),two._id); + assert.equal(found.length, 1); + assert.equal(found[0]._id.toString(), two._id); db.close(); done(); }); @@ -1084,8 +1126,8 @@ describe('model: querying:', function() { }); it('where or()', function(done) { - var db = start() - , Mod = db.model('Mod', 'mods_' + random()); + var db = start(), + Mod = db.model('Mod', 'mods_' + random()); Mod.create({num: 1}, {num: 2, str: 'two'}, function(err, one, two) { assert.ifError(err); @@ -1099,14 +1141,17 @@ describe('model: querying:', function() { Mod.find({$or: [{num: 1}, {num: 2}]}, function(err, found) { cb(); assert.ifError(err); - assert.equal(2, found.length); + assert.equal(found.length, 2); var found1 = false; var found2 = false; found.forEach(function(doc) { - if (doc.id == one.id) found1 = true; - else if (doc.id == two.id) found2 = true; + if (doc.id === one.id) { + found1 = true; + } else if (doc.id === two.id) { + found2 = true; + } }); assert.ok(found1); @@ -1115,26 +1160,29 @@ describe('model: querying:', function() { } function test2() { - Mod.find({ $or: [{ str: 'two'}, {str:'three'}] }, function(err, found) { + Mod.find({$or: [{str: 'two'}, {str: 'three'}]}, function(err, found) { cb(); assert.ifError(err); - assert.equal(1, found.length); - assert.equal(found[0]._id.toString(),two._id); + assert.equal(found.length, 1); + assert.equal(found[0]._id.toString(), two._id); }); } function test3() { - Mod.find({$or: [{num: 1}]}).or([{ str: 'two' }]).exec(function(err, found) { + Mod.find({$or: [{num: 1}]}).or([{str: 'two'}]).exec(function(err, found) { cb(); assert.ifError(err); - assert.equal(2, found.length); + assert.equal(found.length, 2); var found1 = false; var found2 = false; found.forEach(function(doc) { - if (doc.id == one.id) found1 = true; - else if (doc.id == two.id) found2 = true; + if (doc.id === one.id) { + found1 = true; + } else if (doc.id === two.id) { + found2 = true; + } }); assert.ok(found1); @@ -1143,7 +1191,9 @@ describe('model: querying:', function() { } function cb() { - if (--pending) return; + if (--pending) { + return; + } db.close(); done(); } @@ -1151,8 +1201,8 @@ describe('model: querying:', function() { }); it('using $or with array of Document', function(done) { - var db = start() - , Mod = db.model('Mod', 'mods_' + random()); + var db = start(), + Mod = db.model('Mod', 'mods_' + random()); Mod.create({num: 1}, function(err, one) { assert.ifError(err); @@ -1160,7 +1210,7 @@ describe('model: querying:', function() { assert.ifError(err); Mod.find({$or: found}, function(err, found) { assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); assert.equal(found[0]._id.toString(), one._id); db.close(); done(); @@ -1170,8 +1220,8 @@ describe('model: querying:', function() { }); it('where $ne', function(done) { - var db = start() - , Mod = db.model('Mod', 'mods_' + random()); + var db = start(), + Mod = db.model('Mod', 'mods_' + random()); Mod.create({num: 1}, function(err) { assert.ifError(err); Mod.create({num: 2}, function(err, two) { @@ -1182,8 +1232,8 @@ describe('model: querying:', function() { assert.ifError(err); assert.equal(found.length, 2); - assert.equal(found[0]._id.toString(),two._id); - assert.equal(found[1]._id.toString(),three._id); + assert.equal(found[0]._id.toString(), two._id); + assert.equal(found[1]._id.toString(), three._id); db.close(); done(); }); @@ -1193,8 +1243,8 @@ describe('model: querying:', function() { }); it('where $nor', function(done) { - var db = start() - , Mod = db.model('Mod', 'nor_' + random()); + var db = start(), + Mod = db.model('Mod', 'nor_' + random()); Mod.create({num: 1}, {num: 2, str: 'two'}, function(err, one, two) { assert.ifError(err); @@ -1208,31 +1258,33 @@ describe('model: querying:', function() { Mod.find({$nor: [{num: 1}, {num: 3}]}, function(err, found) { cb(); assert.ifError(err); - assert.equal(1, found.length); - assert.equal(found[0]._id.toString(),two._id); + assert.equal(found.length, 1); + assert.equal(found[0]._id.toString(), two._id); }); } function test2() { - Mod.find({ $nor: [{ str: 'two'}, {str:'three'}] }, function(err, found) { + Mod.find({$nor: [{str: 'two'}, {str: 'three'}]}, function(err, found) { cb(); assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); assert.equal(found[0]._id.toString(), one._id); }); } function test3() { - Mod.find({$nor: [{num: 2}]}).nor([{ str: 'two' }]).exec(function(err, found) { + Mod.find({$nor: [{num: 2}]}).nor([{str: 'two'}]).exec(function(err, found) { cb(); assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); assert.equal(found[0]._id.toString(), one._id); }); } function cb() { - if (--pending) return; + if (--pending) { + return; + } db.close(); done(); } @@ -1243,14 +1295,14 @@ describe('model: querying:', function() { var db = start(); var BlogPostB = db.model('BlogPostB', collection + random()); - var a = { title: 'A', author: null}; - var b = { title: 'B' }; + var a = {title: 'A', author: null}; + var b = {title: 'B'}; BlogPostB.create(a, b, function(err, createdA) { assert.ifError(err); BlogPostB.find({author: {$in: [null], $exists: true}}, function(err, found) { db.close(); assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); assert.equal(found[0]._id.toString(), createdA._id); done(); }); @@ -1258,27 +1310,27 @@ describe('model: querying:', function() { }); it('null matches null and undefined', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection + random()); + var db = start(), + BlogPostB = db.model('BlogPostB', collection + random()); BlogPostB.create( - { title: 'A', author: null } - , { title: 'B' }, function(err) { - assert.ifError(err); - BlogPostB.find({author: null}, function(err, found) { - db.close(); + {title: 'A', author: null}, + {title: 'B'}, function(err) { assert.ifError(err); - assert.equal(2, found.length); - done(); + BlogPostB.find({author: null}, function(err, found) { + db.close(); + assert.ifError(err); + assert.equal(found.length, 2); + done(); + }); }); - }); }); it('a document whose arrays contain at least $all string values', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); - var post = new BlogPostB({ title: "Aristocats" }); + var post = new BlogPostB({title: 'Aristocats'}); post.tags.push('onex'); post.tags.push('twox'); @@ -1290,23 +1342,23 @@ describe('model: querying:', function() { BlogPostB.findById(post._id, function(err, post) { assert.ifError(err); - BlogPostB.find({ title: { '$all': ['Aristocats']}}, function(err, docs) { + BlogPostB.find({title: {'$all': ['Aristocats']}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); - BlogPostB.find({ title: { '$all': [/^Aristocats/]}}, function(err, docs) { + BlogPostB.find({title: {'$all': [/^Aristocats/]}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); - BlogPostB.find({tags: { '$all': ['onex','twox','threex']}}, function(err, docs) { + BlogPostB.find({tags: {'$all': ['onex', 'twox', 'threex']}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); - BlogPostB.find({tags: { '$all': [/^onex/i]}}, function(err, docs) { + BlogPostB.find({tags: {'$all': [/^onex/i]}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); - BlogPostB.findOne({tags: { '$all': /^two/ }}, function(err, doc) { + BlogPostB.findOne({tags: {'$all': /^two/}}, function(err, doc) { db.close(); assert.ifError(err); assert.equal(post.id, doc.id); @@ -1317,31 +1369,30 @@ describe('model: querying:', function() { }); }); }); - }); }); it('using #nor with nested #elemMatch', function(done) { - var db = start() - , P = db.model('BlogPostB', collection + '_norWithNestedElemMatch'); + var db = start(), + P = db.model('BlogPostB', collection + '_norWithNestedElemMatch'); - var p0 = { title: "nested $nor elemMatch1", comments: [] }; + var p0 = {title: 'nested $nor elemMatch1', comments: []}; - var p1 = { title: "nested $nor elemMatch0", comments: [] }; - p1.comments.push({ title: 'comment X' }, { title: 'comment Y' }, { title: 'comment W' }); + var p1 = {title: 'nested $nor elemMatch0', comments: []}; + p1.comments.push({title: 'comment X'}, {title: 'comment Y'}, {title: 'comment W'}); P.create(p0, p1, function(err, post0, post1) { assert.ifError(err); var id = post1.comments[1]._id; - var query0 = { comments: { $elemMatch: { title: 'comment Z' }}}; - var query1 = { comments: { $elemMatch: { _id: id.toString(), title: 'comment Y' }}}; + var query0 = {comments: {$elemMatch: {title: 'comment Z'}}}; + var query1 = {comments: {$elemMatch: {_id: id.toString(), title: 'comment Y'}}}; - P.find({ $nor: [query0, query1] }, function(err, posts) { + P.find({$nor: [query0, query1]}, function(err, posts) { db.close(); assert.ifError(err); - assert.equal(1, posts.length); + assert.equal(posts.length, 1); assert.equal(posts[0].id, post0.id); done(); }); @@ -1349,8 +1400,8 @@ describe('model: querying:', function() { }); it('strings via regexp', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); BlogPostB.create({title: 'Next to Normal'}, function(err, created) { assert.ifError(err); @@ -1360,12 +1411,12 @@ describe('model: querying:', function() { var reg = '^Next to Normal$'; - BlogPostB.find({ title: { $regex: reg }}, function(err, found) { + BlogPostB.find({title: {$regex: reg}}, function(err, found) { assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); assert.equal(found[0]._id.toString(), created._id); - BlogPostB.findOne({ title: { $regex: reg }}, function(err, found) { + BlogPostB.findOne({title: {$regex: reg}}, function(err, found) { assert.ifError(err); assert.equal(found._id.toString(), created._id); @@ -1387,24 +1438,24 @@ describe('model: querying:', function() { }); it('a document whose arrays contain at least $all values', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); - var a1 = {numbers: [-1,-2,-3,-4], meta: { visitors: 4 }}; - var a2 = {numbers: [0,-1,-2,-3,-4]}; + var db = start(), + BlogPostB = db.model('BlogPostB', collection); + var a1 = {numbers: [-1, -2, -3, -4], meta: {visitors: 4}}; + var a2 = {numbers: [0, -1, -2, -3, -4]}; BlogPostB.create(a1, a2, function(err, whereoutZero, whereZero) { assert.ifError(err); BlogPostB.find({numbers: {$all: [-1, -2, -3, -4]}}, function(err, found) { assert.ifError(err); - assert.equal(2, found.length); - BlogPostB.find({'meta.visitors': {$all: [4] }}, function(err, found) { + assert.equal(found.length, 2); + BlogPostB.find({'meta.visitors': {$all: [4]}}, function(err, found) { assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); assert.equal(found[0]._id.toString(), whereoutZero._id); BlogPostB.find({numbers: {$all: [0, -1]}}, function(err, found) { db.close(); assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); assert.equal(found[0]._id.toString(), whereZero._id); done(); }); @@ -1414,21 +1465,21 @@ describe('model: querying:', function() { }); it('where $size', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); - BlogPostB.create({numbers: [1,2,3,4,5,6,7,8,9,10]}, function(err) { + BlogPostB.create({numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}, function(err) { assert.ifError(err); - BlogPostB.create({numbers: [11,12,13,14,15,16,17,18,19,20]}, function(err) { + BlogPostB.create({numbers: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}, function(err) { assert.ifError(err); - BlogPostB.create({numbers: [1,2,3,4,5,6,7,8,9,10,11]}, function(err) { + BlogPostB.create({numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}, function(err) { assert.ifError(err); BlogPostB.find({numbers: {$size: 10}}, function(err, found) { assert.ifError(err); - assert.equal(2, found.length); + assert.equal(found.length, 2); BlogPostB.find({numbers: {$size: 11}}, function(err, found) { assert.ifError(err); - assert.equal(1, found.length); + assert.equal(found.length, 1); db.close(); done(); }); @@ -1442,47 +1493,57 @@ describe('model: querying:', function() { var db = start(); var D = db.model('D', new Schema({dt: String}), collection); - D.create({ dt: '2011-03-30' }, cb); - D.create({ dt: '2011-03-31' }, cb); - D.create({ dt: '2011-04-01' }, cb); - D.create({ dt: '2011-04-02' }, cb); + D.create({dt: '2011-03-30'}, cb); + D.create({dt: '2011-03-31'}, cb); + D.create({dt: '2011-04-01'}, cb); + D.create({dt: '2011-04-02'}, cb); var pending = 4; + function cb(err) { - if (err) db.close(); + if (err) { + db.close(); + } assert.ifError(err); - if (--pending) return; + if (--pending) { + return; + } pending = 2; - D.find({ 'dt': { $gte: '2011-03-30', $lte: '2011-04-01' }}).sort('dt').exec(function(err, docs) { + D.find({'dt': {$gte: '2011-03-30', $lte: '2011-04-01'}}).sort('dt').exec(function(err, docs) { if (!--pending) { db.close(); done(); } assert.ifError(err); - assert.equal(3, docs.length); + assert.equal(docs.length, 3); assert.equal(docs[0].dt, '2011-03-30'); assert.equal(docs[1].dt, '2011-03-31'); assert.equal(docs[2].dt, '2011-04-01'); - assert.equal(false, docs.some(function(d) { return '2011-04-02' === d.dt; })); + assert.ok(!docs.some(function(d) { + return d.dt === '2011-04-02'; + })); }); - D.find({ 'dt': { $gt: '2011-03-30', $lt: '2011-04-02' }}).sort('dt').exec(function(err, docs) { + D.find({'dt': {$gt: '2011-03-30', $lt: '2011-04-02'}}).sort('dt').exec(function(err, docs) { if (!--pending) { db.close(); done(); } assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); assert.equal(docs[0].dt, '2011-03-31'); assert.equal(docs[1].dt, '2011-04-01'); - assert.equal(false, docs.some(function(d) { return '2011-03-30' === d.dt; })); - assert.equal(false, docs.some(function(d) { return '2011-04-02' === d.dt; })); + assert.ok(!docs.some(function(d) { + return d.dt === '2011-03-30'; + })); + assert.ok(!docs.some(function(d) { + return d.dt === '2011-04-02'; + })); }); } - }); describe('text search indexes', function() { @@ -1491,57 +1552,67 @@ describe('model: querying:', function() { return done(); } - var db = start() - , blogPost = db.model('BlogPostB', collection); + var db = start(), + blogPost = db.model('BlogPostB', collection); - blogPost.collection.ensureIndex({ title : 'text' }, function(error) { + blogPost.collection.ensureIndex({title: 'text'}, function(error) { assert.ifError(error); - var a = new blogPost({ title : 'querying in mongoose' }); - var b = new blogPost({ title : 'text search in mongoose' }); + var a = new blogPost({title: 'querying in mongoose'}); + var b = new blogPost({title: 'text search in mongoose'}); a.save(function(error) { assert.ifError(error); b.save(function(error) { assert.ifError(error); blogPost. - find({ $text : { $search : 'text search' } }, { score : { $meta: "textScore" } }). - limit(2). - exec(function(error, documents) { + find({$text: {$search: 'text search'}}, {score: {$meta: 'textScore'}}). + limit(2). + exec(function(error, documents) { + assert.ifError(error); + assert.equal(documents.length, 1); + assert.equal(documents[0].title, 'text search in mongoose'); + a.remove(function(error) { assert.ifError(error); - assert.equal(1, documents.length); - assert.equal('text search in mongoose', documents[0].title); - a.remove(function(error) { + b.remove(function(error) { assert.ifError(error); - b.remove(function(error) { - assert.ifError(error); - db.close(done); - }); + db.close(done); }); }); + }); }); }); }); }); - it('works when text search is called by a schema', function(done) { + it('works when text search is called by a schema (gh-3824)', function(done) { + if (!mongo26_or_greater) { + return done(); + } + var db = start(); var exampleSchema = new Schema({ title: String, - name: {type: String, text: true }, + name: { type: String, text: true }, large_text: String }); - var indexes = exampleSchema.indexes(); - assert.equal(indexes[0][1].text, true); - db.close(done); + var Example = db.model('gh3824', exampleSchema); + + Example.on('index', function(error) { + assert.ifError(error); + Example.findOne({ $text: { $search: 'text search' } }, function(error) { + assert.ifError(error); + db.close(done); + }); + }); }); }); }); describe('limit', function() { it('works', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); BlogPostB.create({title: 'first limit'}, function(err, first) { assert.ifError(err); @@ -1549,10 +1620,10 @@ describe('model: querying:', function() { assert.ifError(err); BlogPostB.create({title: 'third limit'}, function(err) { assert.ifError(err); - BlogPostB.find({title: /limit$/}).limit(2).find( function(err, found) { + BlogPostB.find({title: /limit$/}).limit(2).find(function(err, found) { db.close(); assert.ifError(err); - assert.equal(2, found.length); + assert.equal(found.length, 2); assert.equal(found[0].id, first.id); assert.equal(found[1].id, second.id); done(); @@ -1565,8 +1636,8 @@ describe('model: querying:', function() { describe('skip', function() { it('works', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); BlogPostB.create({title: '1 skip'}, function(err) { assert.ifError(err); @@ -1574,9 +1645,9 @@ describe('model: querying:', function() { assert.ifError(err); BlogPostB.create({title: '3 skip'}, function(err, third) { assert.ifError(err); - BlogPostB.find({title: /skip$/}).sort({ title: 1 }).skip(1).limit(2).find( function(err, found) { + BlogPostB.find({title: /skip$/}).sort({title: 1}).skip(1).limit(2).find(function(err, found) { assert.ifError(err); - assert.equal(2,found.length); + assert.equal(found.length, 2); assert.equal(found[0].id, second._id); assert.equal(found[1].id, third._id); db.close(); @@ -1590,8 +1661,8 @@ describe('model: querying:', function() { describe('sort', function() { it('works', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); BlogPostB.create({meta: {visitors: 100}}, function(err, least) { assert.ifError(err); @@ -1602,9 +1673,9 @@ describe('model: querying:', function() { BlogPostB .where('meta.visitors').gt(99).lt(301) .sort('-meta.visitors') - .find( function(err, found) { + .find(function(err, found) { assert.ifError(err); - assert.equal(3, found.length); + assert.equal(found.length, 3); assert.equal(found[0].id, largest._id); assert.equal(found[1].id, middle._id); assert.equal(found[2].id, least._id); @@ -1620,29 +1691,29 @@ describe('model: querying:', function() { return done(); } - var db = start() - , blogPost = db.model('BlogPostB', collection); + var db = start(), + blogPost = db.model('BlogPostB', collection); - blogPost.collection.ensureIndex({ title : 'text' }, function(error) { + blogPost.collection.ensureIndex({title: 'text'}, function(error) { assert.ifError(error); - var a = new blogPost({ title : 'searching in mongoose' }); - var b = new blogPost({ title : 'text search in mongoose' }); + var a = new blogPost({title: 'searching in mongoose'}); + var b = new blogPost({title: 'text search in mongoose'}); a.save(function(error) { assert.ifError(error); b.save(function(error) { assert.ifError(error); blogPost. - find({ $text : { $search : 'text search' } }, { score : { $meta: "textScore" } }). - sort({ score : { $meta : 'textScore' } }). - limit(2). - exec(function(error, documents) { - assert.ifError(error); - assert.equal(2, documents.length); - assert.equal('text search in mongoose', documents[0].title); - assert.equal('searching in mongoose', documents[1].title); - db.close(); - done(); - }); + find({$text: {$search: 'text search'}}, {score: {$meta: 'textScore'}}). + sort({score: {$meta: 'textScore'}}). + limit(2). + exec(function(error, documents) { + assert.ifError(error); + assert.equal(documents.length, 2); + assert.equal(documents[0].title, 'text search in mongoose'); + assert.equal(documents[1].title, 'searching in mongoose'); + db.close(); + done(); + }); }); }); }); @@ -1651,10 +1722,10 @@ describe('model: querying:', function() { describe('nested mixed "x.y.z"', function() { it('works', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection); + var db = start(), + BlogPostB = db.model('BlogPostB', collection); - BlogPostB.find({ 'mixed.nested.stuff': 'skynet' }, function(err) { + BlogPostB.find({'mixed.nested.stuff': 'skynet'}, function(err) { db.close(); assert.ifError(err); done(); @@ -1664,48 +1735,48 @@ describe('model: querying:', function() { it('by Date (gh-336)', function(done) { // GH-336 - var db = start() - , Test = db.model('TestDateQuery', new Schema({ date: Date }), 'datetest_' + random()) - , now = new Date; + var db = start(), + Test = db.model('TestDateQuery', new Schema({date: Date}), 'datetest_' + random()), + now = new Date; - Test.create({ date: now }, { date: new Date(now - 10000) }, function(err) { + Test.create({date: now}, {date: new Date(now - 10000)}, function(err) { assert.ifError(err); - Test.find({ date: now }, function(err, docs) { + Test.find({date: now}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); }); }); it('mixed types with $elemMatch (gh-591)', function(done) { - var db = start() - , S = new Schema({ a: [{}], b: Number }) - , M = db.model('QueryingMixedArrays', S, random()); + var db = start(), + S = new Schema({a: [{}], b: Number}), + M = db.model('QueryingMixedArrays', S, random()); var m = new M; - m.a = [1,2,{ name: 'Frodo' },'IDK', {name: 100}]; + m.a = [1, 2, {name: 'Frodo'}, 'IDK', {name: 100}]; m.b = 10; m.save(function(err) { assert.ifError(err); - M.find({ a: { name: 'Frodo' }, b: '10' }, function(err, docs) { + M.find({a: {name: 'Frodo'}, b: '10'}, function(err, docs) { assert.ifError(err); - assert.equal(5, docs[0].a.length); - assert.equal(10, docs[0].b.valueOf()); + assert.equal(docs[0].a.length, 5); + assert.equal(docs[0].b.valueOf(), 10); var query = { a: { - $elemMatch: { name: 100 } + $elemMatch: {name: 100} } }; M.find(query, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(5, docs[0].a.length); + assert.equal(docs[0].a.length, 5); done(); }); }); @@ -1716,28 +1787,28 @@ describe('model: querying:', function() { it('with ObjectIds (gh-690)', function(done) { var db = start(); - var SSchema = new Schema({ name: String }); - var PSchema = new Schema({ sub: [SSchema] }); + var SSchema = new Schema({name: String}); + var PSchema = new Schema({sub: [SSchema]}); var P = db.model('usingAllWithObjectIds', PSchema); - var sub = [{ name: 'one' }, { name: 'two' }, { name: 'three' }]; + var sub = [{name: 'one'}, {name: 'two'}, {name: 'three'}]; - P.create({ sub: sub }, function(err, p) { + P.create({sub: sub}, function(err, p) { assert.ifError(err); var o0 = p.sub[0]._id; var o1 = p.sub[1]._id; var o2 = p.sub[2]._id; - P.findOne({ 'sub._id': { $all: [o1, o2.toString()] }}, function(err, doc) { + P.findOne({'sub._id': {$all: [o1, o2.toString()]}}, function(err, doc) { assert.ifError(err); assert.equal(doc.id, p.id); - P.findOne({ 'sub._id': { $all: [o0, new DocumentObjectId] }}, function(err, doc) { + P.findOne({'sub._id': {$all: [o0, new DocumentObjectId]}}, function(err, doc) { assert.ifError(err); - assert.equal(false, !!doc); + assert.equal(!!doc, false); - P.findOne({ 'sub._id': { $all: [o2] }}, function(err, doc) { + P.findOne({'sub._id': {$all: [o2]}}, function(err, doc) { db.close(); assert.ifError(err); assert.equal(doc.id, p.id); @@ -1749,35 +1820,35 @@ describe('model: querying:', function() { }); it('with Dates', function(done) { - this.timeout(3000); var db = start(); - var SSchema = new Schema({ d: Date }); - var PSchema = new Schema({ sub: [SSchema] }); + this.timeout(process.env.TRAVIS ? 8000 : 4500); + var SSchema = new Schema({d: Date}); + var PSchema = new Schema({sub: [SSchema]}); var P = db.model('usingAllWithDates', PSchema); var sub = [ - { d: new Date } - , { d: new Date(Date.now() - 10000) } - , { d: new Date(Date.now() - 30000) } + {d: new Date}, + {d: new Date(Date.now() - 10000)}, + {d: new Date(Date.now() - 30000)} ]; - P.create({ sub: sub }, function(err, p) { + P.create({sub: sub}, function(err, p) { assert.ifError(err); var o0 = p.sub[0].d; var o1 = p.sub[1].d; var o2 = p.sub[2].d; - P.findOne({ 'sub.d': { $all: [o1, o2] }}, function(err, doc) { + P.findOne({'sub.d': {$all: [o1, o2]}}, function(err, doc) { assert.ifError(err); - assert.equal(doc.id,p.id); + assert.equal(doc.id, p.id); - P.findOne({ 'sub.d': { $all: [o0, new Date] }}, function(err, doc) { + P.findOne({'sub.d': {$all: [o0, new Date]}}, function(err, doc) { assert.ifError(err); - assert.equal(false, !!doc); + assert.equal(!!doc, false); - P.findOne({ 'sub.d': { $all: [o2] }}, function(err, doc) { + P.findOne({'sub.d': {$all: [o2]}}, function(err, doc) { assert.ifError(err); assert.equal(doc.id, p.id); db.close(done); @@ -1794,7 +1865,7 @@ describe('model: querying:', function() { if (err) { throw err; } - var mongo26_or_greater = 2 < version[0] || (2 == version[0] && 6 <= version[1]); + var mongo26_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 6); if (!mongo26_or_greater) { return done(); } @@ -1803,12 +1874,12 @@ describe('model: querying:', function() { }); var next = function() { - var schema = new Schema({ test: [String] }); + var schema = new Schema({test: [String]}); var MyModel = db.model('gh3163', schema); - MyModel.create({ test: ['log1', 'log2'] }, function(error) { + MyModel.create({test: ['log1', 'log2']}, function(error) { assert.ifError(error); - var query = { test: { $all: [{ $elemMatch: { $regex: /log/g } }] } }; + var query = {test: {$all: [{$elemMatch: {$regex: /log/g}}]}}; MyModel.find(query, function(error, docs) { assert.ifError(error); assert.equal(docs.length, 1); @@ -1824,38 +1895,38 @@ describe('model: querying:', function() { var db = start(); var B = db.model('BlogPostB'); - B.create({ title: 'and operator', published: false, author: 'Me' }, function(err) { + B.create({title: 'and operator', published: false, author: 'Me'}, function(err) { assert.ifError(err); - B.find({ $and: [{ title: 'and operator' }] }, function(err, docs) { + B.find({$and: [{title: 'and operator'}]}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); - B.find({ $and: [{ title: 'and operator' }, { published: true }] }, function(err, docs) { + B.find({$and: [{title: 'and operator'}, {published: true}]}, function(err, docs) { assert.ifError(err); - assert.equal(0, docs.length); + assert.equal(docs.length, 0); - B.find({ $and: [{ title: 'and operator' }, { published: false }] }, function(err, docs) { + B.find({$and: [{title: 'and operator'}, {published: false}]}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); var query = B.find(); query.and([ - { title: 'and operator', published: false }, - { author: 'Me' } + {title: 'and operator', published: false}, + {author: 'Me'} ]); query.exec(function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); var query = B.find(); query.and([ - { title: 'and operator', published: false }, - { author: 'You' } + {title: 'and operator', published: false}, + {author: 'You'} ]); query.exec(function(err, docs) { assert.ifError(err); - assert.equal(0, docs.length); + assert.equal(docs.length, 0); db.close(done); }); }); @@ -1867,57 +1938,56 @@ describe('model: querying:', function() { it('works with nested query selectors gh-1884', function(done) { var db = start(); - var B = db.model('gh1884', { a: String, b: String }, 'gh1884'); + var B = db.model('gh1884', {a: String, b: String}, 'gh1884'); - B.remove({ $and: [{ a: 'coffee' }, { b: { $in: ['bacon', 'eggs'] } }] }, function(error) { + B.remove({$and: [{a: 'coffee'}, {b: {$in: ['bacon', 'eggs']}}]}, function(error) { assert.ifError(error); db.close(done); }); }); }); -}); -describe('buffers', function() { it('works with different methods and query types', function(done) { - var db = start() - , BufSchema = new Schema({ name: String, block: Buffer }) - , Test = db.model('Buffer', BufSchema, "buffers"); + var db = start(), + BufSchema = new Schema({name: String, block: Buffer}), + Test = db.model('BufferTest', BufSchema, 'buffers'); - var docA = { name: 'A', block: new Buffer('über') }; - var docB = { name: 'B', block: new Buffer("buffer shtuffs are neat") }; - var docC = { name: 'C', block: 'hello world' }; + var docA = {name: 'A', block: new Buffer('über')}; + var docB = {name: 'B', block: new Buffer('buffer shtuffs are neat')}; + var docC = {name: 'C', block: 'hello world'}; Test.create(docA, docB, docC, function(err, a, b, c) { assert.ifError(err); - assert.equal(b.block.toString('utf8'),'buffer shtuffs are neat'); - assert.equal(a.block.toString('utf8'),'über'); - assert.equal(c.block.toString('utf8'),'hello world'); + assert.equal(b.block.toString('utf8'), 'buffer shtuffs are neat'); + assert.equal(a.block.toString('utf8'), 'über'); + assert.equal(c.block.toString('utf8'), 'hello world'); Test.findById(a._id, function(err, a) { assert.ifError(err); - assert.equal(a.block.toString('utf8'),'über'); + assert.equal(a.block.toString('utf8'), 'über'); - Test.findOne({ block: 'buffer shtuffs are neat' }, function(err, rb) { + Test.findOne({block: 'buffer shtuffs are neat'}, function(err, rb) { assert.ifError(err); - assert.equal(rb.block.toString('utf8'),'buffer shtuffs are neat'); + assert.equal(rb.block.toString('utf8'), 'buffer shtuffs are neat'); - Test.findOne({ block: /buffer/i }, function(err) { - assert.equal(err.message, 'Cast to buffer failed for value "/buffer/i" at path "block"'); - Test.findOne({ block: [195, 188, 98, 101, 114] }, function(err, rb) { + Test.findOne({block: /buffer/i}, function(err) { + assert.equal(err.message, 'Cast to buffer failed for value ' + + '"/buffer/i" at path "block" for model "BufferTest"'); + Test.findOne({block: [195, 188, 98, 101, 114]}, function(err, rb) { assert.ifError(err); - assert.equal(rb.block.toString('utf8'),'über'); + assert.equal(rb.block.toString('utf8'), 'über'); - Test.findOne({ block: 'aGVsbG8gd29ybGQ=' }, function(err, rb) { + Test.findOne({block: 'aGVsbG8gd29ybGQ='}, function(err, rb) { assert.ifError(err); assert.strictEqual(rb, null); - Test.findOne({ block: new Buffer('aGVsbG8gd29ybGQ=', 'base64') }, function(err, rb) { + Test.findOne({block: new Buffer('aGVsbG8gd29ybGQ=', 'base64')}, function(err, rb) { assert.ifError(err); - assert.equal(rb.block.toString('utf8'),'hello world'); + assert.equal(rb.block.toString('utf8'), 'hello world'); - Test.findOne({ block: new MongooseBuffer('aGVsbG8gd29ybGQ=', 'base64') }, function(err, rb) { + Test.findOne({block: new MongooseBuffer('aGVsbG8gd29ybGQ=', 'base64')}, function(err, rb) { assert.ifError(err); - assert.equal(rb.block.toString('utf8'),'hello world'); + assert.equal(rb.block.toString('utf8'), 'hello world'); Test.remove({}, function(err) { db.close(); @@ -1931,80 +2001,79 @@ describe('buffers', function() { }); }); }); - }); }); it('with conditionals', function(done) { // $in $nin etc - var db = start() - , BufSchema = new Schema({ name: String, block: Buffer }) - , Test = db.model('Buffer2', BufSchema, "buffer_" + random()); + var db = start(), + BufSchema = new Schema({name: String, block: Buffer}), + Test = db.model('Buffer2', BufSchema, 'buffer_' + random()); - var docA = { name: 'A', block: new MongooseBuffer([195, 188, 98, 101, 114]) }; //über - var docB = { name: 'B', block: new MongooseBuffer("buffer shtuffs are neat") }; - var docC = { name: 'C', block: new MongooseBuffer('aGVsbG8gd29ybGQ=', 'base64') }; + var docA = {name: 'A', block: new MongooseBuffer([195, 188, 98, 101, 114])}; // über + var docB = {name: 'B', block: new MongooseBuffer('buffer shtuffs are neat')}; + var docC = {name: 'C', block: new MongooseBuffer('aGVsbG8gd29ybGQ=', 'base64')}; Test.create(docA, docB, docC, function(err, a, b, c) { assert.ifError(err); - assert.equal(a.block.toString('utf8'),'über'); - assert.equal(b.block.toString('utf8'),'buffer shtuffs are neat'); - assert.equal(c.block.toString('utf8'),'hello world'); + assert.equal(a.block.toString('utf8'), 'über'); + assert.equal(b.block.toString('utf8'), 'buffer shtuffs are neat'); + assert.equal(c.block.toString('utf8'), 'hello world'); - Test.find({ block: { $in: [[195, 188, 98, 101, 114], "buffer shtuffs are neat", new Buffer('aGVsbG8gd29ybGQ=', 'base64')] }}, function(err, tests) { + Test.find({block: {$in: [[195, 188, 98, 101, 114], 'buffer shtuffs are neat', new Buffer('aGVsbG8gd29ybGQ=', 'base64')]}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(3, tests.length); + assert.equal(tests.length, 3); }); - Test.find({ block: { $in: ['über', 'hello world'] }}, function(err, tests) { + Test.find({block: {$in: ['über', 'hello world']}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(2, tests.length); + assert.equal(tests.length, 2); }); - Test.find({ block: { $in: ['über'] }}, function(err, tests) { + Test.find({block: {$in: ['über']}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(1, tests.length); - assert.equal(tests[0].block.toString('utf8'),'über'); + assert.equal(tests.length, 1); + assert.equal(tests[0].block.toString('utf8'), 'über'); }); - Test.find({ block: { $nin: ['über'] }}, function(err, tests) { + Test.find({block: {$nin: ['über']}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(2, tests.length); + assert.equal(tests.length, 2); }); - Test.find({ block: { $nin: [[195, 188, 98, 101, 114], new Buffer('aGVsbG8gd29ybGQ=', 'base64')] }}, function(err, tests) { + Test.find({block: {$nin: [[195, 188, 98, 101, 114], new Buffer('aGVsbG8gd29ybGQ=', 'base64')]}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(1, tests.length); - assert.equal(tests[0].block.toString('utf8'),'buffer shtuffs are neat'); + assert.equal(tests.length, 1); + assert.equal(tests[0].block.toString('utf8'), 'buffer shtuffs are neat'); }); - Test.find({ block: { $ne: 'über' }}, function(err, tests) { + Test.find({block: {$ne: 'über'}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(2, tests.length); + assert.equal(tests.length, 2); }); - Test.find({ block: { $gt: 'über' }}, function(err, tests) { + Test.find({block: {$gt: 'über'}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(2, tests.length); + assert.equal(tests.length, 2); }); - Test.find({ block: { $gte: 'über' }}, function(err, tests) { + Test.find({block: {$gte: 'über'}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(3, tests.length); + assert.equal(tests.length, 3); }); - Test.find({ block: { $lt: new Buffer('buffer shtuffs are neat') }}, function(err, tests) { + Test.find({block: {$lt: new Buffer('buffer shtuffs are neat')}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(2, tests.length); + assert.equal(tests.length, 2); var ret = {}; ret[tests[0].block.toString('utf8')] = 1; ret[tests[1].block.toString('utf8')] = 1; @@ -2012,15 +2081,18 @@ describe('buffers', function() { assert.ok(ret['über'] !== undefined); }); - Test.find({ block: { $lte: 'buffer shtuffs are neat' }}, function(err, tests) { + Test.find({block: {$lte: 'buffer shtuffs are neat'}}, function(err, tests) { cb(); assert.ifError(err); - assert.equal(3, tests.length); + assert.equal(tests.length, 3); }); - var pending = 9; + var pending = 10; + function cb() { - if (--pending) return; + if (--pending) { + return; + } Test.remove({}, function(err) { db.close(); assert.ifError(err); @@ -2029,20 +2101,18 @@ describe('buffers', function() { } }); }); -}); -describe('backwards compatibility', function() { it('with previously existing null values in the db', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , post = new BlogPostB(); + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + post = new BlogPostB(); - post.collection.insert({ meta: { visitors: 9898, a: null } }, {}, function(err, b) { + post.collection.insert({meta: {visitors: 9898, a: null}}, {}, function(err, b) { assert.ifError(err); BlogPostB.findOne({_id: b.ops[0]._id}, function(err, found) { assert.ifError(err); - assert.equal(9898, found.get('meta.visitors').valueOf()); + assert.equal(found.get('meta.visitors').valueOf(), 9898); db.close(); done(); }); @@ -2050,16 +2120,16 @@ describe('backwards compatibility', function() { }); it('with unused values in the db', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , post = new BlogPostB(); + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + post = new BlogPostB(); - post.collection.insert({ meta: { visitors: 9898, color: 'blue'}}, {}, function(err, b) { + post.collection.insert({meta: {visitors: 9898, color: 'blue'}}, {}, function(err, b) { assert.ifError(err); BlogPostB.findOne({_id: b.ops[0]._id}, function(err, found) { assert.ifError(err); - assert.equal(9898, found.get('meta.visitors').valueOf()); + assert.equal(found.get('meta.visitors').valueOf(), 9898); found.save(function(err) { assert.ifError(err); db.close(); @@ -2068,76 +2138,89 @@ describe('backwards compatibility', function() { }); }); }); -}); -describe('geo-spatial', function() { describe('2d', function() { it('$near (gh-309)', function(done) { - var db = start() - , Test = db.model('Geo1', geoSchema, 'geospatial' + random()); + var db = start(), + Test = db.model('Geo1', geoSchema, 'geospatial' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.ran = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.ran = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: [ 10, 20 ]}, { loc: [ 40, 90 ]}, complete); + Test.create({loc: [10, 20]}, {loc: [40, 90]}, complete); function test() { - Test.find({ loc: { $near: [30, 40] }}, function(err, docs) { + Test.find({loc: {$near: [30, 40]}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); done(); }); } }); it('$within arrays (gh-586)', function(done) { - var db = start() - , Test = db.model('Geo2', geoSchema, collection + 'geospatial'); + var db = start(), + Test = db.model('Geo2', geoSchema, collection + 'geospatial'); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.ran = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.ran = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: [ 35, 50 ]}, { loc: [ -40, -90 ]}, complete); + Test.create({loc: [35, 50]}, {loc: [-40, -90]}, complete); function test() { - Test.find({ loc: { '$within': { '$box': [[30,40], [40,60]] }}}, function(err, docs) { + Test.find({loc: {'$within': {'$box': [[30, 40], [40, 60]]}}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); done(); }); } }); it('$nearSphere with arrays (gh-610)', function(done) { - var db = start() - , Test = db.model('Geo3', geoSchema, "y" + random()); + var db = start(), + Test = db.model('Geo3', geoSchema, 'y' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.ran = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.ran = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: [ 10, 20 ]}, { loc: [ 40, 90 ]}, complete); + Test.create({loc: [10, 20]}, {loc: [40, 90]}, complete); function test() { - Test.find({ loc: { $nearSphere: [30, 40] }}, function(err, docs) { + Test.find({loc: {$nearSphere: [30, 40]}}, function(err, docs) { assert.ifError(err); - assert.equal(2, docs.length); + assert.equal(docs.length, 2); db.close(done); }); } @@ -2146,32 +2229,37 @@ describe('geo-spatial', function() { it('$nearSphere with invalid coordinate does not crash (gh-1874)', function(done) { var geoSchema = new Schema({ loc: { - type: { type: String }, - coordinates: { type: [Number], index: '2dsphere' } + type: {type: String}, + coordinates: {type: [Number], index: '2dsphere'} } }); - var db = start() - , Test = db.model('gh1874', geoSchema, 'gh1874'); + var db = start(), + Test = db.model('gh1874', geoSchema, 'gh1874'); var pending = 2; var complete = function(err) { - if (complete.ran) return; - if (err) return done(complete.ran = err); + if (complete.ran) { + return; + } + if (err) { + done(complete.ran = err); + return; + } --pending || test(); }; Test.on('index', complete); Test.create( - { loc: { coordinates: [ 30, 41 ] } }, - { loc: { coordinates: [ 31, 40 ] } }, - complete); + {loc: {coordinates: [30, 41]}}, + {loc: {coordinates: [31, 40]}}, + complete); var test = function() { var q = new Query({}, {}, null, Test.collection); q.find({ - 'loc': { + loc: { $nearSphere: { - $geometry: { type: 'Point', coordinates: [30, 40] }, + $geometry: {type: 'Point', coordinates: [30, 40]}, $maxDistance: 10000000 } } @@ -2186,27 +2274,33 @@ describe('geo-spatial', function() { }); it('$maxDistance with arrays', function(done) { - var db = start() - , Test = db.model('Geo4', geoSchema, "x" + random()); + var db = start(), + Test = db.model('Geo4', geoSchema, 'x' + random()); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.ran = err); + if (complete.ran) { + return; + } + if (err) { + done(complete.ran = err); + return; + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: [ 20, 80 ]}, { loc: [ 25, 30 ]}, complete); + Test.create({loc: [20, 80]}, {loc: [25, 30]}, complete); function test() { - Test.find({ loc: { $near: [25, 31], $maxDistance: 1 }}, function(err, docs) { + Test.find({loc: {$near: [25, 31], $maxDistance: 1}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); - Test.find({ loc: { $near: [25, 32], $maxDistance: 1 }}, function(err, docs) { + assert.equal(docs.length, 1); + Test.find({loc: {$near: [25, 32], $maxDistance: 1}}, function(err, docs) { db.close(); assert.ifError(err); - assert.equal(0, docs.length); + assert.equal(docs.length, 0); done(); }); }); @@ -2215,33 +2309,44 @@ describe('geo-spatial', function() { }); describe('2dsphere', function() { - // mongodb 2.4 + var schema2dsphere; + var geoSchema; + var geoMultiSchema; - var schema2dsphere = new Schema({ loc: { type: [Number], index: '2dsphere'}}); + before(function() { + schema2dsphere = new Schema({loc: {type: [Number], index: '2dsphere'}}); - var geoSchema = new Schema({ line: { type: { type: String }, coordinates: []}}); - geoSchema.index({ line: '2dsphere' }); + geoSchema = new Schema({line: {type: {type: String}, coordinates: []}}); + geoSchema.index({line: '2dsphere'}); - var geoMultiSchema = new Schema({ geom: [{ type: { type: String }, coordinates: []}]}); - // see mongodb issue SERVER-8907 - // geoMultiSchema.index({ geom: '2dsphere' }); + geoMultiSchema = new Schema({geom: [{type: {type: String}, coordinates: []}]}); + // see mongodb issue SERVER-8907 + // geoMultiSchema.index({ geom: '2dsphere' }); + }); + // mongodb 2.4 var mongo24_or_greater = false; before(function(done) { start.mongodVersion(function(err, version) { - if (err) throw err; + if (err) { + throw err; + } - mongo24_or_greater = 2 < version[0] || (2 == version[0] && 4 <= version[1]); - if (!mongo24_or_greater) console.log('not testing mongodb 2.4 features'); + mongo24_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 4); + if (!mongo24_or_greater) { + console.log('not testing mongodb 2.4 features'); + } done(); }); }); it('index is allowed in schema', function(done) { - if (!mongo24_or_greater) return done(); + if (!mongo24_or_greater) { + return done(); + } var ok = schema2dsphere.indexes().some(function(index) { - return '2dsphere' == index[0].loc; + return index[0].loc === '2dsphere'; }); assert.ok(ok); done(); @@ -2249,27 +2354,29 @@ describe('geo-spatial', function() { describe('$geometry', function() { it('Polygon', function(done) { - if (!mongo24_or_greater) return done(); + if (!mongo24_or_greater) { + return done(); + } - var db = start() - , Test = db.model('2dsphere-polygon', schema2dsphere, 'geospatial' + random()); + var db = start(), + Test = db.model('2dsphere-polygon', schema2dsphere, 'geospatial' + random()); Test.on('index', function(err) { assert.ifError(err); - Test.create({ loc: [ 0, 0 ]}, function(err, created) { + Test.create({loc: [0, 0]}, function(err, created) { assert.ifError(err); - var geojsonPoly = { type: 'Polygon', coordinates: [[[-5,-5], ['-5',5], [5,5], [5,-5],[-5,'-5']]] }; + var geojsonPoly = {type: 'Polygon', coordinates: [[[-5, -5], ['-5', 5], [5, 5], [5, -5], [-5, '-5']]]}; - Test.find({ loc: { $within: { $geometry: geojsonPoly }}}, function(err, docs) { + Test.find({loc: {$within: {$geometry: geojsonPoly}}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); assert.equal(created.id, docs[0].id); Test.where('loc').within().geometry(geojsonPoly).exec(function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); assert.equal(created.id, docs[0].id); db.close(done); }); @@ -2281,22 +2388,24 @@ describe('geo-spatial', function() { describe('$geoIntersects', function() { it('LineString', function(done) { - if (!mongo24_or_greater) return done(); + if (!mongo24_or_greater) { + return done(); + } - var db = start() - , Test = db.model('2dsphere-geo', geoSchema, 'geospatial' + random()); + var db = start(), + Test = db.model('2dsphere-geo', geoSchema, 'geospatial' + random()); Test.on('index', function(err) { assert.ifError(err); - Test.create({ line: { type:'LineString', coordinates: [[-178.0, 10.0],[178.0,10.0]] }}, function(err, created) { + Test.create({line: {type: 'LineString', coordinates: [[-178.0, 10.0], [178.0, 10.0]]}}, function(err, created) { assert.ifError(err); - var geojsonLine = { type: 'LineString', coordinates: [[180.0, 11.0], [180.0, '9.00']] }; + var geojsonLine = {type: 'LineString', coordinates: [[180.0, 11.0], [180.0, '9.00']]}; - Test.find({ line: { $geoIntersects: { $geometry: geojsonLine }}}, function(err, docs) { + Test.find({line: {$geoIntersects: {$geometry: geojsonLine}}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); assert.equal(created.id, docs[0].id); Test.where('line').intersects().geometry(geojsonLine).findOne(function(err, doc) { @@ -2306,25 +2415,28 @@ describe('geo-spatial', function() { }); }); }); - }); }); it('MultiLineString', function(done) { - if (!mongo24_or_greater) return done(); + if (!mongo24_or_greater) { + return done(); + } - var db = start() - , Test = db.model('2dsphere-geo-multi1', geoMultiSchema, 'geospatial' + random()); + var db = start(), + Test = db.model('2dsphere-geo-multi1', geoMultiSchema, 'geospatial' + random()); - Test.create({ geom: [{ type:'LineString', coordinates: [[-178.0, 10.0],[178.0,10.0]] }, - { type:'LineString', coordinates: [[-178.0, 5.0],[178.0,5.0]] } ]}, function(err, created) { + Test.create({ + geom: [{type: 'LineString', coordinates: [[-178.0, 10.0], [178.0, 10.0]]}, + {type: 'LineString', coordinates: [[-178.0, 5.0], [178.0, 5.0]]}] + }, function(err, created) { assert.ifError(err); - var geojsonLine = { type: 'LineString', coordinates: [[180.0, 11.0], [180.0, '9.00']] }; + var geojsonLine = {type: 'LineString', coordinates: [[180.0, 11.0], [180.0, '9.00']]}; - Test.find({ geom: { $geoIntersects: { $geometry: geojsonLine }}}, function(err, docs) { + Test.find({geom: {$geoIntersects: {$geometry: geojsonLine}}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); assert.equal(created.id, docs[0].id); Test.where('geom').intersects().geometry(geojsonLine).findOne(function(err, doc) { @@ -2337,20 +2449,24 @@ describe('geo-spatial', function() { }); it('MultiPolygon', function(done) { - if (!mongo24_or_greater) return done(); + if (!mongo24_or_greater) { + return done(); + } - var db = start() - , Test = db.model('2dsphere-geo-multi2', geoMultiSchema, 'geospatial' + random()); + var db = start(), + Test = db.model('2dsphere-geo-multi2', geoMultiSchema, 'geospatial' + random()); - Test.create({ geom: [{ type: "Polygon", coordinates: [[ [28.7,41],[29.2,40.9],[29.1,41.3],[28.7,41] ]] }, - { type: "Polygon", coordinates: [[ [-1,-1],[1,-1],[1,1],[-1,1],[-1,-1] ]] }]}, function(err, created) { + Test.create({ + geom: [{type: 'Polygon', coordinates: [[[28.7, 41], [29.2, 40.9], [29.1, 41.3], [28.7, 41]]]}, + {type: 'Polygon', coordinates: [[[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]]}] + }, function(err, created) { assert.ifError(err); - var geojsonPolygon = { type: 'Polygon', coordinates: [[ [26,36],[45,36],[45,42],[26,42],[26,36] ]] }; + var geojsonPolygon = {type: 'Polygon', coordinates: [[[26, 36], [45, 36], [45, 42], [26, 42], [26, 36]]]}; - Test.find({ geom: { $geoIntersects: { $geometry: geojsonPolygon }}}, function(err, docs) { + Test.find({geom: {$geoIntersects: {$geometry: geojsonPolygon}}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); assert.equal(created.id, docs[0].id); Test.where('geom').intersects().geometry(geojsonPolygon).findOne(function(err, doc) { @@ -2365,27 +2481,29 @@ describe('geo-spatial', function() { describe('$near', function() { it('Point', function(done) { - if (!mongo24_or_greater) return done(); + if (!mongo24_or_greater) { + return done(); + } - var db = start() - , Test = db.model('2dsphere-geo', geoSchema, 'geospatial' + random()); + var db = start(), + Test = db.model('2dsphere-geo', geoSchema, 'geospatial' + random()); Test.on('index', function(err) { assert.ifError(err); - Test.create({ line: { type:'Point', coordinates: [-179.0, 0.0] }}, function(err, created) { + Test.create({line: {type: 'Point', coordinates: [-179.0, 0.0]}}, function(err, created) { assert.ifError(err); - var geojsonPoint = { type: 'Point', coordinates: [-179.0, 0.0] }; + var geojsonPoint = {type: 'Point', coordinates: [-179.0, 0.0]}; - Test.find({ line: { $near: geojsonPoint }}, function(err, docs) { + Test.find({line: {$near: geojsonPoint}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); assert.equal(created.id, docs[0].id); - Test.find({ line: { $near: { $geometry: geojsonPoint, $maxDistance: 50 } } }, function(err, docs) { + Test.find({line: {$near: {$geometry: geojsonPoint, $maxDistance: 50}}}, function(err, docs) { assert.ifError(err); - assert.equal(1, docs.length); + assert.equal(docs.length, 1); assert.equal(created.id, docs[0].id); db.close(done); }); @@ -2395,38 +2513,50 @@ describe('geo-spatial', function() { }); it('works with GeoJSON (gh-1482)', function(done) { - if (!mongo24_or_greater) return done(); + if (!mongo24_or_greater) { + return done(); + } - var geoJSONSchema = new Schema({ loc : { type : { type : String }, coordinates : [Number] } }); - geoJSONSchema.index({ loc : '2dsphere' }); + var geoJSONSchema = new Schema({loc: {type: {type: String}, coordinates: [Number]}}); + geoJSONSchema.index({loc: '2dsphere'}); var name = 'geospatial' + random(); - var db = start() - , Test = db.model('Geo1', geoJSONSchema, name); + var db = start(), + Test = db.model('Geo1', geoJSONSchema, name); var pending = 2; + function complete(err) { - if (complete.ran) return; - if (err) return done(complete.ran = err); + if (complete.ran) { + return; + } + if (err) { + return done(complete.ran = err); + } --pending || test(); } Test.on('index', complete); - Test.create({ loc: { type : 'Point', coordinates :[ 10, 20 ]}}, { loc: { - type : 'Point', coordinates: [ 40, 90 ]}}, complete); + Test.create({loc: {type: 'Point', coordinates: [10, 20]}}, { + loc: { + type: 'Point', coordinates: [40, 90] + } + }, complete); function test() { // $maxDistance is in meters... so even though they aren't that far off // in lat/long, need an incredibly high number here - Test.where('loc').near({ center : { type : 'Point', coordinates : - [11,20]}, maxDistance : 1000000 }).exec(function(err, docs) { - db.close(); - assert.ifError(err); - assert.equal(1, docs.length); - done(); - }); + Test.where('loc').near({ + center: { + type: 'Point', coordinates: [11, 20] + }, maxDistance: 1000000 + }).exec(function(err, docs) { + db.close(); + assert.ifError(err); + assert.equal(docs.length, 1); + done(); + }); } }); - }); }); @@ -2435,20 +2565,26 @@ describe('geo-spatial', function() { before(function(done) { start.mongodVersion(function(err, version) { - if (err) return done(err); - mongo24_or_greater = 2 < version[0] || (2 == version[0] && 4 <= version[1]); - if (!mongo24_or_greater) console.log('not testing mongodb 2.4 features'); + if (err) { + return done(err); + } + mongo24_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 4); + if (!mongo24_or_greater) { + console.log('not testing mongodb 2.4 features'); + } done(); }); }); it('work', function(done) { - if (!mongo24_or_greater) return done(); + if (!mongo24_or_greater) { + return done(); + } var db = start(); var schemas = []; - schemas[0] = new Schema({ t: { type: String, index: 'hashed' }}); - schemas[1] = new Schema({ t: { type: String, index: 'hashed', sparse: true }}); - schemas[2] = new Schema({ t: { type: String, index: { type: 'hashed', sparse: true }}}); + schemas[0] = new Schema({t: {type: String, index: 'hashed'}}); + schemas[1] = new Schema({t: {type: String, index: 'hashed', sparse: true}}); + schemas[2] = new Schema({t: {type: String, index: {type: 'hashed', sparse: true}}}); var pending = schemas.length; @@ -2456,15 +2592,15 @@ describe('geo-spatial', function() { var H = db.model('Hashed' + i, schema); H.on('index', function(err) { assert.ifError(err); - H.collection.getIndexes({ full: true }, function(err, indexes) { + H.collection.getIndexes({full: true}, function(err, indexes) { assert.ifError(err); var found = indexes.some(function(index) { - return 'hashed' === index.key.t; + return index.key.t === 'hashed'; }); assert.ok(found); - H.create({ t: 'hashing' }, { }, function(err, doc1, doc2) { + H.create({t: 'hashing'}, {}, function(err, doc1, doc2) { assert.ifError(err); assert.ok(doc1); assert.ok(doc2); @@ -2475,155 +2611,147 @@ describe('geo-spatial', function() { }); function complete() { - if (0 === --pending) { + if (--pending === 0) { db.close(done); } } }); }); -}); -describe('lean option:', function() { - it('find', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , title = 'Wooooot ' + random(); + describe('lean', function() { + it('find', function(done) { + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + title = 'Wooooot ' + random(); - var post = new BlogPostB(); - post.set('title', title); + var post = new BlogPostB(); + post.set('title', title); - post.save(function(err) { - assert.ifError(err); - BlogPostB.find({title : title}).lean().exec(function(err, docs) { + post.save(function(err) { assert.ifError(err); - assert.equal(docs.length, 1); - assert.strictEqual(docs[0] instanceof mongoose.Document, false); - BlogPostB.find({title : title}, null, { lean : true }, function(err, docs) { + BlogPostB.find({title: title}).lean().exec(function(err, docs) { assert.ifError(err); assert.equal(docs.length, 1); assert.strictEqual(docs[0] instanceof mongoose.Document, false); - db.close(); - done(); + BlogPostB.find({title: title}, null, {lean: true}, function(err, docs) { + assert.ifError(err); + assert.equal(docs.length, 1); + assert.strictEqual(docs[0] instanceof mongoose.Document, false); + db.close(); + done(); + }); }); }); }); - }); - it('findOne', function(done) { - var db = start() - , BlogPostB = db.model('BlogPostB', collection) - , title = 'Wooooot ' + random(); + it('findOne', function(done) { + var db = start(), + BlogPostB = db.model('BlogPostB', collection), + title = 'Wooooot ' + random(); - var post = new BlogPostB(); - post.set('title', title); + var post = new BlogPostB(); + post.set('title', title); - post.save(function(err) { - assert.ifError(err); - BlogPostB.findOne({title : title}, null, { lean : true }, function(err, doc) { - db.close(); + post.save(function(err) { assert.ifError(err); - assert.ok(doc); - assert.strictEqual(false, doc instanceof mongoose.Document); - done(); + BlogPostB.findOne({title: title}, null, {lean: true}, function(err, doc) { + db.close(); + assert.ifError(err); + assert.ok(doc); + assert.strictEqual(false, doc instanceof mongoose.Document); + done(); + }); }); }); - }); - it('properly casts nested and/or queries (gh-676)', function(done) { - var sch = new Schema({ - num : Number, - subdoc : { title : String, num : Number } - }); - - var M = mongoose.model('andor' + random(), sch); - - var cond = { - $and : [ - { $or : [ { num : '23' }, { 'subdoc.num' : '45' } ] }, - { $and : [ { 'subdoc.title' : 233 }, { num : '345' } ] } - ] - }; - var q = M.find(cond); - assert.equal('number', typeof q._conditions.$and[0].$or[0].num); - assert.equal('number', typeof q._conditions.$and[0].$or[1]['subdoc.num']); - assert.equal('string', typeof q._conditions.$and[1].$and[0]['subdoc.title']); - assert.equal('number', typeof q._conditions.$and[1].$and[1].num); - done(); - }); - it('properly casts deeply nested and/or queries (gh-676)', function(done) { - var sch = new Schema({ - num : Number, - subdoc : { title : String, num : Number } - }); - - var M = mongoose.model('andor' + random(), sch); - - var cond = { - $and : [ - { $or : [ - { $and : [ - { $or : [ - { num : '12345' }, - { 'subdoc.num' : '56789' } - ] - } - ] - } - ] - } - ] - }; - var q = M.find(cond); - assert.equal('number', typeof q._conditions.$and[0].$or[0].$and[0].$or[0].num); - assert.equal('number', typeof q._conditions.$and[0].$or[0].$and[0].$or[1]['subdoc.num']); - done(); - }); + it('properly casts nested and/or queries (gh-676)', function(done) { + var sch = new Schema({ + num: Number, + subdoc: {title: String, num: Number} + }); - it('casts $elemMatch (gh-2199)', function(done) { - var db = start(); - var schema = new Schema({ dates: [Date] }); - var Dates = db.model('Date', schema, 'dates'); + var M = mongoose.model('andor' + random(), sch); - var array = ['2014-07-01T02:00:00.000Z', '2014-07-01T04:00:00.000Z']; - Dates.create({ dates: array }, function(err) { - assert.ifError(err); - var elemMatch = { $gte: '2014-07-01T03:00:00.000Z' }; - Dates.findOne({}, { dates: { $elemMatch: elemMatch } }, function(err, doc) { - assert.ifError(err); - assert.equal(doc.dates.length, 1); - assert.equal(doc.dates[0].getTime(), - new Date('2014-07-01T04:00:00.000Z').getTime()); - done(); + var cond = { + $and: [ + {$or: [{num: '23'}, {'subdoc.num': '45'}]}, + {$and: [{'subdoc.title': 233}, {num: '345'}]} + ] + }; + var q = M.find(cond); + q._castConditions(); + assert.equal(typeof q._conditions.$and[0].$or[0].num, 'number'); + assert.equal(typeof q._conditions.$and[0].$or[1]['subdoc.num'], 'number'); + assert.equal(typeof q._conditions.$and[1].$and[0]['subdoc.title'], 'string'); + assert.equal(typeof q._conditions.$and[1].$and[1].num, 'number'); + done(); + }); + it('properly casts deeply nested and/or queries (gh-676)', function(done) { + var sch = new Schema({ + num: Number, + subdoc: {title: String, num: Number} }); + + var M = mongoose.model('andor' + random(), sch); + + var cond = { + $and: [{$or: [{$and: [{$or: [{num: '12345'}, {'subdoc.num': '56789'}]}]}]}] + }; + var q = M.find(cond); + q._castConditions(); + assert.equal(typeof q._conditions.$and[0].$or[0].$and[0].$or[0].num, 'number'); + assert.equal(typeof q._conditions.$and[0].$or[0].$and[0].$or[1]['subdoc.num'], 'number'); + done(); }); - }); - describe('$eq', function() { - var mongo26 = false; + it('casts $elemMatch (gh-2199)', function(done) { + var db = start(); + var schema = new Schema({dates: [Date]}); + var Dates = db.model('Date', schema, 'dates'); - before(function(done) { - start.mongodVersion(function(err, version) { - if (err) return done(err); - mongo26 = 2 < version[0] || (2 == version[0] && 6 <= version[1]); - done(); + var array = ['2014-07-01T02:00:00.000Z', '2014-07-01T04:00:00.000Z']; + Dates.create({dates: array}, function(err) { + assert.ifError(err); + var elemMatch = {$gte: '2014-07-01T03:00:00.000Z'}; + Dates.findOne({}, {dates: {$elemMatch: elemMatch}}, function(err, doc) { + assert.ifError(err); + assert.equal(doc.dates.length, 1); + assert.equal(doc.dates[0].getTime(), + new Date('2014-07-01T04:00:00.000Z').getTime()); + db.close(done); + }); }); }); - it('casts $eq (gh-2752)', function(done) { - var db = start(); - var BlogPostB = db.model('BlogPostB', collection); + describe('$eq', function() { + var mongo26 = false; - BlogPostB.findOne( - { _id: { $eq: '000000000000000000000001' }, numbers: { $eq: [1, 2] } }, - function(err, doc) { - if (mongo26) { - assert.ifError(err); - } else { - assert.ok(err.toString().indexOf('MongoError') !== -1); + before(function(done) { + start.mongodVersion(function(err, version) { + if (err) { + return done(err); } - - assert.ok(!doc); - db.close(done); + mongo26 = version[0] > 2 || (version[0] === 2 && version[1] >= 6); + done(); }); + }); + + it('casts $eq (gh-2752)', function(done) { + var db = start(); + var BlogPostB = db.model('BlogPostB', collection); + + BlogPostB.findOne( + {_id: {$eq: '000000000000000000000001'}, numbers: {$eq: [1, 2]}}, + function(err, doc) { + if (mongo26) { + assert.ifError(err); + } else { + assert.ok(err.toString().indexOf('MongoError') !== -1); + } + + assert.ok(!doc); + db.close(done); + }); + }); }); }); }); diff --git a/test/model.stream.test.js b/test/model.stream.test.js index 6daeab357a0..5058a69eb91 100644 --- a/test/model.stream.test.js +++ b/test/model.stream.test.js @@ -3,55 +3,67 @@ * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , utils = require('../lib/utils') - , random = utils.random - , Schema = mongoose.Schema - , fs = require('fs'); +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + utils = require('../lib/utils'), + random = utils.random, + Schema = mongoose.Schema, + fs = require('fs'); var names = ('Aaden Aaron Adrian Aditya Agustin Jim Bob Jonah Frank Sally Lucy').split(' '); -/** - * Setup. - */ +describe('query stream:', function() { + var db; + var Person; + var collection = 'personforstream_' + random(); + var P; -var Person = new Schema({ - name: String -}); + before(function() { + db = start(); -mongoose.model('PersonForStream', Person); -var collection = 'personforstream_' + random(); + Person = new Schema({ + name: String + }); -describe('query stream:', function() { - before(function(done) { - var db = start() - , P = db.model('PersonForStream', collection); + P = db.model('PersonForStream', Person, collection); + }); + before(function(done) { var people = names.map(function(name) { - return { name: name }; + return {name: name}; }); P.create(people, function(err) { assert.ifError(err); - db.close(); done(); }); }); + after(function(done) { + db.close(done); + }); + it('works', function(done) { - var db = start() - , P = db.model('PersonForStream', collection) - , i = 0 - , closed = 0 - , paused = 0 - , resumed = 0 - , seen = {} - , err; + var i = 0; + var closed = 0; + var paused = 0; + var resumed = 0; + var seen = {}; + var err; var stream = P.find().batchSize(3).stream(); + function cb() { + assert.strictEqual(undefined, err); + assert.equal(names.length, i); + assert.equal(closed, 1); + assert.equal(paused, 1); + assert.equal(resumed, 1); + assert.equal(stream._cursor.isClosed(), true); + done(); + } + stream.on('data', function(doc) { assert.strictEqual(true, !!doc.name); assert.strictEqual(true, !!doc._id); @@ -60,7 +72,7 @@ describe('query stream:', function() { assert.ok(!seen[doc.id]); seen[doc.id] = 1; - if (paused > 0 && 0 === resumed) { + if (paused > 0 && resumed === 0) { err = new Error('data emitted during pause'); return cb(); } @@ -68,22 +80,22 @@ describe('query stream:', function() { ++i; if (i === 3) { - assert.equal(false, stream.paused); + assert.equal(stream.paused, false); stream.pause(); - assert.equal(true, stream.paused); + assert.equal(stream.paused, true); paused++; setTimeout(function() { - assert.equal(true, stream.paused); + assert.equal(stream.paused, true); resumed++; stream.resume(); - assert.equal(false, stream.paused); + assert.equal(stream.paused, false); }, 20); } else if (i === 4) { stream.pause(); - assert.equal(true, stream.paused); + assert.equal(stream.paused, true); stream.resume(); - assert.equal(false, stream.paused); + assert.equal(stream.paused, false); } }); @@ -96,134 +108,76 @@ describe('query stream:', function() { closed++; cb(); }); - - function cb() { - db.close(); - assert.strictEqual(undefined, err); - assert.equal(i, names.length); - assert.equal(1, closed); - assert.equal(1, paused); - assert.equal(1, resumed); - assert.equal(true, stream._cursor.isClosed()); - done(); - } }); it('immediately destroying a stream prevents the query from executing', function(done) { - var db = start() - , P = db.model('PersonForStream', collection) - , i = 0; + var i = 0; var stream = P.where('name', 'Jonah').select('name').findOne().stream(); - stream.on('data', function() { - i++; - }); - stream.on('close', cb); - stream.on('error', cb); - - stream.destroy(); - function cb(err) { assert.ifError(err); - assert.equal(0, i); + assert.equal(i, 0); process.nextTick(function() { - db.close(); assert.strictEqual(null, stream._fields); done(); }); } + + stream.on('data', function() { + i++; + }); + stream.on('close', cb); + stream.on('error', cb); + + stream.destroy(); }); it('destroying a stream stops it', function(done) { this.slow(300); - var db = start() - , P = db.model('PersonForStream', collection) - , finished = 0 - , i = 0; + var finished = 0; + var i = 0; var stream = P.where('name').exists().limit(10).select('_id').stream(); assert.strictEqual(null, stream._destroyed); - assert.equal(true, stream.readable); - - stream.on('data', function(doc) { - assert.strictEqual(undefined, doc.name); - if (++i === 5) { - stream.destroy(); - assert.equal(false, stream.readable); - } - }); - - stream.on('close', cb); - stream.on('error', cb); + assert.equal(stream.readable, true); function cb(err) { ++finished; setTimeout(function() { - db.close(); assert.strictEqual(undefined, err); - assert.equal(5, i); - assert.equal(1, finished); - assert.equal(true, stream._destroyed); - assert.equal(false, stream.readable); - assert.equal(true, stream._cursor.isClosed()); + assert.equal(i, 5); + assert.equal(finished, 1); + assert.equal(stream._destroyed, true); + assert.equal(stream.readable, false); + assert.equal(stream._cursor.isClosed(), true); done(); }, 100); } - }); - - it('errors', function(done) { - this.slow(300); - - var db = start({ server: { auto_reconnect: false }}) - , P = db.model('PersonForStream', collection) - , finished = 0 - , closed = 0 - , i = 0; - var stream = P.find().batchSize(5).stream(); - - stream.on('data', function() { + stream.on('data', function(doc) { + assert.strictEqual(undefined, doc.name); if (++i === 5) { - db.close(); + stream.destroy(); + assert.equal(stream.readable, false); } }); - stream.on('close', function() { - closed++; - }); - + stream.on('close', cb); stream.on('error', cb); - - function cb(err) { - ++finished; - setTimeout(function() { - assert.ok(/destroyed/.test(err.message), err.message); - assert.equal(i, 5); - assert.equal(1, closed); - assert.equal(1, finished); - assert.equal(stream._destroyed,true); - assert.equal(stream.readable, false); - assert.equal(stream._cursor.isClosed(), true); - done(); - }, 100); - } }); it('pipe', function(done) { - var db = start() - , P = db.model('PersonForStream', collection) - , filename = '/tmp/_mongoose_stream_out.txt' - , out = fs.createWriteStream(filename); + var filename = '/tmp/_mongoose_stream_out.txt'; + var out = fs.createWriteStream(filename); - var opts = { transform: JSON.stringify }; + var opts = {transform: JSON.stringify}; var stream = P.find().sort('name').limit(20).stream(opts); stream.pipe(out); - var cb = function(err) { - db.close(); + function cb(err) { assert.ifError(err); var contents = fs.readFileSync(filename, 'utf8'); assert.ok(/Aaden/.test(contents)); @@ -233,37 +187,43 @@ describe('query stream:', function() { assert.ok(/Agustin/.test(contents)); fs.unlink(filename); done(); - }; + } stream.on('error', cb); out.on('close', cb); }); it('lean', function(done) { - var db = start() - , P = db.model('PersonForStream', collection) - , i = 0 - , closed = 0 - , err; + var i = 0; + var closed = 0; + var err; var stream = P.find({}).lean().stream(); + function cb() { + assert.strictEqual(undefined, err); + assert.equal(names.length, i); + assert.equal(closed, 1); + assert.equal(stream._cursor.isClosed(), true); + done(); + } + stream.on('data', function(doc) { assert.strictEqual(false, doc instanceof mongoose.Document); i++; - if (1 === i) { + if (i === 1) { stream.pause(); - assert.equal(true, stream.paused); + assert.equal(stream.paused, true); stream.resume(); - assert.equal(false, stream.paused); - } else if (2 === i) { + assert.equal(stream.paused, false); + } else if (i === 2) { stream.pause(); - assert.equal(true, stream.paused); + assert.equal(stream.paused, true); process.nextTick(function() { - assert.equal(true, stream.paused); + assert.equal(stream.paused, true); stream.resume(); - assert.equal(false, stream.paused); + assert.equal(stream.paused, false); }); } }); @@ -277,75 +237,69 @@ describe('query stream:', function() { closed++; cb(); }); - - var cb = function() { - db.close(); - assert.strictEqual(undefined, err); - assert.equal(i, names.length); - assert.equal(1, closed); - assert.equal(true, stream._cursor.isClosed()); - done(); - }; }); it('supports $elemMatch with $in (gh-1091)', function(done) { this.timeout(3000); - var db = start(); - var postSchema = new Schema({ - ids: [{type: Schema.ObjectId}] - , title: String + ids: [{type: Schema.ObjectId}], + title: String }); var B = db.model('gh-1100-stream', postSchema); var _id1 = new mongoose.Types.ObjectId; var _id2 = new mongoose.Types.ObjectId; - B.create({ ids: [_id1, _id2] }, function(err, doc) { + B.create({ids: [_id1, _id2]}, function(err, doc) { assert.ifError(err); var error; - var stream = B.find({ _id: doc._id }) - .select({ title: 1, ids: { $elemMatch: { $in: [_id2.toString()] }}}) + var stream = B.find({_id: doc._id}) + .select({title: 1, ids: {$elemMatch: {$in: [_id2.toString()]}}}) .stream(); stream. on('data', function(found) { assert.equal(found.id, doc.id); - assert.equal(1, found.ids.length); + assert.equal(found.ids.length, 1); assert.equal(_id2.toString(), found.ids[0].toString()); }). on('error', function(err) { error = err; }). on('close', function() { - db.close(); done(error); }); }); }); it('supports population (gh-1411)', function(done) { - var db = start(); - - var barSchema = Schema({ + var barSchema = new Schema({ value: Number }); - var fooSchema = Schema({ - bar: { type: "ObjectId", ref: "Bar" } + var fooSchema = new Schema({ + bar: {type: 'ObjectId', ref: 'Bar'} }); var Foo = db.model('Foo', fooSchema); var Bar = db.model('Bar', barSchema); var found = []; - Bar.create({ value: 2 }, { value: 3 }, function(err, bar1, bar2) { + function complete(err) { + if (!err) { + assert.ok(~found.indexOf(2)); + assert.ok(~found.indexOf(3)); + } + done(); + } + + Bar.create({value: 2}, {value: 3}, function(err, bar1, bar2) { if (err) return complete(err); - Foo.create({ bar: bar1 }, { bar: bar2 }, function(err) { + Foo.create({bar: bar1}, {bar: bar2}, function(err) { if (err) return complete(err); Foo. @@ -359,49 +313,37 @@ describe('query stream:', function() { on('error', complete); }); }); - - var complete = function(err) { - if (!err) { - assert.ok(~found.indexOf(2)); - assert.ok(~found.indexOf(3)); - } - db.close(done); - }; }); it('respects schema options (gh-1862)', function(done) { - var db = start(); - - var schema = Schema({ - fullname: { type: String }, - password: { type: String, select: false } + var schema = new Schema({ + fullname: {type: String}, + password: {type: String, select: false} }); var User = db.model('gh-1862', schema, 'gh-1862'); - User.create({ fullname: 'val', password: 'taco' }, function(error) { + User.create({fullname: 'val', password: 'taco'}, function(error) { assert.ifError(error); User.find().stream().on('data', function(doc) { - assert.equal(undefined, doc.password); - db.close(done); + assert.equal(doc.password, void 0); + done(); }); }); }); it('works with populate + lean (gh-2841)', function(done) { - var db = start(); - var Sku = db.model('Sku', {}, 'gh2841_0'); var Item = db.model('Item', { - sku: { ref: 'Sku', type: Schema.Types.ObjectId } + sku: {ref: 'Sku', type: Schema.Types.ObjectId} }, 'gh2841_1'); Sku.create({}, function(error, sku) { assert.ifError(error); - Item.create({ sku: sku._id }, function(error) { + Item.create({sku: sku._id}, function(error) { assert.ifError(error); var found = 0; - var popOpts = { path: 'sku', options: { lean: true } }; + var popOpts = {path: 'sku', options: {lean: true}}; var stream = Item.find().populate(popOpts).stream(); stream.on('data', function(doc) { ++found; @@ -409,15 +351,13 @@ describe('query stream:', function() { }); stream.on('end', function() { assert.equal(found, 1); - db.close(done); + done(); }); }); }); }); it('works with populate + dynref (gh-3108)', function(done) { - var db = start(); - var reviewSchema = new Schema({ _id: Number, text: String, @@ -431,15 +371,15 @@ describe('query stream:', function() { } }, items: [ - { - id: { - type: Number, - refPath: 'items.type' - }, - type: { - type: String - } + { + id: { + type: Number, + refPath: 'items.type' + }, + type: { + type: String } + } ] }); @@ -460,17 +400,17 @@ describe('query stream:', function() { var c = 0; var create = function(cb) { - Item1.create({ _id: ++c, name: 'Val' }, function(error) { + Item1.create({_id: ++c, name: 'Val'}, function(error) { assert.ifError(error); - Item2.create({ _id: ++c, otherName: 'Val' }, function(error) { + Item2.create({_id: ++c, otherName: 'Val'}, function(error) { assert.ifError(error); var review = { _id: c, text: 'Test', - item: { id: c - 1, type: 'dynrefItem1' }, + item: {id: c - 1, type: 'dynrefItem1'}, items: [ - { id: c - 1, type: 'dynrefItem1' }, - { id: c, type: 'dynrefItem2' } + {id: c - 1, type: 'dynrefItem1'}, + {id: c, type: 'dynrefItem2'} ] }; Review.create(review, function(error) { @@ -487,13 +427,13 @@ describe('query stream:', function() { stream.on('data', function(doc) { ++count; - assert.equal('Val', doc.items[0].id.name); - assert.equal('Val', doc.items[1].id.otherName); + assert.equal(doc.items[0].id.name, 'Val'); + assert.equal(doc.items[1].id.otherName, 'Val'); }); stream.on('close', function() { assert.equal(count, 2); - db.close(done); + done(); }); }; diff --git a/test/model.test.js b/test/model.test.js index 7532d55bece..82b5dbcef70 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -1,76 +1,78 @@ -Error.stackTraceLimit = Infinity; /** * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ValidatorError = mongoose.Error.ValidatorError - , ValidationError = mongoose.Error.ValidationError - , ObjectId = Schema.Types.ObjectId - , DocumentObjectId = mongoose.Types.ObjectId - , EmbeddedDocument = mongoose.Types.Embedded - , MongooseError = mongoose.Error; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema, + ValidatorError = mongoose.Error.ValidatorError, + ValidationError = mongoose.Error.ValidationError, + ObjectId = Schema.Types.ObjectId, + DocumentObjectId = mongoose.Types.ObjectId, + EmbeddedDocument = mongoose.Types.Embedded, + MongooseError = mongoose.Error; -/** - * Setup. - */ - -var Comments = new Schema; +describe('Model', function() { + var db; + var Test; + var Comments; + var BlogPost; + var bpSchema; + var collection; -Comments.add({ - title : String - , date : Date - , body : String - , comments : [Comments] -}); + before(function() { + Comments = new Schema; -var BlogPost = new Schema({ - title : String - , author : String - , slug : String - , date : Date - , meta : { - date : Date - , visitors : Number - } - , published : Boolean - , mixed : {} - , numbers : [Number] - , owners : [ObjectId] - , comments : [Comments] - , nested : { array: [Number] } -}); + Comments.add({ + title: String, + date: Date, + body: String, + comments: [Comments] + }); -BlogPost -.virtual('titleWithAuthor') -.get(function() { - return this.get('title') + ' by ' + this.get('author'); -}) -.set(function(val) { - var split = val.split(' by '); - this.set('title', split[0]); - this.set('author', split[1]); -}); + BlogPost = new Schema({ + title: String, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number + }, + published: Boolean, + mixed: {}, + numbers: [Number], + owners: [ObjectId], + comments: [Comments], + nested: {array: [Number]} + }); -BlogPost.method('cool', function() { - return this; -}); + BlogPost + .virtual('titleWithAuthor') + .get(function() { + return this.get('title') + ' by ' + this.get('author'); + }) + .set(function(val) { + var split = val.split(' by '); + this.set('title', split[0]); + this.set('author', split[1]); + }); -BlogPost.static('woot', function() { - return this; -}); + BlogPost.method('cool', function() { + return this; + }); -mongoose.model('BlogPost', BlogPost); -var bpSchema = BlogPost; + BlogPost.static('woot', function() { + return this; + }); -var collection = 'blogposts_' + random(); + mongoose.model('BlogPost', BlogPost); + bpSchema = BlogPost; -describe('Model', function() { - var db, Test; + collection = 'blogposts_' + random(); + }); before(function() { db = start(); @@ -95,12 +97,12 @@ describe('Model', function() { it('can be created using _id as embedded document', function(done) { var t = new Test({ _id: { - first_name: "Daniel", + first_name: 'Daniel', age: 21 }, - last_name: "Alabi", + last_name: 'Alabi', doc_embed: { - some: "a" + some: 'a' } }); @@ -112,20 +114,18 @@ describe('Model', function() { assert.ok('last_name' in doc); assert.ok('_id' in doc); assert.ok('first_name' in doc._id); - assert.equal("Daniel", doc._id.first_name); + assert.equal(doc._id.first_name, 'Daniel'); assert.ok('age' in doc._id); - assert.equal(21, doc._id.age); + assert.equal(doc._id.age, 21); assert.ok('doc_embed' in doc); assert.ok('some' in doc.doc_embed); - assert.equal("a", doc.doc_embed.some); + assert.equal(doc.doc_embed.some, 'a'); done(); }); }); }); -}); -describe('Model', function() { describe('constructor', function() { it('works without "new" keyword', function(done) { var B = mongoose.model('BlogPost'); @@ -152,12 +152,12 @@ describe('Model', function() { }); describe('isNew', function() { it('is true on instantiation', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); db.close(); var post = new BlogPost; - assert.equal(true, post.isNew); + assert.equal(post.isNew, true); done(); }); @@ -165,19 +165,19 @@ describe('Model', function() { var db = start(); var schema = new Schema({ - name: { type: String, unique: true } - , em: [new Schema({ x: Number })] - }, { collection: 'testisnewonfail_' + random() }); + name: {type: String, unique: true}, + em: [new Schema({x: Number})] + }, {collection: 'testisnewonfail_' + random()}); var A = db.model('isNewOnFail', schema); A.on('index', function() { - var a = new A({ name: 'i am new', em: [{ x: 1 }] }); + var a = new A({name: 'i am new', em: [{x: 1}]}); a.save(function(err) { assert.ifError(err); assert.equal(a.isNew, false); assert.equal(a.em[0].isNew, false); - var b = new A({ name: 'i am new', em: [{x:2}] }); + var b = new A({name: 'i am new', em: [{x: 2}]}); b.save(function(err) { db.close(); assert.ok(err); @@ -207,8 +207,8 @@ describe('Model', function() { describe('schema', function() { it('should exist', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); db.close(); assert.ok(BlogPost.schema instanceof Schema); @@ -216,9 +216,9 @@ describe('Model', function() { done(); }); it('emits init event', function(done) { - var db = start() - , schema = new Schema({ name: String }) - , model; + var db = start(), + schema = new Schema({name: String}), + model; schema.on('init', function(model_) { model = model_; @@ -226,33 +226,33 @@ describe('Model', function() { var Named = db.model('EmitInitOnSchema', schema); db.close(); - assert.equal(model,Named); + assert.equal(model, Named); done(); }); }); describe('structure', function() { it('default when instantiated', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); db.close(); var post = new BlogPost; - assert.equal(post.db.model('BlogPost').modelName,'BlogPost'); - assert.equal(post.constructor.modelName,'BlogPost'); + assert.equal(post.db.model('BlogPost').modelName, 'BlogPost'); + assert.equal(post.constructor.modelName, 'BlogPost'); assert.ok(post.get('_id') instanceof DocumentObjectId); - assert.equal(undefined, post.get('title')); - assert.equal(undefined, post.get('slug')); - assert.equal(undefined, post.get('date')); + assert.equal(post.get('title'), undefined); + assert.equal(post.get('slug'), undefined); + assert.equal(post.get('date'), undefined); - assert.equal('object', typeof post.get('meta')); + assert.equal(typeof post.get('meta'), 'object'); assert.deepEqual(post.get('meta'), {}); - assert.equal(undefined, post.get('meta.date')); - assert.equal(undefined, post.get('meta.visitors')); - assert.equal(undefined, post.get('published')); - assert.equal(1, Object.keys(post.get('nested')).length); + assert.equal(post.get('meta.date'), undefined); + assert.equal(post.get('meta.visitors'), undefined); + assert.equal(post.get('published'), undefined); + assert.equal(Object.keys(post.get('nested')).length, 1); assert.ok(Array.isArray(post.get('nested').array)); assert.ok(post.get('numbers').isMongooseArray); @@ -265,30 +265,30 @@ describe('Model', function() { describe('array', function() { describe('defaults', function() { it('to a non-empty array', function(done) { - var db = start() - , DefaultArraySchema = new Schema({ - arr: {type: Array, cast: String, default: ['a', 'b', 'c']} - , single: {type: Array, cast: String, default: ['a']} - }); + var db = start(), + DefaultArraySchema = new Schema({ + arr: {type: Array, cast: String, default: ['a', 'b', 'c']}, + single: {type: Array, cast: String, default: ['a']} + }); mongoose.model('DefaultArray', DefaultArraySchema); var DefaultArray = db.model('DefaultArray', collection); var arr = new DefaultArray; db.close(); assert.equal(arr.get('arr').length, 3); - assert.equal(arr.get('arr')[0],'a'); - assert.equal(arr.get('arr')[1],'b'); - assert.equal(arr.get('arr')[2],'c'); + assert.equal(arr.get('arr')[0], 'a'); + assert.equal(arr.get('arr')[1], 'b'); + assert.equal(arr.get('arr')[2], 'c'); assert.equal(arr.get('single').length, 1); - assert.equal(arr.get('single')[0],'a'); + assert.equal(arr.get('single')[0], 'a'); done(); }); it('empty', function(done) { - var db = start() - , DefaultZeroCardArraySchema = new Schema({ - arr: {type: Array, cast: String, default: []} - , auto: [Number] - }); + var db = start(), + DefaultZeroCardArraySchema = new Schema({ + arr: {type: Array, cast: String, default: []}, + auto: [Number] + }); mongoose.model('DefaultZeroCardArray', DefaultZeroCardArraySchema); var DefaultZeroCardArray = db.model('DefaultZeroCardArray', collection); db.close(); @@ -302,8 +302,8 @@ describe('Model', function() { }); it('a hash with one null value', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ title: null @@ -314,12 +314,14 @@ describe('Model', function() { }); it('when saved', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , pending = 2; + var db = start(), + BlogPost = db.model('BlogPost', collection), + pending = 2; function cb() { - if (--pending) return; + if (--pending) { + return; + } db.close(); done(); } @@ -328,15 +330,15 @@ describe('Model', function() { post.on('save', function(post) { assert.ok(post.get('_id') instanceof DocumentObjectId); - assert.equal(undefined, post.get('title')); - assert.equal(undefined, post.get('slug')); - assert.equal(undefined, post.get('date')); - assert.equal(undefined, post.get('published')); + assert.equal(post.get('title'), undefined); + assert.equal(post.get('slug'), undefined); + assert.equal(post.get('date'), undefined); + assert.equal(post.get('published'), undefined); assert.equal(typeof post.get('meta'), 'object'); assert.deepEqual(post.get('meta'), {}); - assert.equal(undefined, post.get('meta.date')); - assert.equal(undefined, post.get('meta.visitors')); + assert.equal(post.get('meta.date'), undefined); + assert.equal(post.get('meta.visitors'), undefined); assert.ok(post.get('owners').isMongooseArray); assert.ok(post.get('comments').isMongooseDocumentArray); @@ -347,15 +349,15 @@ describe('Model', function() { assert.ifError(err); assert.ok(post.get('_id') instanceof DocumentObjectId); - assert.equal(undefined, post.get('title')); - assert.equal(undefined, post.get('slug')); - assert.equal(undefined, post.get('date')); - assert.equal(undefined, post.get('published')); + assert.equal(post.get('title'), undefined); + assert.equal(post.get('slug'), undefined); + assert.equal(post.get('date'), undefined); + assert.equal(post.get('published'), undefined); assert.equal(typeof post.get('meta'), 'object'); - assert.deepEqual(post.get('meta'),{}); - assert.equal(undefined, post.get('meta.date')); - assert.equal(undefined, post.get('meta.visitors')); + assert.deepEqual(post.get('meta'), {}); + assert.equal(post.get('meta.date'), undefined); + assert.equal(post.get('meta.visitors'), undefined); assert.ok(post.get('owners').isMongooseArray); assert.ok(post.get('comments').isMongooseDocumentArray); @@ -365,8 +367,8 @@ describe('Model', function() { it('when saved using the promise not the callback', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); var p = post.save(); @@ -374,15 +376,15 @@ describe('Model', function() { assert.ifError(err); assert.ok(post.get('_id') instanceof DocumentObjectId); - assert.equal(undefined, post.get('title')); - assert.equal(undefined, post.get('slug')); - assert.equal(undefined, post.get('date')); - assert.equal(undefined, post.get('published')); + assert.equal(post.get('title'), undefined); + assert.equal(post.get('slug'), undefined); + assert.equal(post.get('date'), undefined); + assert.equal(post.get('published'), undefined); assert.equal(typeof post.get('meta'), 'object'); - assert.deepEqual(post.get('meta'),{}); - assert.equal(undefined, post.get('meta.date')); - assert.equal(undefined, post.get('meta.visitors')); + assert.deepEqual(post.get('meta'), {}); + assert.equal(post.get('meta.date'), undefined); + assert.equal(post.get('meta.visitors'), undefined); assert.ok(post.get('owners').isMongooseArray); assert.ok(post.get('comments').isMongooseDocumentArray); @@ -393,42 +395,42 @@ describe('Model', function() { describe('init', function() { it('works', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); db.close(); post.init({ - title : 'Test' - , slug : 'test' - , date : new Date - , meta : { - date : new Date - , visitors : 5 - } - , published : true - , owners : [new DocumentObjectId, new DocumentObjectId] - , comments : [ - { title: 'Test', date: new Date, body: 'Test' } - , { title: 'Super', date: new Date, body: 'Cool' } + title: 'Test', + slug: 'test', + date: new Date, + meta: { + date: new Date, + visitors: 5 + }, + published: true, + owners: [new DocumentObjectId, new DocumentObjectId], + comments: [ + {title: 'Test', date: new Date, body: 'Test'}, + {title: 'Super', date: new Date, body: 'Cool'} ] }); - assert.equal(post.get('title'),'Test'); - assert.equal(post.get('slug'),'test'); + assert.equal(post.get('title'), 'Test'); + assert.equal(post.get('slug'), 'test'); assert.ok(post.get('date') instanceof Date); - assert.equal('object', typeof post.get('meta')); + assert.equal(typeof post.get('meta'), 'object'); assert.ok(post.get('meta').date instanceof Date); assert.equal(typeof post.get('meta').visitors, 'number'); assert.equal(post.get('published'), true); - assert.equal(post.title,'Test'); - assert.equal(post.slug,'test'); + assert.equal(post.title, 'Test'); + assert.equal(post.slug, 'test'); assert.ok(post.date instanceof Date); - assert.equal(typeof post.meta,'object'); + assert.equal(typeof post.meta, 'object'); assert.ok(post.meta.date instanceof Date); - assert.equal(typeof post.meta.visitors,'number'); + assert.equal(typeof post.meta.visitors, 'number'); assert.equal(post.published, true); assert.ok(post.get('owners').isMongooseArray); @@ -450,26 +452,26 @@ describe('Model', function() { }); it('partially', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); db.close(); var post = new BlogPost; post.init({ - title : 'Test' - , slug : 'test' - , date : new Date + title: 'Test', + slug: 'test', + date: new Date }); - assert.equal(post.get('title'),'Test'); - assert.equal(post.get('slug'),'test'); + assert.equal(post.get('title'), 'Test'); + assert.equal(post.get('slug'), 'test'); assert.ok(post.get('date') instanceof Date); - assert.equal('object', typeof post.get('meta')); + assert.equal(typeof post.get('meta'), 'object'); - assert.deepEqual(post.get('meta'),{}); - assert.equal(undefined, post.get('meta.date')); - assert.equal(undefined, post.get('meta.visitors')); - assert.equal(undefined, post.get('published')); + assert.deepEqual(post.get('meta'), {}); + assert.equal(post.get('meta.date'), undefined); + assert.equal(post.get('meta.visitors'), undefined); + assert.equal(post.get('published'), undefined); assert.ok(post.get('owners').isMongooseArray); assert.ok(post.get('comments').isMongooseDocumentArray); @@ -477,56 +479,56 @@ describe('Model', function() { }); it('with partial hash', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); db.close(); var post = new BlogPost({ meta: { - date : new Date - , visitors : 5 + date: new Date, + visitors: 5 } }); - assert.equal(5, post.get('meta.visitors').valueOf()); + assert.equal(post.get('meta.visitors').valueOf(), 5); done(); }); it('isNew on embedded documents', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); db.close(); var post = new BlogPost(); post.init({ - title : 'Test' - , slug : 'test' - , comments : [ { title: 'Test', date: new Date, body: 'Test' } ] + title: 'Test', + slug: 'test', + comments: [{title: 'Test', date: new Date, body: 'Test'}] }); - assert.equal(false, post.get('comments')[0].isNew); + assert.equal(post.get('comments')[0].isNew, false); done(); }); it('isNew on embedded documents after saving', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); - var post = new BlogPost({ title: 'hocus pocus' }); - post.comments.push({ title: 'Humpty Dumpty', comments: [{title: 'nested'}] }); - assert.equal(true, post.get('comments')[0].isNew); - assert.equal(true, post.get('comments')[0].comments[0].isNew); + var post = new BlogPost({title: 'hocus pocus'}); + post.comments.push({title: 'Humpty Dumpty', comments: [{title: 'nested'}]}); + assert.equal(post.get('comments')[0].isNew, true); + assert.equal(post.get('comments')[0].comments[0].isNew, true); post.invalidate('title'); // force error post.save(function() { - assert.equal(true, post.isNew); - assert.equal(true, post.get('comments')[0].isNew); - assert.equal(true, post.get('comments')[0].comments[0].isNew); + assert.equal(post.isNew, true); + assert.equal(post.get('comments')[0].isNew, true); + assert.equal(post.get('comments')[0].comments[0].isNew, true); post.save(function(err) { db.close(); assert.strictEqual(null, err); - assert.equal(false, post.isNew); - assert.equal(false, post.get('comments')[0].isNew); - assert.equal(false, post.get('comments')[0].comments[0].isNew); + assert.equal(post.isNew, false); + assert.equal(post.get('comments')[0].isNew, false); + assert.equal(post.get('comments')[0].comments[0].isNew, false); done(); }); }); @@ -535,27 +537,27 @@ describe('Model', function() { }); it('collection name can be specified through schema', function(done) { - var schema = new Schema({ name: String }, { collection: 'users1' }); + var schema = new Schema({name: String}, {collection: 'users1'}); var Named = mongoose.model('CollectionNamedInSchema1', schema); - assert.equal(Named.prototype.collection.name,'users1'); + assert.equal(Named.prototype.collection.name, 'users1'); var db = start(); - var users2schema = new Schema({ name: String }, { collection: 'users2' }); + var users2schema = new Schema({name: String}, {collection: 'users2'}); var Named2 = db.model('CollectionNamedInSchema2', users2schema); db.close(); - assert.equal(Named2.prototype.collection.name,'users2'); + assert.equal(Named2.prototype.collection.name, 'users2'); done(); }); it('saving a model with a null value should perpetuate that null value to the db', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ title: null }); assert.strictEqual(null, post.title); - post.save( function(err) { + post.save(function(err) { assert.strictEqual(err, null); BlogPost.findById(post.id, function(err, found) { db.close(); @@ -600,29 +602,27 @@ describe('Model', function() { }); parent.save(function(err, parent) { - assert.equal(parent_hook, "Bob"); - assert.equal(child_hook, "Mary"); + assert.equal(parent_hook, 'Bob'); + assert.equal(child_hook, 'Mary'); assert.ifError(err); parent.children[0].name = 'Jane'; parent.save(function(err) { - assert.equal(child_hook, "Jane"); + assert.equal(child_hook, 'Jane'); assert.ifError(err); done(); }); - }); - }); it('instantiating a model with a hash that maps to at least 1 undefined value', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ title: undefined }); assert.strictEqual(undefined, post.title); - post.save( function(err) { + post.save(function(err) { assert.strictEqual(null, err); BlogPost.findById(post.id, function(err, found) { db.close(); @@ -664,15 +664,15 @@ describe('Model', function() { var db = start(); var MySchema = new Schema({ - _id: { type: Number }, + _id: {type: Number}, name: String }); var MyModel = db.model('MyModel', MySchema, 'numberrangeerror' + random()); var instance = new MyModel({ - name: 'test' - , _id: 35 + name: 'test', + _id: 35 }); instance.save(function(err) { assert.ifError(err); @@ -690,24 +690,24 @@ describe('Model', function() { }); it('over-writing a number should persist to the db (gh-342)', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ meta: { - date : new Date - , visitors : 10 + date: new Date, + visitors: 10 } }); - post.save( function(err) { + post.save(function(err) { assert.ifError(err); post.set('meta.visitors', 20); - post.save( function(err) { + post.save(function(err) { assert.ifError(err); BlogPost.findById(post.id, function(err, found) { assert.ifError(err); - assert.equal(20, found.get('meta.visitors').valueOf()); + assert.equal(found.get('meta.visitors').valueOf(), 20); db.close(); done(); }); @@ -717,18 +717,18 @@ describe('Model', function() { describe('methods', function() { it('can be defined', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); db.close(); var post = new BlogPost(); - assert.equal(post, post.cool()); + assert.equal(post.cool(), post); done(); }); it('can be defined on embedded documents', function(done) { var db = start(); - var ChildSchema = new Schema({ name: String }); + var ChildSchema = new Schema({name: String}); ChildSchema.method('talk', function() { return 'gaga'; }); @@ -742,11 +742,11 @@ describe('Model', function() { db.close(); var c = new ChildA; - assert.equal('function', typeof c.talk); + assert.equal(typeof c.talk, 'function'); var p = new ParentA(); p.children.push({}); - assert.equal('function', typeof p.children[0].talk); + assert.equal(typeof p.children[0].talk, 'function'); done(); }); @@ -760,35 +760,36 @@ describe('Model', function() { }); var NestedKey = db.model('NestedKey', NestedKeySchema); var n = new NestedKey(); - assert.equal(n, n.foo.bar()); + assert.equal(n.foo.bar(), n); done(); }); }); describe('statics', function() { it('can be defined', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); db.close(); - assert.equal(BlogPost, BlogPost.woot()); + assert.equal(BlogPost.woot(), BlogPost); done(); }); }); describe('casting as validation errors', function() { it('error', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , threw = false; + var db = start(), + BlogPost = db.model('BlogPost', collection), + threw = false; + var post; try { - var post = new BlogPost({ date: 'Test', meta: { date: 'Test' } }); + post = new BlogPost({date: 'Test', meta: {date: 'Test'}}); } catch (e) { threw = true; } - assert.equal(false, threw); + assert.equal(threw, false); try { post.set('title', 'Test'); @@ -796,12 +797,12 @@ describe('Model', function() { threw = true; } - assert.equal(false, threw); + assert.equal(threw, false); post.save(function(err) { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); - assert.equal(2, Object.keys(err.errors).length); + assert.equal(Object.keys(err.errors).length, 2); post.date = new Date; post.meta.date = new Date; post.save(function(err) { @@ -812,9 +813,9 @@ describe('Model', function() { }); }); it('nested error', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , threw = false; + var db = start(), + BlogPost = db.model('BlogPost', collection), + threw = false; var post = new BlogPost; @@ -828,7 +829,7 @@ describe('Model', function() { threw = true; } - assert.equal(false, threw); + assert.equal(threw, false); try { post.set('meta.date', 'Test'); @@ -836,7 +837,7 @@ describe('Model', function() { threw = true; } - assert.equal(false, threw); + assert.equal(threw, false); post.save(function(err) { db.close(); @@ -848,13 +849,13 @@ describe('Model', function() { it('subdocument cast error', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ - title : 'Test' - , slug : 'test' - , comments : [ { title: 'Test', date: new Date, body: 'Test' } ] + title: 'Test', + slug: 'test', + comments: [{title: 'Test', date: new Date, body: 'Test'}] }); post.get('comments')[0].set('date', 'invalid'); @@ -873,17 +874,17 @@ describe('Model', function() { return false; } - var db = start() - , subs = new Schema({ - str: { - type: String, validate: failingvalidator - } - }) - , BlogPost = db.model('BlogPost', {subs: [subs]}); + var db = start(), + subs = new Schema({ + str: { + type: String, validate: failingvalidator + } + }), + BlogPost = db.model('BlogPost', {subs: [subs]}); var post = new BlogPost(); post.init({ - subs: [ { str: 'gaga' } ] + subs: [{str: 'gaga'}] }); post.save(function(err) { @@ -895,9 +896,9 @@ describe('Model', function() { it('subdocument error when adding a subdoc', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , threw = false; + var db = start(), + BlogPost = db.model('BlogPost', collection), + threw = false; var post = new BlogPost(); @@ -909,7 +910,7 @@ describe('Model', function() { threw = true; } - assert.equal(false, threw); + assert.equal(threw, false); post.save(function(err) { db.close(); @@ -921,8 +922,8 @@ describe('Model', function() { it('updates', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.set('title', '1'); @@ -932,10 +933,10 @@ describe('Model', function() { post.save(function(err) { assert.ifError(err); - BlogPost.update({ title: 1, _id: id }, { title: 2 }, function(err) { + BlogPost.update({title: 1, _id: id}, {title: 2}, function(err) { assert.ifError(err); - BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) { + BlogPost.findOne({_id: post.get('_id')}, function(err, doc) { db.close(); assert.ifError(err); assert.equal(doc.get('title'), '2'); @@ -946,9 +947,9 @@ describe('Model', function() { }); it('$pull', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , post = new BlogPost(); + var db = start(), + BlogPost = db.model('BlogPost', collection), + post = new BlogPost(); db.close(); post.get('numbers').push('3'); @@ -957,20 +958,20 @@ describe('Model', function() { }); it('$push', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , post = new BlogPost(); + var db = start(), + BlogPost = db.model('BlogPost', collection), + post = new BlogPost(); post.get('numbers').push(1, 2, 3, 4); - post.save( function() { - BlogPost.findById( post.get('_id'), function(err, found) { - assert.equal(found.get('numbers').length,4); + post.save(function() { + BlogPost.findById(post.get('_id'), function(err, found) { + assert.equal(found.get('numbers').length, 4); found.get('numbers').pull('3'); - found.save( function() { + found.save(function() { BlogPost.findById(found.get('_id'), function(err, found2) { db.close(); assert.ifError(err); - assert.equal(found2.get('numbers').length,3); + assert.equal(found2.get('numbers').length, 3); done(); }); }); @@ -979,8 +980,8 @@ describe('Model', function() { }); it('Number arrays', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.numbers.push(1, '2', 3); @@ -1006,29 +1007,29 @@ describe('Model', function() { Date.prototype.toObject = function() { return { - millisecond: 86 - , second: 42 - , minute: 47 - , hour: 17 - , day: 13 - , week: 50 - , month: 11 - , year: 2011 + millisecond: 86, + second: 42, + minute: 47, + hour: 17, + day: 13, + week: 50, + month: 11, + year: 2011 }; }; var S = new Schema({ - name: String - , description: String - , sabreId: String - , data: { - lastPrice: Number - , comm: String - , curr: String - , rateName: String - } - , created: { type: Date, default: Date.now } - , valid: { type: Boolean, default: true } + name: String, + description: String, + sabreId: String, + data: { + lastPrice: Number, + comm: String, + curr: String, + rateName: String + }, + created: {type: Date, default: Date.now}, + valid: {type: Boolean, default: true} }); var M = db.model('gh502', S); @@ -1055,25 +1056,25 @@ describe('Model', function() { describe('validation', function() { it('works', function(done) { function dovalidate() { - assert.equal('correct', this.asyncScope); + assert.equal(this.asyncScope, 'correct'); return true; } function dovalidateAsync(val, callback) { - assert.equal('correct', this.scope); + assert.equal(this.scope, 'correct'); process.nextTick(function() { callback(true); }); } mongoose.model('TestValidation', new Schema({ - simple: { type: String, required: true } - , scope: { type: String, validate: [dovalidate, 'scope failed'], required: true } - , asyncScope: { type: String, validate: [dovalidateAsync, 'async scope failed'], required: true } + simple: {type: String, required: true}, + scope: {type: String, validate: [dovalidate, 'scope failed'], required: true}, + asyncScope: {type: String, validate: [dovalidateAsync, 'async scope failed'], required: true} })); - var db = start() - , TestValidation = db.model('TestValidation'); + var db = start(), + TestValidation = db.model('TestValidation'); var post = new TestValidation(); post.set('simple', ''); @@ -1097,12 +1098,13 @@ describe('Model', function() { function validate(val) { return val === 'abc'; } + mongoose.model('TestValidationMessage', new Schema({ - simple: { type: String, validate: [validate, 'must be abc'] } + simple: {type: String, validate: [validate, 'must be abc']} })); - var db = start() - , TestValidationMessage = db.model('TestValidationMessage'); + var db = start(), + TestValidationMessage = db.model('TestValidationMessage'); var post = new TestValidationMessage(); post.set('simple', ''); @@ -1111,8 +1113,8 @@ describe('Model', function() { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); assert.ok(err.errors.simple instanceof ValidatorError); - assert.equal(err.errors.simple.message,'must be abc'); - assert.equal(post.errors.simple.message,'must be abc'); + assert.equal(err.errors.simple.message, 'must be abc'); + assert.equal(post.errors.simple.message, 'must be abc'); post.set('simple', 'abc'); post.save(function(err) { @@ -1133,22 +1135,22 @@ describe('Model', function() { return value.length < 2; }, 'Name cannot be greater than 1 character for path "{PATH}" with value `{VALUE}`'); var doc = new IntrospectionValidation({name: 'hi'}); - doc.save( function(err) { + doc.save(function(err) { db.close(); assert.equal(err.errors.name.message, 'Name cannot be greater than 1 character for path "name" with value `hi`'); - assert.equal(err.name,"ValidationError"); - assert.equal(err.message,"IntrospectionValidation validation failed"); + assert.equal(err.name, 'ValidationError'); + assert.ok(err.message.indexOf('IntrospectionValidation validation failed') !== -1, err.message); done(); }); }); it('of required undefined values', function(done) { mongoose.model('TestUndefinedValidation', new Schema({ - simple: { type: String, required: true } + simple: {type: String, required: true} })); - var db = start() - , TestUndefinedValidation = db.model('TestUndefinedValidation'); + var db = start(), + TestUndefinedValidation = db.model('TestUndefinedValidation'); var post = new TestUndefinedValidation; @@ -1169,15 +1171,15 @@ describe('Model', function() { var db = start(); var D = db.model('CallbackFiresOnceValidation', new Schema({ - username: { type: String, validate: /^[a-z]{6}$/i } - , email: { type: String, validate: /^[a-z]{6}$/i } - , password: { type: String, validate: /^[a-z]{6}$/i } + username: {type: String, validate: /^[a-z]{6}$/i}, + email: {type: String, validate: /^[a-z]{6}$/i}, + password: {type: String, validate: /^[a-z]{6}$/i} })); var post = new D({ - username: "nope" - , email: "too" - , password: "short" + username: 'nope', + email: 'too', + password: 'short' }); var timesCalled = 0; @@ -1187,34 +1189,34 @@ describe('Model', function() { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); - assert.equal(1, ++timesCalled); + assert.equal(++timesCalled, 1); assert.equal(Object.keys(err.errors).length, 3); assert.ok(err.errors.password instanceof ValidatorError); assert.ok(err.errors.email instanceof ValidatorError); assert.ok(err.errors.username instanceof ValidatorError); - assert.equal(err.errors.password.message,'Validator failed for path `password` with value `short`'); - assert.equal(err.errors.email.message,'Validator failed for path `email` with value `too`'); - assert.equal(err.errors.username.message,'Validator failed for path `username` with value `nope`'); + assert.equal(err.errors.password.message, 'Validator failed for path `password` with value `short`'); + assert.equal(err.errors.email.message, 'Validator failed for path `email` with value `too`'); + assert.equal(err.errors.username.message, 'Validator failed for path `username` with value `nope`'); assert.equal(Object.keys(post.errors).length, 3); assert.ok(post.errors.password instanceof ValidatorError); assert.ok(post.errors.email instanceof ValidatorError); assert.ok(post.errors.username instanceof ValidatorError); - assert.equal(post.errors.password.message,'Validator failed for path `password` with value `short`'); - assert.equal(post.errors.email.message,'Validator failed for path `email` with value `too`'); - assert.equal(post.errors.username.message,'Validator failed for path `username` with value `nope`'); + assert.equal(post.errors.password.message, 'Validator failed for path `password` with value `short`'); + assert.equal(post.errors.email.message, 'Validator failed for path `email` with value `too`'); + assert.equal(post.errors.username.message, 'Validator failed for path `username` with value `nope`'); done(); }); }); it('query result', function(done) { mongoose.model('TestValidationOnResult', new Schema({ - resultv: { type: String, required: true } + resultv: {type: String, required: true} })); - var db = start() - , TestV = db.model('TestValidationOnResult'); + var db = start(), + TestV = db.model('TestValidationOnResult'); var post = new TestV; @@ -1225,9 +1227,9 @@ describe('Model', function() { post.resultv = 'yeah'; post.save(function(err) { assert.ifError(err); - TestV.findOne({ _id: post.id }, function(err, found) { + TestV.findOne({_id: post.id}, function(err, found) { assert.ifError(err); - assert.equal(found.resultv,'yeah'); + assert.equal(found.resultv, 'yeah'); found.save(function(err) { db.close(); assert.ifError(err); @@ -1240,18 +1242,18 @@ describe('Model', function() { it('of required previously existing null values', function(done) { mongoose.model('TestPreviousNullValidation', new Schema({ - previous: { type: String, required: true } - , a: String + previous: {type: String, required: true}, + a: String })); - var db = start() - , TestP = db.model('TestPreviousNullValidation'); + var db = start(), + TestP = db.model('TestPreviousNullValidation'); - TestP.collection.insert({ a: null, previous: null}, {}, function(err, f) { + TestP.collection.insert({a: null, previous: null}, {}, function(err, f) { assert.ifError(err); TestP.findOne({_id: f.ops[0]._id}, function(err, found) { assert.ifError(err); - assert.equal(false, found.isNew); + assert.equal(found.isNew, false); assert.strictEqual(found.get('previous'), null); found.validate(function(err) { @@ -1272,12 +1274,12 @@ describe('Model', function() { it('nested', function(done) { mongoose.model('TestNestedValidation', new Schema({ nested: { - required: { type: String, required: true } + required: {type: String, required: true} } })); - var db = start() - , TestNestedValidation = db.model('TestNestedValidation'); + var db = start(), + TestNestedValidation = db.model('TestNestedValidation'); var post = new TestNestedValidation(); post.set('nested.required', null); @@ -1296,43 +1298,43 @@ describe('Model', function() { }); it('of nested subdocuments', function(done) { - var Subsubdocs = new Schema({ required: { type: String, required: true }}); + var Subsubdocs = new Schema({required: {type: String, required: true}}); var Subdocs = new Schema({ - required: { type: String, required: true } - , subs: [Subsubdocs] + required: {type: String, required: true}, + subs: [Subsubdocs] }); mongoose.model('TestSubdocumentsValidation', new Schema({ items: [Subdocs] })); - var db = start() - , TestSubdocumentsValidation = db.model('TestSubdocumentsValidation'); + var db = start(), + TestSubdocumentsValidation = db.model('TestSubdocumentsValidation'); var post = new TestSubdocumentsValidation(); - post.get('items').push({ required: '', subs: [{required: ''}] }); + post.get('items').push({required: '', subs: [{required: ''}]}); post.save(function(err) { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); assert.ok(err.errors['items.0.subs.0.required'] instanceof ValidatorError); - assert.equal(err.errors['items.0.subs.0.required'].message,'Path `required` is required.'); + assert.equal(err.errors['items.0.subs.0.required'].message, 'Path `required` is required.'); assert.ok(post.errors['items.0.subs.0.required'] instanceof ValidatorError); - assert.equal(post.errors['items.0.subs.0.required'].message,'Path `required` is required.'); + assert.equal(post.errors['items.0.subs.0.required'].message, 'Path `required` is required.'); assert.ok(err.errors['items.0.required']); assert.ok(post.errors['items.0.required']); post.items[0].subs[0].set('required', true); - assert.equal(undefined, post.$__.validationError); + assert.equal(post.$__.validationError, undefined); post.save(function(err) { assert.ok(err); assert.ok(err.errors); assert.ok(err.errors['items.0.required'] instanceof ValidatorError); - assert.equal(err.errors['items.0.required'].message,'Path `required` is required.'); + assert.equal(err.errors['items.0.required'].message, 'Path `required` is required.'); assert.ok(!err.errors['items.0.subs.0.required']); assert.ok(!err.errors['items.0.subs.0.required']); @@ -1360,12 +1362,13 @@ describe('Model', function() { fn(v !== 'test'); }, 5); } + mongoose.model('TestAsyncValidation', new Schema({ - async: { type: String, validate: [validator, 'async validator failed for `{PATH}`'] } + async: {type: String, validate: [validator, 'async validator failed for `{PATH}`']} })); - var db = start() - , TestAsyncValidation = db.model('TestAsyncValidation'); + var db = start(), + TestAsyncValidation = db.model('TestAsyncValidation'); var post = new TestAsyncValidation(); post.set('async', 'test'); @@ -1374,14 +1377,14 @@ describe('Model', function() { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); assert.ok(err.errors.async instanceof ValidatorError); - assert.equal(err.errors.async.message,'async validator failed for `async`'); - assert.equal(true, executed); + assert.equal(err.errors.async.message, 'async validator failed for `async`'); + assert.equal(executed, true); executed = false; post.set('async', 'woot'); post.save(function(err) { db.close(); - assert.equal(true, executed); + assert.equal(executed, true); assert.strictEqual(err, null); done(); }); @@ -1400,12 +1403,12 @@ describe('Model', function() { mongoose.model('TestNestedAsyncValidation', new Schema({ nested: { - async: { type: String, validate: [validator, 'async validator'] } + async: {type: String, validate: [validator, 'async validator']} } })); - var db = start() - , TestNestedAsyncValidation = db.model('TestNestedAsyncValidation'); + var db = start(), + TestNestedAsyncValidation = db.model('TestNestedAsyncValidation'); var post = new TestNestedAsyncValidation(); post.set('nested.async', 'test'); @@ -1450,19 +1453,19 @@ describe('Model', function() { } var Subdocs = new Schema({ - required: { type: String, validate: [validator, 'async in subdocs'] } + required: {type: String, validate: [validator, 'async in subdocs']} }); mongoose.model('TestSubdocumentsAsyncValidation', new Schema({ items: [Subdocs] })); - var db = start() - , Test = db.model('TestSubdocumentsAsyncValidation'); + var db = start(), + Test = db.model('TestSubdocumentsAsyncValidation'); var post = new Test(); - post.get('items').push({ required: '' }); + post.get('items').push({required: ''}); post.save(function(err) { assert.ok(err instanceof MongooseError); @@ -1470,7 +1473,7 @@ describe('Model', function() { assert.ok(executed); executed = false; - post.get('items')[0].set({ required: 'here' }); + post.get('items')[0].set({required: 'here'}); post.save(function(err) { db.close(); assert.ok(executed); @@ -1479,20 +1482,19 @@ describe('Model', function() { }); }); }); - }); it('without saving', function(done) { mongoose.model('TestCallingValidation', new Schema({ - item: { type: String, required: true } + item: {type: String, required: true} })); - var db = start() - , TestCallingValidation = db.model('TestCallingValidation'); + var db = start(), + TestCallingValidation = db.model('TestCallingValidation'); var post = new TestCallingValidation; - assert.equal(true, post.schema.path('item').isRequired); + assert.equal(post.schema.path('item').isRequired, true); assert.strictEqual(post.isNew, true); post.validate(function(err) { @@ -1516,32 +1518,32 @@ describe('Model', function() { } mongoose.model('TestRequiredFalse', new Schema({ - result: { type: String, validate: [validator, 'chump validator'], required: false } + result: {type: String, validate: [validator, 'chump validator'], required: false} })); - var db = start() - , TestV = db.model('TestRequiredFalse'); + var db = start(), + TestV = db.model('TestRequiredFalse'); var post = new TestV; db.close(); - assert.equal(false, post.schema.path('result').isRequired); + assert.equal(post.schema.path('result').isRequired, false); done(); }); describe('middleware', function() { it('works', function(done) { - var db = start() - , ValidationMiddlewareSchema = null - , Post = null - , post = null; + var db = start(), + ValidationMiddlewareSchema = null, + Post = null, + post = null; ValidationMiddlewareSchema = new Schema({ - baz: { type: String } + baz: {type: String} }); ValidationMiddlewareSchema.pre('validate', function(next) { - if (this.get('baz') == 'bad') { + if (this.get('baz') === 'bad') { this.invalidate('baz', 'bad'); } next(); @@ -1556,8 +1558,8 @@ describe('Model', function() { post.save(function(err) { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); - assert.equal(err.errors.baz.kind,'user defined'); - assert.equal(err.errors.baz.path,'baz'); + assert.equal(err.errors.baz.kind, 'user defined'); + assert.equal(err.errors.baz.path, 'baz'); post.set('baz', 'good'); post.save(function(err) { @@ -1569,20 +1571,20 @@ describe('Model', function() { }); it('async', function(done) { - var db = start() - , AsyncValidationMiddlewareSchema = null - , Post = null - , post = null; + var db = start(), + AsyncValidationMiddlewareSchema = null, + Post = null, + post = null; AsyncValidationMiddlewareSchema = new Schema({ - prop: { type: String } + prop: {type: String} }); AsyncValidationMiddlewareSchema.pre('validate', true, function(next, done) { - var self = this; + var _this = this; setTimeout(function() { - if (self.get('prop') == 'bad') { - self.invalidate('prop', 'bad'); + if (_this.get('prop') === 'bad') { + _this.invalidate('prop', 'bad'); } done(); }, 5); @@ -1598,8 +1600,8 @@ describe('Model', function() { post.save(function(err) { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); - assert.equal(err.errors.prop.kind,'user defined'); - assert.equal(err.errors.prop.path,'prop'); + assert.equal(err.errors.prop.kind, 'user defined'); + assert.equal(err.errors.prop.path, 'prop'); post.set('prop', 'good'); post.save(function(err) { @@ -1611,26 +1613,26 @@ describe('Model', function() { }); it('complex', function(done) { - var db = start() - , ComplexValidationMiddlewareSchema = null - , Post = null - , post = null - , abc = function(v) { - return v === 'abc'; - }; + var db = start(), + ComplexValidationMiddlewareSchema = null, + Post = null, + post = null, + abc = function(v) { + return v === 'abc'; + }; ComplexValidationMiddlewareSchema = new Schema({ - baz: { type: String }, - abc: { type: String, validate: [abc, 'must be abc'] }, - test: { type: String, validate: [/test/, 'must also be abc'] }, - required: { type: String, required: true } + baz: {type: String}, + abc: {type: String, validate: [abc, 'must be abc']}, + test: {type: String, validate: [/test/, 'must also be abc']}, + required: {type: String, required: true} }); ComplexValidationMiddlewareSchema.pre('validate', true, function(next, done) { - var self = this; + var _this = this; setTimeout(function() { - if (self.get('baz') == 'bad') { - self.invalidate('baz', 'bad'); + if (_this.get('baz') === 'bad') { + _this.invalidate('baz', 'bad'); } done(); }, 5); @@ -1650,21 +1652,21 @@ describe('Model', function() { post.save(function(err) { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); - assert.equal(4, Object.keys(err.errors).length); + assert.equal(Object.keys(err.errors).length, 4); assert.ok(err.errors.baz instanceof ValidatorError); - assert.equal(err.errors.baz.kind,'user defined'); - assert.equal(err.errors.baz.path,'baz'); + assert.equal(err.errors.baz.kind, 'user defined'); + assert.equal(err.errors.baz.path, 'baz'); assert.ok(err.errors.abc instanceof ValidatorError); - assert.equal(err.errors.abc.kind,'user defined'); - assert.equal(err.errors.abc.message,'must be abc'); - assert.equal(err.errors.abc.path,'abc'); + assert.equal(err.errors.abc.kind, 'user defined'); + assert.equal(err.errors.abc.message, 'must be abc'); + assert.equal(err.errors.abc.path, 'abc'); assert.ok(err.errors.test instanceof ValidatorError); - assert.equal(err.errors.test.message,'must also be abc'); - assert.equal(err.errors.test.kind,'user defined'); - assert.equal(err.errors.test.path,'test'); + assert.equal(err.errors.test.message, 'must also be abc'); + assert.equal(err.errors.test.kind, 'user defined'); + assert.equal(err.errors.test.path, 'test'); assert.ok(err.errors.required instanceof ValidatorError); - assert.equal(err.errors.required.kind,'required'); - assert.equal(err.errors.required.path,'required'); + assert.equal(err.errors.required.kind, 'required'); + assert.equal(err.errors.required.path, 'required'); post.set({ baz: 'good', @@ -1688,11 +1690,11 @@ describe('Model', function() { var now = Date.now(); mongoose.model('TestDefaults', new Schema({ - date: { type: Date, default: now } + date: {type: Date, default: now} })); - var db = start() - , TestDefaults = db.model('TestDefaults'); + var db = start(), + TestDefaults = db.model('TestDefaults'); db.close(); var post = new TestDefaults; @@ -1706,12 +1708,12 @@ describe('Model', function() { mongoose.model('TestNestedDefaults', new Schema({ nested: { - date: { type: Date, default: now } + date: {type: Date, default: now} } })); - var db = start() - , TestDefaults = db.model('TestNestedDefaults'); + var db = start(), + TestDefaults = db.model('TestNestedDefaults'); var post = new TestDefaults(); db.close(); @@ -1724,15 +1726,15 @@ describe('Model', function() { var now = Date.now(); var Items = new Schema({ - date: { type: Date, default: now } + date: {type: Date, default: now} }); mongoose.model('TestSubdocumentsDefaults', new Schema({ items: [Items] })); - var db = start() - , TestSubdocumentsDefaults = db.model('TestSubdocumentsDefaults'); + var db = start(), + TestSubdocumentsDefaults = db.model('TestSubdocumentsDefaults'); db.close(); var post = new TestSubdocumentsDefaults(); @@ -1744,7 +1746,7 @@ describe('Model', function() { it('allows nulls', function(done) { var db = start(); - var T = db.model('NullDefault', new Schema({ name: { type: String, default: null }}), collection); + var T = db.model('NullDefault', new Schema({name: {type: String, default: null}}), collection); var t = new T(); assert.strictEqual(null, t.name); @@ -1763,7 +1765,7 @@ describe('Model', function() { it('do not cause the document to stay dirty after save', function(done) { var db = start(), - Model = db.model('SavingDefault', new Schema({ name: { type: String, default: 'saving' }}), collection), + Model = db.model('SavingDefault', new Schema({name: {type: String, default: 'saving'}}), collection), doc = new Model(); doc.save(function(err, doc, numberAffected) { @@ -1783,35 +1785,35 @@ describe('Model', function() { describe('virtuals', function() { it('getters', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , post = new BlogPost({ - title: 'Letters from Earth' - , author: 'Mark Twain' - }); + var db = start(), + BlogPost = db.model('BlogPost', collection), + post = new BlogPost({ + title: 'Letters from Earth', + author: 'Mark Twain' + }); db.close(); assert.equal(post.get('titleWithAuthor'), 'Letters from Earth by Mark Twain'); - assert.equal(post.titleWithAuthor,'Letters from Earth by Mark Twain'); + assert.equal(post.titleWithAuthor, 'Letters from Earth by Mark Twain'); done(); }); it('set()', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , post = new BlogPost(); + var db = start(), + BlogPost = db.model('BlogPost', collection), + post = new BlogPost(); db.close(); post.set('titleWithAuthor', 'Huckleberry Finn by Mark Twain'); - assert.equal(post.get('title'),'Huckleberry Finn'); - assert.equal(post.get('author'),'Mark Twain'); + assert.equal(post.get('title'), 'Huckleberry Finn'); + assert.equal(post.get('author'), 'Mark Twain'); done(); }); it('should not be saved to the db', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , post = new BlogPost(); + var db = start(), + BlogPost = db.model('BlogPost', collection), + post = new BlogPost(); post.set('titleWithAuthor', 'Huckleberry Finn by Mark Twain'); @@ -1821,8 +1823,8 @@ describe('Model', function() { BlogPost.findById(post.get('_id'), function(err, found) { assert.ifError(err); - assert.equal(found.get('title'),'Huckleberry Finn'); - assert.equal(found.get('author'),'Mark Twain'); + assert.equal(found.get('title'), 'Huckleberry Finn'); + assert.equal(found.get('author'), 'Mark Twain'); assert.ok(!('titleWithAuthor' in found.toObject())); db.close(); done(); @@ -1831,13 +1833,13 @@ describe('Model', function() { }); it('nested', function(done) { - var db = start() - , PersonSchema = new Schema({ - name: { - first: String - , last: String - } - }); + var db = start(), + PersonSchema = new Schema({ + name: { + first: String, + last: String + } + }); PersonSchema .virtual('name.full') @@ -1852,46 +1854,46 @@ describe('Model', function() { mongoose.model('Person', PersonSchema); - var Person = db.model('Person') - , person = new Person({ - name: { - first: 'Michael' - , last: 'Sorrentino' - } - }); + var Person = db.model('Person'), + person = new Person({ + name: { + first: 'Michael', + last: 'Sorrentino' + } + }); db.close(); - assert.equal(person.get('name.full'),'Michael Sorrentino'); + assert.equal(person.get('name.full'), 'Michael Sorrentino'); person.set('name.full', 'The Situation'); - assert.equal(person.get('name.first'),'The'); - assert.equal(person.get('name.last'),'Situation'); + assert.equal(person.get('name.first'), 'The'); + assert.equal(person.get('name.last'), 'Situation'); - assert.equal(person.name.full,'The Situation'); + assert.equal(person.name.full, 'The Situation'); person.name.full = 'Michael Sorrentino'; - assert.equal(person.name.first,'Michael'); - assert.equal(person.name.last,'Sorrentino'); + assert.equal(person.name.first, 'Michael'); + assert.equal(person.name.last, 'Sorrentino'); done(); }); }); describe('.remove()', function() { it('works', function(done) { - var db = start() - , collection = 'blogposts_' + random() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + collection = 'blogposts_' + random(), + BlogPost = db.model('BlogPost', collection); - BlogPost.create({ title: 1 }, { title: 2 }, function(err) { + BlogPost.create({title: 1}, {title: 2}, function(err) { assert.ifError(err); - BlogPost.remove({ title: 1 }, function(err) { + BlogPost.remove({title: 1}, function(err) { assert.ifError(err); BlogPost.find({}, function(err, found) { db.close(); assert.ifError(err); - assert.equal(1, found.length); - assert.equal('2', found[0].title); + assert.equal(found.length, 1); + assert.equal(found[0].title, '2'); done(); }); }); @@ -1899,17 +1901,17 @@ describe('Model', function() { }); it('errors when id deselected (gh-3118)', function(done) { - var db = start() - , collection = 'blogposts_' + random() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + collection = 'blogposts_' + random(), + BlogPost = db.model('BlogPost', collection); - BlogPost.create({ title: 1 }, { title: 2 }, function(err) { + BlogPost.create({title: 1}, {title: 2}, function(err) { assert.ifError(err); - BlogPost.findOne({ title: 1 }, { _id: 0 }, function(error, doc) { + BlogPost.findOne({title: 1}, {_id: 0}, function(error, doc) { assert.ifError(error); doc.remove(function(err) { assert.ok(err); - assert.equal(err.toString(), 'Error: No _id found on document!'); + assert.equal(err.message, 'No _id found on document!'); db.close(done); }); }); @@ -1920,10 +1922,10 @@ describe('Model', function() { var db = start(); var collection = 'blogposts_' + random(); var BlogPost = db.model('BlogPost', collection); - BlogPost.create({ title: 1 }, { title: 2 }, function(err) { + BlogPost.create({title: 1}, {title: 2}, function(err) { assert.ifError(err); - BlogPost.remove({ _id: undefined }, function(err) { + BlogPost.remove({_id: undefined}, function(err) { assert.ifError(err); BlogPost.find({}, function(err, found) { assert.equal(found.length, 2, 'Should not remove any records'); @@ -1934,21 +1936,21 @@ describe('Model', function() { }); it('should not remove all documents in the collection (gh-3326)', function(done) { - var db = start() - , collection = 'blogposts_' + random() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + collection = 'blogposts_' + random(), + BlogPost = db.model('BlogPost', collection); - BlogPost.create({ title: 1 }, { title: 2 }, function(err) { + BlogPost.create({title: 1}, {title: 2}, function(err) { assert.ifError(err); - BlogPost.findOne({ title: 1 }, function(error, doc) { + BlogPost.findOne({title: 1}, function(error, doc) { assert.ifError(error); doc.remove(function(err) { assert.ifError(err); BlogPost.find(function(err, found) { db.close(); assert.ifError(err); - assert.equal(1, found.length); - assert.equal('2', found[0].title); + assert.equal(found.length, 1); + assert.equal(found[0].title, '2'); done(); }); }); @@ -2049,10 +2051,10 @@ describe('Model', function() { describe('when called multiple times', function() { it('always executes the passed callback gh-1210', function(done) { - var db = start() - , collection = 'blogposts_' + random() - , BlogPost = db.model('BlogPost', collection) - , post = new BlogPost(); + var db = start(), + collection = 'blogposts_' + random(), + BlogPost = db.model('BlogPost', collection), + post = new BlogPost(); post.save(function(err) { assert.ifError(err); @@ -2060,11 +2062,15 @@ describe('Model', function() { var pending = 2; post.remove(function() { - if (--pending) return; + if (--pending) { + return; + } done(); }); post.remove(function() { - if (--pending) return; + if (--pending) { + return; + } done(); }); }); @@ -2075,76 +2081,78 @@ describe('Model', function() { describe('getters', function() { it('with same name on embedded docs do not class', function(done) { var Post = new Schema({ - title : String - , author : { name : String } - , subject : { name : String } + title: String, + author: {name: String}, + subject: {name: String} }); mongoose.model('PostWithClashGetters', Post); - var db = start() - , PostModel = db.model('PostWithClashGetters', 'postwithclash' + random()); + var db = start(), + PostModel = db.model('PostWithClashGetters', 'postwithclash' + random()); var post = new PostModel({ - title: 'Test' - , author: { name: 'A' } - , subject: { name: 'B' } + title: 'Test', + author: {name: 'A'}, + subject: {name: 'B'} }); db.close(); - assert.equal(post.author.name,'A'); - assert.equal(post.subject.name,'B'); - assert.equal(post.author.name,'A'); + assert.equal(post.author.name, 'A'); + assert.equal(post.subject.name, 'B'); + assert.equal(post.author.name, 'A'); done(); }); it('should not be triggered at construction (gh-685)', function(done) { - var db = start() - , called = false; + var db = start(), + called = false; db.close(); var schema = new mongoose.Schema({ number: { - type:Number - , set: function(x) { return x / 2;} - , get: function(x) { - called = true; - return x * 2; - } + type: Number, + set: function(x) { + return x / 2; + }, + get: function(x) { + called = true; + return x * 2; + } } }); var A = mongoose.model('gettersShouldNotBeTriggeredAtConstruction', schema); - var a = new A({ number: 100 }); - assert.equal(false, called); + var a = new A({number: 100}); + assert.equal(called, false); var num = a.number; - assert.equal(true, called); - assert.equal(100, num.valueOf()); - assert.equal(50, a.getValue('number').valueOf()); + assert.equal(called, true); + assert.equal(num.valueOf(), 100); + assert.equal(a.getValue('number').valueOf(), 50); called = false; var b = new A; - b.init({ number: 50 }); - assert.equal(false, called); + b.init({number: 50}); + assert.equal(called, false); num = b.number; - assert.equal(true, called); - assert.equal(100, num.valueOf()); - assert.equal(50, b.getValue('number').valueOf()); + assert.equal(called, true); + assert.equal(num.valueOf(), 100); + assert.equal(b.getValue('number').valueOf(), 50); done(); }); it('with type defined with { type: Native } (gh-190)', function(done) { var schema = new Schema({ - date: { type: Date } + date: {type: Date} }); mongoose.model('ShortcutGetterObject', schema); - var db = start() - , ShortcutGetter = db.model('ShortcutGetterObject', 'shortcut' + random()) - , post = new ShortcutGetter(); + var db = start(), + ShortcutGetter = db.model('ShortcutGetterObject', 'shortcut' + random()), + post = new ShortcutGetter(); db.close(); post.set('date', Date.now()); @@ -2161,26 +2169,26 @@ describe('Model', function() { }); mongoose.model('ShortcutGetterNested', schema); - var db = start() - , ShortcutGetterNested = db.model('ShortcutGetterNested', collection) - , doc = new ShortcutGetterNested(); + var db = start(), + ShortcutGetterNested = db.model('ShortcutGetterNested', collection), + doc = new ShortcutGetterNested(); db.close(); - assert.equal('object', typeof doc.first); + assert.equal(typeof doc.first, 'object'); assert.ok(doc.first.second.isMongooseArray); done(); }); it('works with object literals', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); db.close(); var date = new Date; var meta = { - date: date - , visitors: 5 + date: date, + visitors: 5 }; var post = new BlogPost(); @@ -2202,7 +2210,7 @@ describe('Model', function() { threw = true; } - assert.equal(false, threw); + assert.equal(threw, false); getter1 = JSON.parse(getter1); getter2 = JSON.parse(getter2); assert.equal(getter1.visitors, 5); @@ -2214,39 +2222,39 @@ describe('Model', function() { assert.ok(post.get('meta').date instanceof Date); post.meta.visitors = 2; - assert.equal('number', typeof post.get('meta').visitors); - assert.equal('number', typeof post.meta.visitors); + assert.equal(typeof post.get('meta').visitors, 'number'); + assert.equal(typeof post.meta.visitors, 'number'); var newmeta = { - date: date - 2000 - , visitors: 234 + date: date - 2000, + visitors: 234 }; post.set(newmeta, 'meta'); assert.ok(post.meta.date instanceof Date); assert.ok(post.get('meta').date instanceof Date); - assert.equal('number', typeof post.meta.visitors); - assert.equal('number', typeof post.get('meta').visitors); - assert.equal((+post.meta.date),date - 2000); - assert.equal((+post.get('meta').date),date - 2000); - assert.equal((+post.meta.visitors),234); - assert.equal((+post.get('meta').visitors),234); + assert.equal(typeof post.meta.visitors, 'number'); + assert.equal(typeof post.get('meta').visitors, 'number'); + assert.equal((+post.meta.date), date - 2000); + assert.equal((+post.get('meta').date), date - 2000); + assert.equal((+post.meta.visitors), 234); + assert.equal((+post.get('meta').visitors), 234); // set object directly post.meta = { - date: date - 3000 - , visitors: 4815162342 + date: date - 3000, + visitors: 4815162342 }; assert.ok(post.meta.date instanceof Date); assert.ok(post.get('meta').date instanceof Date); - assert.equal('number', typeof post.meta.visitors); - assert.equal('number', typeof post.get('meta').visitors); - assert.equal((+post.meta.date),date - 3000); - assert.equal((+post.get('meta').date),date - 3000); - assert.equal((+post.meta.visitors),4815162342); - assert.equal((+post.get('meta').visitors),4815162342); + assert.equal(typeof post.meta.visitors, 'number'); + assert.equal(typeof post.get('meta').visitors, 'number'); + assert.equal((+post.meta.date), date - 3000); + assert.equal((+post.get('meta').date), date - 3000); + assert.equal((+post.meta.visitors), 4815162342); + assert.equal((+post.get('meta').visitors), 4815162342); done(); }); @@ -2262,12 +2270,12 @@ describe('Model', function() { mongoose.model('NestedStringA', schema); var T = db.model('NestedStringA', collection); - var t = new T({ nest: null }); + var t = new T({nest: null}); assert.strictEqual(t.nest.st, undefined); - t.nest = { st: "jsconf rules" }; - assert.deepEqual(t.nest.toObject(),{ st: "jsconf rules" }); - assert.equal(t.nest.st,"jsconf rules"); + t.nest = {st: 'jsconf rules'}; + assert.deepEqual(t.nest.toObject(), {st: 'jsconf rules'}); + assert.equal(t.nest.st, 'jsconf rules'); t.save(function(err) { db.close(); @@ -2288,12 +2296,12 @@ describe('Model', function() { mongoose.model('NestedStringB', schema); var T = db.model('NestedStringB', collection); - var t = new T({ nest: undefined }); + var t = new T({nest: undefined}); assert.strictEqual(t.nest.st, undefined); - t.nest = { st: "jsconf rules" }; - assert.deepEqual(t.nest.toObject(),{ st: "jsconf rules" }); - assert.equal(t.nest.st,"jsconf rules"); + t.nest = {st: 'jsconf rules'}; + assert.deepEqual(t.nest.toObject(), {st: 'jsconf rules'}); + assert.equal(t.nest.st, 'jsconf rules'); t.save(function(err) { db.close(); @@ -2307,26 +2315,26 @@ describe('Model', function() { var schema = new Schema({ nest: { - st: String - , yep: String + st: String, + yep: String } }); mongoose.model('NestedStringC', schema); var T = db.model('NestedStringC', collection); - var t = new T({ nest: null }); + var t = new T({nest: null}); t.save(function(err) { assert.ifError(err); - t.nest = { st: "jsconf rules", yep: "it does" }; + t.nest = {st: 'jsconf rules', yep: 'it does'}; // check that entire `nest` object is being $set var u = t.$__delta()[1]; assert.ok(u.$set); assert.ok(u.$set.nest); - assert.equal(2, Object.keys(u.$set.nest).length); + assert.equal(Object.keys(u.$set.nest).length, 2); assert.ok(u.$set.nest.yep); assert.ok(u.$set.nest.st); @@ -2335,8 +2343,8 @@ describe('Model', function() { T.findById(t.id, function(err, t) { assert.ifError(err); - assert.equal(t.nest.st,"jsconf rules"); - assert.equal(t.nest.yep,"it does"); + assert.equal(t.nest.st, 'jsconf rules'); + assert.equal(t.nest.yep, 'it does'); t.nest = null; t.save(function(err) { @@ -2359,11 +2367,11 @@ describe('Model', function() { } })); - var DooDad = db.model('MySchema') - , doodad = new DooDad({ nested: { arrays: [] } }) - , date = 1234567890; + var DooDad = db.model('MySchema'), + doodad = new DooDad({nested: {arrays: []}}), + date = 1234567890; - doodad.nested.arrays.push(["+10", "yup", date]); + doodad.nested.arrays.push(['+10', 'yup', date]); doodad.save(function(err) { assert.ifError(err); @@ -2371,9 +2379,9 @@ describe('Model', function() { DooDad.findById(doodad._id, function(err, doodad) { assert.ifError(err); - assert.deepEqual(doodad.nested.arrays.toObject(), [['+10','yup',date]]); + assert.deepEqual(doodad.nested.arrays.toObject(), [['+10', 'yup', date]]); - doodad.nested.arrays.push(["another", 1]); + doodad.nested.arrays.push(['another', 1]); doodad.save(function(err) { assert.ifError(err); @@ -2381,7 +2389,7 @@ describe('Model', function() { DooDad.findById(doodad._id, function(err, doodad) { db.close(); assert.ifError(err); - assert.deepEqual(doodad.nested.arrays.toObject(), [['+10','yup',date], ["another", 1]]); + assert.deepEqual(doodad.nested.arrays.toObject(), [['+10', 'yup', date], ['another', 1]]); done(); }); }); @@ -2393,20 +2401,20 @@ describe('Model', function() { var db = start(); function def() { - return [{ x: 1 }, { x: 2 }, { x:3 }]; + return [{x: 1}, {x: 2}, {x: 3}]; } mongoose.model('MySchema2', new Schema({ nested: { - type: { type: String, default: 'yep' } - , array: { + type: {type: String, default: 'yep'}, + array: { type: Array, default: def } } })); - var DooDad = db.model('MySchema2', collection) - , doodad = new DooDad(); + var DooDad = db.model('MySchema2', collection), + doodad = new DooDad(); doodad.save(function(err) { assert.ifError(err); @@ -2414,11 +2422,11 @@ describe('Model', function() { DooDad.findById(doodad._id, function(err, doodad) { assert.ifError(err); - assert.equal(doodad.nested.type,"yep"); - assert.deepEqual(doodad.nested.array.toObject(), [{x:1},{x:2},{x:3}]); + assert.equal(doodad.nested.type, 'yep'); + assert.deepEqual(doodad.nested.array.toObject(), [{x: 1}, {x: 2}, {x: 3}]); - doodad.nested.type = "nope"; - doodad.nested.array = ["some", "new", "stuff"]; + doodad.nested.type = 'nope'; + doodad.nested.array = ['some', 'new', 'stuff']; doodad.save(function(err) { assert.ifError(err); @@ -2426,15 +2434,14 @@ describe('Model', function() { DooDad.findById(doodad._id, function(err, doodad) { db.close(); assert.ifError(err); - assert.equal(doodad.nested.type,"nope"); - assert.deepEqual(doodad.nested.array.toObject(), ["some", "new", "stuff"]); + assert.equal(doodad.nested.type, 'nope'); + assert.deepEqual(doodad.nested.array.toObject(), ['some', 'new', 'stuff']); done(); }); }); }); }); }); - }); }); @@ -2443,43 +2450,44 @@ describe('Model', function() { var db = start(); function setLat(val) { - return parseInt(val); + return parseInt(val, 10); } var tick = 0; + function uptick() { return ++tick; } var Location = new Schema({ - lat: { type: Number, default: 0, set: setLat} - , long: { type: Number, set: uptick } + lat: {type: Number, default: 0, set: setLat}, + long: {type: Number, set: uptick} }); var Deal = new Schema({ - title: String - , locations: [Location] + title: String, + locations: [Location] }); Location = db.model('Location', Location, 'locations_' + random()); Deal = db.model('Deal', Deal, 'deals_' + random()); var location = new Location({lat: 1.2, long: 10}); - assert.equal(location.lat.valueOf(),1); - assert.equal(location.long.valueOf(),1); + assert.equal(location.lat.valueOf(), 1); + assert.equal(location.long.valueOf(), 1); - var deal = new Deal({title: "My deal", locations: [{lat: 1.2, long: 33}]}); - assert.equal(deal.locations[0].lat.valueOf(),1); - assert.equal(deal.locations[0].long.valueOf(),2); + var deal = new Deal({title: 'My deal', locations: [{lat: 1.2, long: 33}]}); + assert.equal(deal.locations[0].lat.valueOf(), 1); + assert.equal(deal.locations[0].long.valueOf(), 2); deal.save(function(err) { assert.ifError(err); Deal.findById(deal._id, function(err, deal) { db.close(); assert.ifError(err); - assert.equal(deal.locations[0].lat.valueOf(),1); + assert.equal(deal.locations[0].lat.valueOf(), 1); // GH-422 - assert.equal(deal.locations[0].long.valueOf(),2); + assert.equal(deal.locations[0].long.valueOf(), 2); done(); }); }); @@ -2487,8 +2495,8 @@ describe('Model', function() { }); it('changing a number non-atomically (gh-203)', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); @@ -2508,7 +2516,7 @@ describe('Model', function() { BlogPost.findById(post._id, function(err, doc) { db.close(); assert.ifError(err); - assert.equal(3, +doc.meta.visitors); + assert.equal(+doc.meta.visitors, 3); done(); }); }); @@ -2518,100 +2526,100 @@ describe('Model', function() { describe('atomic subdocument', function() { it('saving', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , totalDocs = 4 - , saveQueue = []; + var db = start(), + BlogPost = db.model('BlogPost', collection), + totalDocs = 4, + saveQueue = []; var post = new BlogPost; - post.save(function(err) { - assert.ifError(err); + function complete() { + BlogPost.findOne({_id: post.get('_id')}, function(err, doc) { + db.close(); - BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) { assert.ifError(err); - doc.get('comments').push({ title: '1' }); - save(doc); - }); + assert.equal(doc.get('comments').length, 5); - BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) { - assert.ifError(err); - doc.get('comments').push({ title: '2' }); - save(doc); - }); + var v = doc.get('comments').some(function(comment) { + return comment.get('title') === '1'; + }); - BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) { - assert.ifError(err); - doc.get('comments').push({ title: '3' }); - save(doc); - }); + assert.ok(v); - BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) { - assert.ifError(err); - doc.get('comments').push({ title: '4' }, { title: '5' }); - save(doc); - }); + v = doc.get('comments').some(function(comment) { + return comment.get('title') === '2'; + }); - function save(doc) { - saveQueue.push(doc); - if (saveQueue.length == 4) { - saveQueue.forEach(function(doc) { - doc.save(function(err) { - assert.ifError(err); - --totalDocs || complete(); - }); - }); - } - } + assert.ok(v); - function complete() { - BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) { - db.close(); + v = doc.get('comments').some(function(comment) { + return comment.get('title') === '3'; + }); - assert.ifError(err); - assert.equal(doc.get('comments').length,5); + assert.ok(v); - var v = doc.get('comments').some(function(comment) { - return comment.get('title') == '1'; - }); + v = doc.get('comments').some(function(comment) { + return comment.get('title') === '4'; + }); - assert.ok(v); + assert.ok(v); - v = doc.get('comments').some(function(comment) { - return comment.get('title') == '2'; - }); + v = doc.get('comments').some(function(comment) { + return comment.get('title') === '5'; + }); - assert.ok(v); + assert.ok(v); + done(); + }); + } - v = doc.get('comments').some(function(comment) { - return comment.get('title') == '3'; + function save(doc) { + saveQueue.push(doc); + if (saveQueue.length === 4) { + saveQueue.forEach(function(doc) { + doc.save(function(err) { + assert.ifError(err); + --totalDocs || complete(); }); + }); + } + } - assert.ok(v); + post.save(function(err) { + assert.ifError(err); - v = doc.get('comments').some(function(comment) { - return comment.get('title') == '4'; - }); + BlogPost.findOne({_id: post.get('_id')}, function(err, doc) { + assert.ifError(err); + doc.get('comments').push({title: '1'}); + save(doc); + }); - assert.ok(v); + BlogPost.findOne({_id: post.get('_id')}, function(err, doc) { + assert.ifError(err); + doc.get('comments').push({title: '2'}); + save(doc); + }); - v = doc.get('comments').some(function(comment) { - return comment.get('title') == '5'; - }); + BlogPost.findOne({_id: post.get('_id')}, function(err, doc) { + assert.ifError(err); + doc.get('comments').push({title: '3'}); + save(doc); + }); - assert.ok(v); - done(); - }); - } + BlogPost.findOne({_id: post.get('_id')}, function(err, doc) { + assert.ifError(err); + doc.get('comments').push({title: '4'}, {title: '5'}); + save(doc); + }); }); }); it('setting (gh-310)', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); BlogPost.create({ - comments: [{ title: 'first-title', body: 'first-body'}] + comments: [{title: 'first-title', body: 'first-body'}] }, function(err, blog) { assert.ifError(err); BlogPost.findById(blog.id, function(err, agent1blog) { @@ -2619,17 +2627,17 @@ describe('Model', function() { BlogPost.findById(blog.id, function(err, agent2blog) { assert.ifError(err); agent1blog.get('comments')[0].title = 'second-title'; - agent1blog.save( function(err) { + agent1blog.save(function(err) { assert.ifError(err); agent2blog.get('comments')[0].body = 'second-body'; - agent2blog.save( function(err) { + agent2blog.save(function(err) { assert.ifError(err); BlogPost.findById(blog.id, function(err, foundBlog) { assert.ifError(err); db.close(); var comment = foundBlog.get('comments')[0]; - assert.equal(comment.title,'second-title'); - assert.equal(comment.body,'second-body'); + assert.equal(comment.title, 'second-title'); + assert.equal(comment.body, 'second-body'); done(); }); }); @@ -2661,7 +2669,7 @@ describe('Model', function() { Outer.findById(outer.get('_id'), function(err, found) { assert.ifError(err); - assert.equal(1, found.inner.length); + assert.equal(found.inner.length, 1); found.inner[0].arr.push(5); found.save(function(err) { assert.ifError(err); @@ -2669,9 +2677,9 @@ describe('Model', function() { Outer.findById(found.get('_id'), function(err, found2) { db.close(); assert.ifError(err); - assert.equal(1, found2.inner.length); - assert.equal(1, found2.inner[0].arr.length); - assert.equal(5, found2.inner[0].arr[0]); + assert.equal(found2.inner.length, 1); + assert.equal(found2.inner[0].arr.length, 1); + assert.equal(found2.inner[0].arr[0], 5); done(); }); }); @@ -2680,12 +2688,12 @@ describe('Model', function() { }); it('updating multiple Number $pushes as a single $pushAll', function(done) { - var db = start() - , schema = new Schema({ - nested: { - nums: [Number] - } - }); + var db = start(), + schema = new Schema({ + nested: { + nums: [Number] + } + }); mongoose.model('NestedPushes', schema); var Temp = db.model('NestedPushes', collection); @@ -2695,15 +2703,15 @@ describe('Model', function() { t.nested.nums.push(1); t.nested.nums.push(2); - assert.equal(t.nested.nums.length,2); + assert.equal(t.nested.nums.length, 2); t.save(function(err) { assert.ifError(err); - assert.equal(t.nested.nums.length,2); + assert.equal(t.nested.nums.length, 2); Temp.findById(t._id, function(err) { db.close(); assert.ifError(err); - assert.equal(t.nested.nums.length,2); + assert.equal(t.nested.nums.length, 2); done(); }); }); @@ -2711,12 +2719,12 @@ describe('Model', function() { }); it('updating at least a single $push and $pushAll as a single $pushAll', function(done) { - var db = start() - , schema = new Schema({ - nested: { - nums: [Number] - } - }); + var db = start(), + schema = new Schema({ + nested: { + nums: [Number] + } + }); var Temp = db.model('NestedPushes', schema, collection); @@ -2724,7 +2732,7 @@ describe('Model', function() { assert.ifError(err); t.nested.nums.push(1); t.nested.nums.push(2, 3); - assert.equal(3, t.nested.nums.length); + assert.equal(t.nested.nums.length, 3); t.save(function(err) { assert.ifError(err); @@ -2740,12 +2748,12 @@ describe('Model', function() { }); it('activePaths should be updated for nested modifieds', function(done) { - var db = start() - , schema = new Schema({ - nested: { - nums: [Number] - } - }); + var db = start(), + schema = new Schema({ + nested: { + nums: [Number] + } + }); var Temp = db.model('NestedPushes', schema, collection); @@ -2753,7 +2761,7 @@ describe('Model', function() { assert.ifError(err); t.nested.nums.pull(1); t.nested.nums.pull(2); - assert.equal(t.$__.activePaths.paths['nested.nums'],'modify'); + assert.equal(t.$__.activePaths.paths['nested.nums'], 'modify'); db.close(); done(); }); @@ -2761,12 +2769,12 @@ describe('Model', function() { it('activePaths should be updated for nested modifieds as promise', function(done) { - var db = start() - , schema = new Schema({ - nested: { - nums: [Number] - } - }); + var db = start(), + schema = new Schema({ + nested: { + nums: [Number] + } + }); var Temp = db.model('NestedPushes', schema, collection); @@ -2775,45 +2783,43 @@ describe('Model', function() { assert.ifError(err); t.nested.nums.pull(1); t.nested.nums.pull(2); - assert.equal(t.$__.activePaths.paths['nested.nums'],'modify'); + assert.equal(t.$__.activePaths.paths['nested.nums'], 'modify'); db.close(); done(); }); }); - - it('$pull should affect what you see in an array before a save', function(done) { - var db = start() - , schema = new Schema({ - nested: { - nums: [Number] - } - }); + var db = start(), + schema = new Schema({ + nested: { + nums: [Number] + } + }); var Temp = db.model('NestedPushes', schema, collection); Temp.create({nested: {nums: [1, 2, 3, 4, 5]}}, function(err, t) { assert.ifError(err); t.nested.nums.pull(1); - assert.equal(4, t.nested.nums.length); + assert.equal(t.nested.nums.length, 4); db.close(); done(); }); }); it('$shift', function(done) { - var db = start() - , schema = new Schema({ - nested: { - nums: [Number] - } - }); + var db = start(), + schema = new Schema({ + nested: { + nums: [Number] + } + }); mongoose.model('TestingShift', schema); var Temp = db.model('TestingShift', collection); - Temp.create({ nested: { nums: [1,2,3] }}, function(err, t) { + Temp.create({nested: {nums: [1, 2, 3]}}, function(err, t) { assert.ifError(err); Temp.findById(t._id, function(err, found) { @@ -2821,27 +2827,27 @@ describe('Model', function() { assert.equal(found.nested.nums.length, 3); found.nested.nums.$pop(); assert.equal(found.nested.nums.length, 2); - assert.equal(found.nested.nums[0],1); - assert.equal(found.nested.nums[1],2); + assert.equal(found.nested.nums[0], 1); + assert.equal(found.nested.nums[1], 2); found.save(function(err) { assert.ifError(err); Temp.findById(t._id, function(err, found) { assert.ifError(err); - assert.equal(2, found.nested.nums.length); - assert.equal(1, found.nested.nums[0],1); - assert.equal(2, found.nested.nums[1],2); + assert.equal(found.nested.nums.length, 2); + assert.equal(found.nested.nums[0], 1, 1); + assert.equal(found.nested.nums[1], 2, 2); found.nested.nums.$shift(); - assert.equal(1, found.nested.nums.length); - assert.equal(found.nested.nums[0],2); + assert.equal(found.nested.nums.length, 1); + assert.equal(found.nested.nums[0], 2); found.save(function(err) { assert.ifError(err); Temp.findById(t._id, function(err, found) { db.close(); assert.ifError(err); - assert.equal(found.nested.nums.length,1); - assert.equal(found.nested.nums[0],2); + assert.equal(found.nested.nums.length, 1); + assert.equal(found.nested.nums[0], 2); done(); }); }); @@ -2853,210 +2859,209 @@ describe('Model', function() { describe('saving embedded arrays', function() { it('of Numbers atomically', function(done) { - var db = start() - , TempSchema = new Schema({ - nums: [Number] - }) - , totalDocs = 2 - , saveQueue = []; + var db = start(), + TempSchema = new Schema({ + nums: [Number] + }), + totalDocs = 2, + saveQueue = []; mongoose.model('Temp', TempSchema); var Temp = db.model('Temp', collection); var t = new Temp(); + function complete() { + Temp.findOne({_id: t.get('_id')}, function(err, doc) { + assert.ifError(err); + assert.equal(doc.get('nums').length, 3); + + var v = doc.get('nums').some(function(num) { + return num.valueOf() === 1; + }); + assert.ok(v); + + v = doc.get('nums').some(function(num) { + return num.valueOf() === 2; + }); + assert.ok(v); + + v = doc.get('nums').some(function(num) { + return num.valueOf() === 3; + }); + assert.ok(v); + db.close(done); + }); + } + + function save(doc) { + saveQueue.push(doc); + if (saveQueue.length === totalDocs) { + saveQueue.forEach(function(doc) { + doc.save(function(err) { + assert.ifError(err); + --totalDocs || complete(); + }); + }); + } + } + t.save(function(err) { assert.ifError(err); - Temp.findOne({ _id: t.get('_id') }, function(err, doc) { + Temp.findOne({_id: t.get('_id')}, function(err, doc) { assert.ifError(err); doc.get('nums').push(1); save(doc); }); - Temp.findOne({ _id: t.get('_id') }, function(err, doc) { + Temp.findOne({_id: t.get('_id')}, function(err, doc) { assert.ifError(err); doc.get('nums').push(2, 3); save(doc); }); + }); + }); - function save(doc) { - saveQueue.push(doc); - if (saveQueue.length == totalDocs) { - saveQueue.forEach(function(doc) { - doc.save(function(err) { - assert.ifError(err); - --totalDocs || complete(); - }); - }); - } - } + it('of Strings atomically', function(done) { + var db = start(), + StrListSchema = new Schema({ + strings: [String] + }), + totalDocs = 2, + saveQueue = []; - function complete() { - Temp.findOne({ _id: t.get('_id') }, function(err, doc) { - assert.ifError(err); - assert.equal(3, doc.get('nums').length); + mongoose.model('StrList', StrListSchema); + var StrList = db.model('StrList'); - var v = doc.get('nums').some(function(num) { - return num.valueOf() == '1'; - }); - assert.ok(v); + var t = new StrList(); - v = doc.get('nums').some(function(num) { - return num.valueOf() == '2'; - }); - assert.ok(v); + function complete() { + StrList.findOne({_id: t.get('_id')}, function(err, doc) { + db.close(); + assert.ifError(err); - v = doc.get('nums').some(function(num) { - return num.valueOf() == '3'; - }); - assert.ok(v); - db.close(done); + assert.equal(doc.get('strings').length, 3); + + var v = doc.get('strings').some(function(str) { + return str === 'a'; }); - } - }); - }); + assert.ok(v); - it('of Strings atomically', function(done) { - var db = start() - , StrListSchema = new Schema({ - strings: [String] - }) - , totalDocs = 2 - , saveQueue = []; + v = doc.get('strings').some(function(str) { + return str === 'b'; + }); + assert.ok(v); - mongoose.model('StrList', StrListSchema); - var StrList = db.model('StrList'); + v = doc.get('strings').some(function(str) { + return str === 'c'; + }); + assert.ok(v); + done(); + }); + } - var t = new StrList(); + function save(doc) { + saveQueue.push(doc); + if (saveQueue.length === totalDocs) { + saveQueue.forEach(function(doc) { + doc.save(function(err) { + assert.ifError(err); + --totalDocs || complete(); + }); + }); + } + } t.save(function(err) { assert.ifError(err); - StrList.findOne({ _id: t.get('_id') }, function(err, doc) { + StrList.findOne({_id: t.get('_id')}, function(err, doc) { assert.ifError(err); doc.get('strings').push('a'); save(doc); }); - StrList.findOne({ _id: t.get('_id') }, function(err, doc) { + StrList.findOne({_id: t.get('_id')}, function(err, doc) { assert.ifError(err); doc.get('strings').push('b', 'c'); save(doc); }); + }); + }); + it('of Buffers atomically', function(done) { + var db = start(), + BufListSchema = new Schema({ + buffers: [Buffer] + }), + totalDocs = 2, + saveQueue = []; - function save(doc) { - saveQueue.push(doc); - if (saveQueue.length == totalDocs) { - saveQueue.forEach(function(doc) { - doc.save(function(err) { - assert.ifError(err); - --totalDocs || complete(); - }); - }); - } - } + mongoose.model('BufList', BufListSchema); + var BufList = db.model('BufList'); - function complete() { - StrList.findOne({ _id: t.get('_id') }, function(err, doc) { - db.close(); - assert.ifError(err); + var t = new BufList(); - assert.equal(3, doc.get('strings').length); + function complete() { + BufList.findOne({_id: t.get('_id')}, function(err, doc) { + db.close(); + assert.ifError(err); - var v = doc.get('strings').some(function(str) { - return str == 'a'; - }); - assert.ok(v); + assert.equal(doc.get('buffers').length, 3); - v = doc.get('strings').some(function(str) { - return str == 'b'; - }); - assert.ok(v); + var v = doc.get('buffers').some(function(buf) { + return buf[0] === 140; + }); + assert.ok(v); - v = doc.get('strings').some(function(str) { - return str == 'c'; - }); - assert.ok(v); - done(); + v = doc.get('buffers').some(function(buf) { + return buf[0] === 141; }); - } - }); - }); + assert.ok(v); - it('of Buffers atomically', function(done) { - var db = start() - , BufListSchema = new Schema({ - buffers: [Buffer] - }) - , totalDocs = 2 - , saveQueue = []; + v = doc.get('buffers').some(function(buf) { + return buf[0] === 142; + }); + assert.ok(v); - mongoose.model('BufList', BufListSchema); - var BufList = db.model('BufList'); + done(); + }); + } - var t = new BufList(); + function save(doc) { + saveQueue.push(doc); + if (saveQueue.length === totalDocs) { + saveQueue.forEach(function(doc) { + doc.save(function(err) { + assert.ifError(err); + --totalDocs || complete(); + }); + }); + } + } t.save(function(err) { assert.ifError(err); - BufList.findOne({ _id: t.get('_id') }, function(err, doc) { + BufList.findOne({_id: t.get('_id')}, function(err, doc) { assert.ifError(err); doc.get('buffers').push(new Buffer([140])); save(doc); }); - BufList.findOne({ _id: t.get('_id') }, function(err, doc) { + BufList.findOne({_id: t.get('_id')}, function(err, doc) { assert.ifError(err); doc.get('buffers').push(new Buffer([141]), new Buffer([142])); save(doc); }); - - function save(doc) { - saveQueue.push(doc); - if (saveQueue.length == totalDocs) { - saveQueue.forEach(function(doc) { - doc.save(function(err) { - assert.ifError(err); - --totalDocs || complete(); - }); - }); - } - } - - function complete() { - BufList.findOne({ _id: t.get('_id') }, function(err, doc) { - db.close(); - assert.ifError(err); - - assert.equal(3, doc.get('buffers').length); - - var v = doc.get('buffers').some(function(buf) { - return buf[0] == 140; - }); - assert.ok(v); - - v = doc.get('buffers').some(function(buf) { - return buf[0] == 141; - }); - assert.ok(v); - - v = doc.get('buffers').some(function(buf) { - return buf[0] == 142; - }); - assert.ok(v); - - done(); - }); - } }); }); it('works with modified element properties + doc removal (gh-975)', function(done) { - var db = start() - , B = db.model('BlogPost', collection) - , b = new B({ comments: [{ title: 'gh-975' }] }); + var db = start(), + B = db.model('BlogPost', collection), + b = new B({comments: [{title: 'gh-975'}]}); b.save(function(err) { assert.ifError(err); @@ -3069,7 +3074,7 @@ describe('Model', function() { b.save(function(err) { assert.ifError(err); - B.findByIdAndUpdate({ _id: b._id }, { $set: { comments: [{ title: 'a' }] }}, { 'new': true }, function(err, doc) { + B.findByIdAndUpdate({_id: b._id}, {$set: {comments: [{title: 'a'}]}}, {new: true}, function(err, doc) { assert.ifError(err); doc.comments[0].title = 'differ'; doc.comments[0].remove(); @@ -3078,7 +3083,7 @@ describe('Model', function() { B.findById(doc._id, function(err, doc) { db.close(); assert.ifError(err); - assert.equal(0, doc.comments.length); + assert.equal(doc.comments.length, 0); done(); }); }); @@ -3100,7 +3105,7 @@ describe('Model', function() { assert.ifError(err); BlogPost.findById(post._id, function(err, found) { assert.ifError(err); - assert.equal('before-change', found.comments[0].title); + assert.equal(found.comments[0].title, 'before-change'); var subDoc = [{ _id: found.comments[0]._id, title: 'after-change' @@ -3112,7 +3117,7 @@ describe('Model', function() { BlogPost.findById(found._id, function(err, updated) { db.close(); assert.ifError(err); - assert.equal('after-change', updated.comments[0].title); + assert.equal(updated.comments[0].title, 'after-change'); done(); }); }); @@ -3122,21 +3127,21 @@ describe('Model', function() { }); it('updating an embedded document in an embedded array (gh-255)', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); BlogPost.create({comments: [{title: 'woot'}]}, function(err, post) { assert.ifError(err); BlogPost.findById(post._id, function(err, found) { assert.ifError(err); - assert.equal('woot', found.comments[0].title); + assert.equal(found.comments[0].title, 'woot'); found.comments[0].title = 'notwoot'; - found.save( function(err) { + found.save(function(err) { assert.ifError(err); BlogPost.findById(found._id, function(err, updated) { db.close(); assert.ifError(err); - assert.equal('notwoot', updated.comments[0].title); + assert.equal(updated.comments[0].title, 'notwoot'); done(); }); }); @@ -3145,29 +3150,29 @@ describe('Model', function() { }); it('updating an embedded array document to an Object value (gh-334)', function(done) { - var db = start() - , SubSchema = new Schema({ - name : String , - subObj : { subName : String } - }); - var GH334Schema = new Schema({ name : String , arrData : [ SubSchema] }); + var db = start(), + SubSchema = new Schema({ + name: String, + subObj: {subName: String} + }); + var GH334Schema = new Schema({name: String, arrData: [SubSchema]}); - mongoose.model('GH334' , GH334Schema); + mongoose.model('GH334', GH334Schema); var AModel = db.model('GH334'); var instance = new AModel(); - instance.set( { name : 'name-value' , arrData : [ { name : 'arrName1' , subObj : { subName : 'subName1' } } ] }); + instance.set({name: 'name-value', arrData: [{name: 'arrName1', subObj: {subName: 'subName1'}}]}); instance.save(function(err) { assert.ifError(err); AModel.findById(instance.id, function(err, doc) { assert.ifError(err); - doc.arrData[0].set('subObj', { subName : 'modified subName' }); + doc.arrData[0].set('subObj', {subName: 'modified subName'}); doc.save(function(err) { assert.ifError(err); AModel.findById(instance.id, function(err, doc) { db.close(); assert.ifError(err); - assert.equal(doc.arrData[0].subObj.subName,'modified subName'); + assert.equal(doc.arrData[0].subObj.subName, 'modified subName'); done(); }); }); @@ -3176,24 +3181,24 @@ describe('Model', function() { }); it('saving an embedded document twice should not push that doc onto the parent doc twice (gh-267)', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , post = new BlogPost(); + var db = start(), + BlogPost = db.model('BlogPost', collection), + post = new BlogPost(); post.comments.push({title: 'woot'}); - post.save( function(err) { + post.save(function(err) { assert.ifError(err); - assert.equal(1, post.comments.length); + assert.equal(post.comments.length, 1); BlogPost.findById(post.id, function(err, found) { assert.ifError(err); - assert.equal(1, found.comments.length); - post.save( function(err) { + assert.equal(found.comments.length, 1); + post.save(function(err) { assert.ifError(err); - assert.equal(1, post.comments.length); + assert.equal(post.comments.length, 1); BlogPost.findById(post.id, function(err, found) { db.close(); assert.ifError(err); - assert.equal(1, found.comments.length); + assert.equal(found.comments.length, 1); done(); }); }); @@ -3203,13 +3208,13 @@ describe('Model', function() { describe('embedded array filtering', function() { it('by the id shortcut function', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); - post.comments.push({ title: 'woot' }); - post.comments.push({ title: 'aaaa' }); + post.comments.push({title: 'woot'}); + post.comments.push({title: 'aaaa'}); var subdoc1 = post.comments[0]; var subdoc2 = post.comments[1]; @@ -3222,19 +3227,19 @@ describe('Model', function() { assert.ifError(err); // test with an objectid - assert.equal(doc.comments.id(subdoc1.get('_id')).title,'woot'); + assert.equal(doc.comments.id(subdoc1.get('_id')).title, 'woot'); // test with a string var id = subdoc2._id.toString(); - assert.equal(doc.comments.id(id).title,'aaaa'); + assert.equal(doc.comments.id(id).title, 'aaaa'); done(); }); }); }); it('by the id with cast error', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); @@ -3251,8 +3256,8 @@ describe('Model', function() { }); it('by the id shortcut with no match', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); @@ -3270,13 +3275,13 @@ describe('Model', function() { }); it('removing a subdocument atomically', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.title = 'hahaha'; - post.comments.push({ title: 'woot' }); - post.comments.push({ title: 'aaaa' }); + post.comments.push({title: 'woot'}); + post.comments.push({title: 'aaaa'}); post.save(function(err) { assert.ifError(err); @@ -3291,8 +3296,8 @@ describe('Model', function() { BlogPost.findById(post.get('_id'), function(err, doc) { db.close(); assert.ifError(err); - assert.equal(1, doc.comments.length); - assert.equal(doc.comments[0].title,'aaaa'); + assert.equal(doc.comments.length, 1); + assert.equal(doc.comments[0].title, 'aaaa'); done(); }); }); @@ -3301,13 +3306,13 @@ describe('Model', function() { }); it('single pull embedded doc', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.title = 'hahaha'; - post.comments.push({ title: 'woot' }); - post.comments.push({ title: 'aaaa' }); + post.comments.push({title: 'woot'}); + post.comments.push({title: 'aaaa'}); post.save(function(err) { assert.ifError(err); @@ -3323,7 +3328,7 @@ describe('Model', function() { BlogPost.findById(post.get('_id'), function(err, doc) { db.close(); assert.ifError(err); - assert.equal(0, doc.comments.length); + assert.equal(doc.comments.length, 0); done(); }); }); @@ -3332,9 +3337,9 @@ describe('Model', function() { }); it('saving mixed data', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection) - , count = 3; + var db = start(), + BlogPost = db.model('BlogPost', collection), + count = 3; // string var post = new BlogPost(); @@ -3344,7 +3349,9 @@ describe('Model', function() { BlogPost.findById(post._id, function(err) { assert.ifError(err); - if (--count) return; + if (--count) { + return; + } db.close(); done(); }); @@ -3352,14 +3359,14 @@ describe('Model', function() { // array var post2 = new BlogPost(); - post2.mixed = { name: "mr bungle", arr: [] }; + post2.mixed = {name: 'mr bungle', arr: []}; post2.save(function(err) { assert.ifError(err); BlogPost.findById(post2._id, function(err, doc) { assert.ifError(err); - assert.equal(true, Array.isArray(doc.mixed.arr)); + assert.equal(Array.isArray(doc.mixed.arr), true); doc.mixed = [{foo: 'bar'}]; doc.save(function(err) { @@ -3368,9 +3375,9 @@ describe('Model', function() { BlogPost.findById(doc._id, function(err, doc) { assert.ifError(err); - assert.equal(true, Array.isArray(doc.mixed)); - doc.mixed.push({ hello: 'world' }); - doc.mixed.push([ 'foo', 'bar' ]); + assert.equal(Array.isArray(doc.mixed), true); + doc.mixed.push({hello: 'world'}); + doc.mixed.push(['foo', 'bar']); doc.markModified('mixed'); doc.save(function(err) { @@ -3379,10 +3386,12 @@ describe('Model', function() { BlogPost.findById(post2._id, function(err, doc) { assert.ifError(err); - assert.deepEqual(doc.mixed[0],{ foo: 'bar' }); - assert.deepEqual(doc.mixed[1],{ hello: 'world' }); - assert.deepEqual(doc.mixed[2],['foo','bar']); - if (--count) return; + assert.deepEqual(doc.mixed[0], {foo: 'bar'}); + assert.deepEqual(doc.mixed[1], {hello: 'world'}); + assert.deepEqual(doc.mixed[2], ['foo', 'bar']); + if (--count) { + return; + } db.close(); done(); }); @@ -3398,7 +3407,9 @@ describe('Model', function() { BlogPost.findById(post3._id, function(err, doc) { assert.ifError(err); assert.ok(doc.mixed instanceof Date); - if (--count) return; + if (--count) { + return; + } db.close(); done(); }); @@ -3409,46 +3420,46 @@ describe('Model', function() { }); it('populating mixed data from the constructor (gh-200)', function(done) { - var db = start() - , BlogPost = db.model('BlogPost'); + var db = start(), + BlogPost = db.model('BlogPost'); var post = new BlogPost({ mixed: { - type: 'test' - , github: 'rules' - , nested: { + type: 'test', + github: 'rules', + nested: { number: 3 } } }); db.close(); - assert.equal('test', post.mixed.type); - assert.equal('rules', post.mixed.github); - assert.equal(3, post.mixed.nested.number); + assert.equal(post.mixed.type, 'test'); + assert.equal(post.mixed.github, 'rules'); + assert.equal(post.mixed.nested.number, 3); done(); }); it('"type" is allowed as a key', function(done) { mongoose.model('TestTypeDefaults', new Schema({ - type: { type: String, default: 'YES!' } + type: {type: String, default: 'YES!'} })); - var db = start() - , TestDefaults = db.model('TestTypeDefaults'); + var db = start(), + TestDefaults = db.model('TestTypeDefaults'); var post = new TestDefaults(); - assert.equal(typeof post.get('type'),'string'); - assert.equal(post.get('type'),'YES!'); + assert.equal(typeof post.get('type'), 'string'); + assert.equal(post.get('type'), 'YES!'); // GH-402 var TestDefaults2 = db.model('TestTypeDefaults2', new Schema({ - x: { y: { type: { type: String }, owner: String } } + x: {y: {type: {type: String}, owner: String}} })); post = new TestDefaults2; - post.x.y.type = "#402"; - post.x.y.owner = "me"; + post.x.y.type = '#402'; + post.x.y.owner = 'me'; post.save(function(err) { db.close(); assert.ifError(err); @@ -3457,8 +3468,8 @@ describe('Model', function() { }); it('unaltered model does not clear the doc (gh-195)', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.title = 'woot'; @@ -3475,7 +3486,7 @@ describe('Model', function() { BlogPost.findById(doc._id, function(err, doc) { db.close(); assert.ifError(err); - assert.equal(doc.title,'woot'); + assert.equal(doc.title, 'woot'); done(); }); }); @@ -3486,8 +3497,8 @@ describe('Model', function() { describe('safe mode', function() { it('works', function(done) { var Human = new Schema({ - name : String - , email : { type: String, index: { unique: true, background: false }} + name: String, + email: {type: String, index: {unique: true, background: false}} }); mongoose.model('SafeHuman', Human, true); @@ -3498,8 +3509,8 @@ describe('Model', function() { Human.on('index', function(err) { assert.ifError(err); var me = new Human({ - name : 'Guillermo Rauch' - , email : 'rauchg@gmail.com' + name: 'Guillermo Rauch', + email: 'rauchg@gmail.com' }); me.save(function(err) { @@ -3507,11 +3518,11 @@ describe('Model', function() { Human.findById(me._id, function(err, doc) { assert.ifError(err); - assert.equal(doc.email,'rauchg@gmail.com'); + assert.equal(doc.email, 'rauchg@gmail.com'); var copycat = new Human({ - name : 'Lionel Messi' - , email : 'rauchg@gmail.com' + name: 'Lionel Messi', + email: 'rauchg@gmail.com' }); copycat.save(function(err) { @@ -3523,13 +3534,12 @@ describe('Model', function() { }); }); }); - }); it('can be disabled', function(done) { var Human = new Schema({ - name : String - , email : { type: String, index: { unique: true, background: false }} + name: String, + email: {type: String, index: {unique: true, background: false}} }); // turn it off @@ -3545,8 +3555,8 @@ describe('Model', function() { }); var me = new Human({ - name : 'Guillermo Rauch' - , email : 'rauchg@gmail.com' + name: 'Guillermo Rauch', + email: 'rauchg@gmail.com' }); me.save(function(err) { @@ -3557,11 +3567,11 @@ describe('Model', function() { setTimeout(function() { Human.findById(me._id, function(err, doc) { assert.ifError(err); - assert.equal(doc.email,'rauchg@gmail.com'); + assert.equal(doc.email, 'rauchg@gmail.com'); var copycat = new Human({ - name : 'Lionel Messi' - , email : 'rauchg@gmail.com' + name: 'Lionel Messi', + email: 'rauchg@gmail.com' }); copycat.save(function(err) { @@ -3579,7 +3589,7 @@ describe('Model', function() { describe('pre', function() { it('can pass non-error values to the next middleware', function(done) { var db = start(); - var schema = new Schema({ name: String }); + var schema = new Schema({name: String}); schema.pre('save', function(next) { next('hey there'); @@ -3596,7 +3606,7 @@ describe('Model', function() { next('don\'t call me'); }); var S = db.model('S', schema, collection); - var s = new S({ name: 'angelina' }); + var s = new S({name: 'angelina'}); s.save(function(err) { db.close(); @@ -3608,7 +3618,7 @@ describe('Model', function() { it('with undefined and null', function(done) { var db = start(); - var schema = new Schema({ name: String }); + var schema = new Schema({name: String}); var called = 0; schema.pre('save', function(next) { @@ -3627,7 +3637,7 @@ describe('Model', function() { s.save(function(err) { db.close(); assert.ifError(err); - assert.equal(2, called); + assert.equal(called, 2); done(); }); }); @@ -3635,7 +3645,7 @@ describe('Model', function() { it('with an async waterfall', function(done) { var db = start(); - var schema = new Schema({ name: String }); + var schema = new Schema({name: String}); var called = 0; schema.pre('save', true, function(next, done) { @@ -3658,7 +3668,7 @@ describe('Model', function() { p.onResolve(function(err) { db.close(); assert.ifError(err); - assert.equal(2, called); + assert.equal(called, 2); done(); }); }); @@ -3667,19 +3677,19 @@ describe('Model', function() { it('called on all sub levels', function(done) { var db = start(); - var grandSchema = new Schema({ name : String }); + var grandSchema = new Schema({name: String}); grandSchema.pre('save', function(next) { this.name = 'grand'; next(); }); - var childSchema = new Schema({ name : String, grand : [grandSchema]}); + var childSchema = new Schema({name: String, grand: [grandSchema]}); childSchema.pre('save', function(next) { this.name = 'child'; next(); }); - var schema = new Schema({ name: String, child : [childSchema] }); + var schema = new Schema({name: String, child: [childSchema]}); schema.pre('save', function(next) { this.name = 'parent'; @@ -3687,14 +3697,14 @@ describe('Model', function() { }); var S = db.model('presave_hook', schema, 'presave_hook'); - var s = new S({ name : 'a' , child : [ { name : 'b', grand : [{ name : 'c'}] } ]}); + var s = new S({name: 'a', child: [{name: 'b', grand: [{name: 'c'}]}]}); s.save(function(err, doc) { db.close(); assert.ifError(err); - assert.equal(doc.name,'parent'); - assert.equal(doc.child[0].name,'child'); - assert.equal(doc.child[0].grand[0].name,'grand'); + assert.equal(doc.name, 'parent'); + assert.equal(doc.child[0].name, 'child'); + assert.equal(doc.child[0].grand[0].name, 'grand'); done(); }); }); @@ -3703,39 +3713,39 @@ describe('Model', function() { it('error on any sub level', function(done) { var db = start(); - var grandSchema = new Schema({ name : String }); + var grandSchema = new Schema({name: String}); grandSchema.pre('save', function(next) { next(new Error('Error 101')); }); - var childSchema = new Schema({ name : String, grand : [grandSchema]}); + var childSchema = new Schema({name: String, grand: [grandSchema]}); childSchema.pre('save', function(next) { this.name = 'child'; next(); }); - var schema = new Schema({ name: String, child : [childSchema] }); + var schema = new Schema({name: String, child: [childSchema]}); schema.pre('save', function(next) { this.name = 'parent'; next(); }); var S = db.model('presave_hook_error', schema, 'presave_hook_error'); - var s = new S({ name : 'a' , child : [ { name : 'b', grand : [{ name : 'c'}] } ]}); + var s = new S({name: 'a', child: [{name: 'b', grand: [{name: 'c'}]}]}); s.save(function(err) { db.close(); assert.ok(err instanceof Error); - assert.equal(err.message,'Error 101'); + assert.equal(err.message, 'Error 101'); done(); }); }); describe('init', function() { it('has access to the true ObjectId when used with querying (gh-289)', function(done) { - var db = start() - , PreInitSchema = new Schema({}) - , preId = null; + var db = start(), + PreInitSchema = new Schema({}), + preId = null; PreInitSchema.pre('init', function(next) { preId = this._id; @@ -3779,18 +3789,17 @@ describe('Model', function() { new Kaboom().funky(); done(); }); - }); describe('post', function() { it('works', function(done) { var schema = new Schema({ title: String - }) - , save = false - , remove = false - , init = false - , post = undefined; + }), + save = false, + remove = false, + init = false, + post = undefined; schema.post('save', function(arg) { assert.equal(arg.id, post.id); @@ -3802,14 +3811,14 @@ describe('Model', function() { }); schema.post('remove', function(arg) { - assert.equal(arg.id,post.id); + assert.equal(arg.id, post.id); remove = true; }); mongoose.model('PostHookTest', schema); - var db = start() - , BlogPost = db.model('PostHookTest'); + var db = start(), + BlogPost = db.model('PostHookTest'); post = new BlogPost(); @@ -3833,39 +3842,28 @@ describe('Model', function() { }); }); }); - }); - }); it('on embedded docs', function(done) { var save = false; var EmbeddedSchema = new Schema({ - title : String - }); - - var ParentSchema = new Schema({ - embeds : [EmbeddedSchema] + title: String }); EmbeddedSchema.post('save', function() { save = true; }); - // Don't know how to test those on a embedded document. - //EmbeddedSchema.post('init', function () { - //init = true; - //}); - - //EmbeddedSchema.post('remove', function () { - //remove = true; - //}); + var ParentSchema = new Schema({ + embeds: [EmbeddedSchema] + }); mongoose.model('Parent', ParentSchema); - var db = start(), - Parent = db.model('Parent'); + var db = start(); + var Parent = db.model('Parent'); var parent = new Parent(); @@ -3879,13 +3877,12 @@ describe('Model', function() { }); }); }); - }); describe('#exec()', function() { it('count()', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema); + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema); BlogPost.create({title: 'interoperable count as promise'}, function(err) { assert.ifError(err); @@ -3893,7 +3890,7 @@ describe('Model', function() { query.exec(function(err, count) { db.close(); assert.ifError(err); - assert.equal(1, count); + assert.equal(count, 1); done(); }); }); @@ -3901,8 +3898,8 @@ describe('Model', function() { it('update()', function(done) { var col = 'BlogPost' + random(); - var db = start() - , BlogPost = db.model(col, bpSchema); + var db = start(), + BlogPost = db.model(col, bpSchema); BlogPost.create({title: 'interoperable update as promise'}, function(err) { assert.ifError(err); @@ -3912,7 +3909,7 @@ describe('Model', function() { BlogPost.count({title: 'interoperable update as promise delta'}, function(err, count) { db.close(); assert.ifError(err); - assert.equal(1, count); + assert.equal(count, 1); done(); }); }); @@ -3920,8 +3917,8 @@ describe('Model', function() { }); it('findOne()', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema); + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema); BlogPost.create({title: 'interoperable findOne as promise'}, function(err, created) { assert.ifError(err); @@ -3929,68 +3926,68 @@ describe('Model', function() { query.exec(function(err, found) { db.close(); assert.ifError(err); - assert.equal(found.id,created.id); + assert.equal(found.id, created.id); done(); }); }); }); it('find()', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema); + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema); BlogPost.create( - {title: 'interoperable find as promise'} - , {title: 'interoperable find as promise'} - , function(err, createdOne, createdTwo) { - assert.ifError(err); - var query = BlogPost.find({title: 'interoperable find as promise'}).sort('_id'); - query.exec(function(err, found) { - db.close(); + {title: 'interoperable find as promise'}, + {title: 'interoperable find as promise'}, + function(err, createdOne, createdTwo) { assert.ifError(err); - assert.equal(found.length, 2); - var ids = {}; - ids[String(found[0]._id)] = 1; - ids[String(found[1]._id)] = 1; - assert.ok(String(createdOne._id) in ids); - assert.ok(String(createdTwo._id) in ids); - done(); + var query = BlogPost.find({title: 'interoperable find as promise'}).sort('_id'); + query.exec(function(err, found) { + db.close(); + assert.ifError(err); + assert.equal(found.length, 2); + var ids = {}; + ids[String(found[0]._id)] = 1; + ids[String(found[1]._id)] = 1; + assert.ok(String(createdOne._id) in ids); + assert.ok(String(createdTwo._id) in ids); + done(); + }); }); - }); }); it('remove()', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema); + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema); BlogPost.create( - {title: 'interoperable remove as promise'} - , function(err) { - assert.ifError(err); - var query = BlogPost.remove({title: 'interoperable remove as promise'}); - query.exec(function(err) { + {title: 'interoperable remove as promise'}, + function(err) { assert.ifError(err); - BlogPost.count({title: 'interoperable remove as promise'}, function(err, count) { - db.close(); - assert.equal(count, 0); - done(); + var query = BlogPost.remove({title: 'interoperable remove as promise'}); + query.exec(function(err) { + assert.ifError(err); + BlogPost.count({title: 'interoperable remove as promise'}, function(err, count) { + db.close(); + assert.equal(count, 0); + done(); + }); }); }); - }); }); it('op can be changed', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema) - , title = 'interop ad-hoc as promise'; + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema), + title = 'interop ad-hoc as promise'; - BlogPost.create({title: title }, function(err, created) { + BlogPost.create({title: title}, function(err, created) { assert.ifError(err); - var query = BlogPost.count({title: title }); + var query = BlogPost.count({title: title}); query.exec('findOne', function(err, found) { db.close(); assert.ifError(err); - assert.equal(found.id,created.id); + assert.equal(found.id, created.id); done(); }); }); @@ -3998,8 +3995,8 @@ describe('Model', function() { describe('promises', function() { it('count()', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema); + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema); BlogPost.create({title: 'interoperable count as promise 2'}, function(err) { assert.ifError(err); @@ -4008,7 +4005,7 @@ describe('Model', function() { promise.onResolve(function(err, count) { db.close(); assert.ifError(err); - assert.equal(1, count); + assert.equal(count, 1); done(); }); }); @@ -4016,8 +4013,8 @@ describe('Model', function() { it('update()', function(done) { var col = 'BlogPost' + random(); - var db = start() - , BlogPost = db.model(col, bpSchema); + var db = start(), + BlogPost = db.model(col, bpSchema); BlogPost.create({title: 'interoperable update as promise 2'}, function(err) { assert.ifError(err); @@ -4028,7 +4025,7 @@ describe('Model', function() { BlogPost.count({title: 'interoperable update as promise delta 2'}, function(err, count) { db.close(); assert.ifError(err); - assert.equal(1, count); + assert.equal(count, 1); done(); }); }); @@ -4036,8 +4033,8 @@ describe('Model', function() { }); it('findOne()', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema); + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema); BlogPost.create({title: 'interoperable findOne as promise 2'}, function(err, created) { assert.ifError(err); @@ -4046,58 +4043,58 @@ describe('Model', function() { promise.onResolve(function(err, found) { db.close(); assert.ifError(err); - assert.equal(found.id,created.id); + assert.equal(found.id, created.id); done(); }); }); }); it('find()', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema); + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema); BlogPost.create( - {title: 'interoperable find as promise 2'} - , {title: 'interoperable find as promise 2'} - , function(err, createdOne, createdTwo) { - assert.ifError(err); - var query = BlogPost.find({title: 'interoperable find as promise 2'}).sort('_id'); - var promise = query.exec(); - promise.onResolve(function(err, found) { - db.close(); + {title: 'interoperable find as promise 2'}, + {title: 'interoperable find as promise 2'}, + function(err, createdOne, createdTwo) { assert.ifError(err); - assert.equal(found.length,2); - assert.equal(found[0].id,createdOne.id); - assert.equal(found[1].id,createdTwo.id); - done(); + var query = BlogPost.find({title: 'interoperable find as promise 2'}).sort('_id'); + var promise = query.exec(); + promise.onResolve(function(err, found) { + db.close(); + assert.ifError(err); + assert.equal(found.length, 2); + assert.equal(found[0].id, createdOne.id); + assert.equal(found[1].id, createdTwo.id); + done(); + }); }); - }); }); it('remove()', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema); + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema); BlogPost.create( - {title: 'interoperable remove as promise 2'} - , function(err) { - assert.ifError(err); - var query = BlogPost.remove({title: 'interoperable remove as promise 2'}); - var promise = query.exec(); - promise.onResolve(function(err) { + {title: 'interoperable remove as promise 2'}, + function(err) { assert.ifError(err); - BlogPost.count({title: 'interoperable remove as promise 2'}, function(err, count) { - db.close(); - assert.equal(count,0); - done(); + var query = BlogPost.remove({title: 'interoperable remove as promise 2'}); + var promise = query.exec(); + promise.onResolve(function(err) { + assert.ifError(err); + BlogPost.count({title: 'interoperable remove as promise 2'}, function(err, count) { + db.close(); + assert.equal(count, 0); + done(); + }); }); }); - }); }); it('are compatible with op modification on the fly', function(done) { - var db = start() - , BlogPost = db.model('BlogPost' + random(), bpSchema); + var db = start(), + BlogPost = db.model('BlogPost' + random(), bpSchema); BlogPost.create({title: 'interoperable ad-hoc as promise 2'}, function(err, created) { assert.ifError(err); @@ -4106,58 +4103,58 @@ describe('Model', function() { promise.onResolve(function(err, found) { db.close(); assert.ifError(err); - assert.equal(found._id.id,created._id.id); + assert.equal(found._id.toHexString(), created._id.toHexString()); done(); }); }); }); it('are thenable', function(done) { - var db = start() - , B = db.model('BlogPost' + random(), bpSchema); + var db = start(), + B = db.model('BlogPost' + random(), bpSchema); - var peopleSchema = Schema({ name: String, likes: ['ObjectId'] }); + var peopleSchema = new Schema({name: String, likes: ['ObjectId']}); var P = db.model('promise-BP-people', peopleSchema, random()); B.create( - { title: 'then promise 1' } - , { title: 'then promise 2' } - , { title: 'then promise 3' } - , function(err, d1, d2, d3) { - assert.ifError(err); - - P.create( - { name: 'brandon', likes: [d1] } - , { name: 'ben', likes: [d2] } - , { name: 'bernie', likes: [d3] } - , function(err) { + {title: 'then promise 1'}, + {title: 'then promise 2'}, + {title: 'then promise 3'}, + function(err, d1, d2, d3) { assert.ifError(err); - var promise = B.find({ title: /^then promise/ }).select('_id').exec(); - promise.then(function(blogs) { - var ids = blogs.map(function(m) { - return m._id; - }); - return P.where('likes').in(ids).exec(); - }).then(function(people) { - assert.equal(3, people.length); - return people; - }).then(function() { - db.close(); - done(); - }, function(err) { - db.close(); - done(new Error(err)); - }); + P.create( + {name: 'brandon', likes: [d1]}, + {name: 'ben', likes: [d2]}, + {name: 'bernie', likes: [d3]}, + function(err) { + assert.ifError(err); + + var promise = B.find({title: /^then promise/}).select('_id').exec(); + promise.then(function(blogs) { + var ids = blogs.map(function(m) { + return m._id; + }); + return P.where('likes').in(ids).exec(); + }).then(function(people) { + assert.equal(people.length, 3); + return people; + }).then(function() { + db.close(); + done(); + }, function(err) { + db.close(); + done(new Error(err)); + }); + }); }); - }); }); }); }); describe('console.log', function() { it('hides private props', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var date = new Date(1305730951086); var id0 = new DocumentObjectId('4dd3e169dbfb13b4570000b9'); @@ -4166,38 +4163,38 @@ describe('Model', function() { var id3 = new DocumentObjectId('4dd3e169dbfb13b4570000b8'); var post = new BlogPost({ - title: 'Test' - , _id: id0 - , date: date - , numbers: [5,6,7] - , owners: [id1] - , meta: { visitors: 45 } - , comments: [ - { _id: id2, title: 'my comment', date: date, body: 'this is a comment' }, - { _id: id3, title: 'the next thang', date: date, body: 'this is a comment too!' }] + title: 'Test', + _id: id0, + date: date, + numbers: [5, 6, 7], + owners: [id1], + meta: {visitors: 45}, + comments: [ + {_id: id2, title: 'my comment', date: date, body: 'this is a comment'}, + {_id: id3, title: 'the next thang', date: date, body: 'this is a comment too!'}] }); db.close(); var out = post.inspect(); - assert.ok(/meta: { visitors: 45 }/.test(out)); - assert.ok(/numbers: \[ 5, 6, 7 \]/.test(out)); - assert.ok(/Wed.+ 2011 \d\d:02:31 GMT/.test(out)); - assert.ok(!/activePaths:/.test(out)); - assert.ok(!/_atomics:/.test(out)); + assert.equal(out.meta.visitors, post.meta.visitors); + assert.deepEqual(out.numbers, Array.prototype.slice.call(post.numbers)); + assert.equal(out.date.valueOf(), post.date.valueOf()); + assert.equal(out.activePaths, undefined); + assert.equal(out._atomics, undefined); done(); }); }); describe('pathnames', function() { it('named path can be used', function(done) { - var db = start() - , P = db.model('pathnametest', new Schema({ path: String })); + var db = start(), + P = db.model('pathnametest', new Schema({path: String})); db.close(); var threw = false; try { - new P({ path: 'i should not throw' }); + new P({path: 'i should not throw'}); } catch (err) { threw = true; } @@ -4211,11 +4208,11 @@ describe('Model', function() { describe('if disabled', function() { describe('with mongo down', function() { it('and no command buffering should pass an error', function(done) { - var db = start({ db: { bufferMaxEntries: 0 } }); - var schema = Schema({ type: String }, { bufferCommands: false }); + var db = start({db: {bufferMaxEntries: 0}}); + var schema = new Schema({type: String}, {bufferCommands: false}); var T = db.model('Thing', schema); db.on('open', function() { - var t = new T({ type: "monster" }); + var t = new T({type: 'monster'}); var worked = false; t.save(function(err) { @@ -4237,26 +4234,26 @@ describe('Model', function() { it('subdocuments with changed values should persist the values', function(done) { var db = start(); - var Subdoc = new Schema({ name: String, mixed: Schema.Types.Mixed }); - var T = db.model('SubDocMixed', new Schema({ subs: [Subdoc] })); + var Subdoc = new Schema({name: String, mixed: Schema.Types.Mixed}); + var T = db.model('SubDocMixed', new Schema({subs: [Subdoc]})); - var t = new T({ subs: [{ name: "Hubot", mixed: { w: 1, x: 2 }}] }); - assert.equal(t.subs[0].name,"Hubot"); - assert.equal(t.subs[0].mixed.w,1); - assert.equal(t.subs[0].mixed.x,2); + var t = new T({subs: [{name: 'Hubot', mixed: {w: 1, x: 2}}]}); + assert.equal(t.subs[0].name, 'Hubot'); + assert.equal(t.subs[0].mixed.w, 1); + assert.equal(t.subs[0].mixed.x, 2); t.save(function(err) { assert.ifError(err); T.findById(t._id, function(err, t) { assert.ifError(err); - assert.equal(t.subs[0].name,"Hubot"); - assert.equal(t.subs[0].mixed.w,1); - assert.equal(t.subs[0].mixed.x,2); + assert.equal(t.subs[0].name, 'Hubot'); + assert.equal(t.subs[0].mixed.w, 1); + assert.equal(t.subs[0].mixed.x, 2); var sub = t.subs[0]; - sub.name = "Hubot1"; - assert.equal(sub.name,"Hubot1"); + sub.name = 'Hubot1'; + assert.equal(sub.name, 'Hubot1'); assert.ok(sub.isModified('name')); assert.ok(t.isModified()); @@ -4265,11 +4262,11 @@ describe('Model', function() { T.findById(t._id, function(err, t) { assert.ifError(err); - assert.strictEqual(t.subs[0].name, "Hubot1"); + assert.strictEqual(t.subs[0].name, 'Hubot1'); var sub = t.subs[0]; sub.mixed.w = 5; - assert.equal(sub.mixed.w,5); + assert.equal(sub.mixed.w, 5); assert.ok(!sub.isModified('mixed')); sub.markModified('mixed'); assert.ok(sub.isModified('mixed')); @@ -4294,19 +4291,19 @@ describe('Model', function() { describe('RegExps', function() { it('can be saved', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); - var post = new BlogPost({ mixed: { rgx: /^asdf$/ } }); + var post = new BlogPost({mixed: {rgx: /^asdf$/}}); assert.ok(post.mixed.rgx instanceof RegExp); - assert.equal(post.mixed.rgx.source,'^asdf$'); + assert.equal(post.mixed.rgx.source, '^asdf$'); post.save(function(err) { assert.ifError(err); BlogPost.findById(post._id, function(err, post) { db.close(); assert.ifError(err); assert.ok(post.mixed.rgx instanceof RegExp); - assert.equal(post.mixed.rgx.source,'^asdf$'); + assert.equal(post.mixed.rgx.source, '^asdf$'); done(); }); }); @@ -4315,22 +4312,22 @@ describe('Model', function() { // Demonstration showing why GH-261 is a misunderstanding it('a single instantiated document should be able to update its embedded documents more than once', function(done) { - var db = start() - , BlogPost = db.model('BlogPost', collection); + var db = start(), + BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.comments.push({title: 'one'}); post.save(function(err) { assert.ifError(err); - assert.equal(post.comments[0].title,'one'); + assert.equal(post.comments[0].title, 'one'); post.comments[0].title = 'two'; - assert.equal(post.comments[0].title,'two'); + assert.equal(post.comments[0].title, 'two'); post.save(function(err) { assert.ifError(err); BlogPost.findById(post._id, function(err, found) { db.close(); assert.ifError(err); - assert.equal(found.comments[0].title,'two'); + assert.equal(found.comments[0].title, 'two'); done(); }); }); @@ -4361,17 +4358,17 @@ describe('Model', function() { it('returns number of affected docs', function(done) { var db = start(); - var schema = new Schema({ name: String }); + var schema = new Schema({name: String}); var S = db.model('AffectedDocsAreReturned', schema); - var s = new S({ name: 'aaron' }); + var s = new S({name: 'aaron'}); s.save(function(err, doc, affected) { assert.ifError(err); - assert.equal(1, affected); + assert.equal(affected, 1); s.name = 'heckmanananananana'; s.save(function(err, doc, affected) { db.close(); assert.ifError(err); - assert.equal(1, affected); + assert.equal(affected, 1); done(); }); }); @@ -4379,19 +4376,19 @@ describe('Model', function() { it('returns 0 as the number of affected docs if doc was not modified', function(done) { var db = start(), - schema = new Schema({ name: String }), + schema = new Schema({name: String}), Model = db.model('AffectedDocsAreReturned', schema), - doc = new Model({ name: 'aaron' }); + doc = new Model({name: 'aaron'}); doc.save(function(err, doc, affected) { assert.ifError(err); - assert.equal(1, affected); + assert.equal(affected, 1); Model.findById(doc.id).then(function(doc) { doc.save(function(err, doc, affected) { db.close(); assert.ifError(err); - assert.equal(0, affected); + assert.equal(affected, 0); done(); }); }); @@ -4399,10 +4396,10 @@ describe('Model', function() { }); it('saved changes made within callback of a previous no-op save gh-1139', function(done) { - var db = start() - , B = db.model('BlogPost', collection); + var db = start(), + B = db.model('BlogPost', collection); - var post = new B({ title: 'first' }); + var post = new B({title: 'first'}); post.save(function(err) { assert.ifError(err); @@ -4416,8 +4413,8 @@ describe('Model', function() { B.findById(post, function(err, doc) { assert.ifError(err); - assert.equal('changed', doc.title); - done(); + assert.equal(doc.title, 'changed'); + db.close(done); }); }); }); @@ -4426,7 +4423,7 @@ describe('Model', function() { it('rejects new documents that have no _id set (1595)', function(done) { var db = start(); - var s = new Schema({ _id: { type: String }}); + var s = new Schema({_id: {type: String}}); var B = db.model('1595', s); var b = new B; b.save(function(err) { @@ -4441,14 +4438,14 @@ describe('Model', function() { describe('_delta()', function() { it('should overwrite arrays when directly set (gh-1126)', function(done) { - var db = start() - , B = db.model('BlogPost', collection); + var db = start(), + B = db.model('BlogPost', collection); - B.create({ title: 'gh-1126', numbers: [1,2] }, function(err, b) { + B.create({title: 'gh-1126', numbers: [1, 2]}, function(err, b) { assert.ifError(err); B.findById(b._id, function(err, b) { assert.ifError(err); - assert.deepEqual([1,2].join(), b.numbers.join()); + assert.deepEqual([1, 2].join(), b.numbers.join()); b.numbers = []; b.numbers.push(3); @@ -4465,8 +4462,8 @@ describe('Model', function() { B.findById(b._id, function(err, b) { assert.ifError(err); assert.ok(Array.isArray(b.numbers)); - assert.equal(1, b.numbers.length); - assert.equal(3, b.numbers[0]); + assert.equal(b.numbers.length, 1); + assert.equal(b.numbers[0], 3); b.numbers = [3]; var d = b.$__delta(); @@ -4479,10 +4476,10 @@ describe('Model', function() { B.findById(b._id, function(err, b) { assert.ifError(err); assert.ok(Array.isArray(b.numbers)); - assert.equal(2, b.numbers.length); - assert.equal(4, b.numbers[0]); - assert.equal(5, b.numbers[1]); - done(); + assert.equal(b.numbers.length, 2); + assert.equal(b.numbers[0], 4); + assert.equal(b.numbers[1], 5); + db.close(done); }); }); }); @@ -4492,53 +4489,52 @@ describe('Model', function() { }); it('should use $set when subdoc changed before pulling (gh-1303)', function(done) { - var db = start() - , B = db.model('BlogPost', 'gh-1303-' + random()); + var db = start(), + B = db.model('BlogPost', 'gh-1303-' + random()); B.create( - { title: 'gh-1303', comments: [{body:'a'},{body:'b'},{body:'c'}] } - , function(err, b) { - - assert.ifError(err); - B.findById(b._id, function(err, b) { + {title: 'gh-1303', comments: [{body: 'a'}, {body: 'b'}, {body: 'c'}]}, + function(err, b) { assert.ifError(err); + B.findById(b._id, function(err, b) { + assert.ifError(err); - b.comments[2].body = 'changed'; - b.comments.pull(b.comments[1]); - - assert.equal(2, b.comments.length); - assert.equal('a', b.comments[0].body); - assert.equal('changed', b.comments[1].body); + b.comments[2].body = 'changed'; + b.comments.pull(b.comments[1]); - var d = b.$__delta()[1]; - assert.ok('$set' in d, 'invalid delta ' + JSON.stringify(d)); - assert.ok(Array.isArray(d.$set.comments)); - assert.equal(d.$set.comments.length, 2); + assert.equal(b.comments.length, 2); + assert.equal(b.comments[0].body, 'a'); + assert.equal(b.comments[1].body, 'changed'); - b.save(function(err) { - assert.ifError(err); + var d = b.$__delta()[1]; + assert.ok('$set' in d, 'invalid delta ' + JSON.stringify(d)); + assert.ok(Array.isArray(d.$set.comments)); + assert.equal(d.$set.comments.length, 2); - B.findById(b._id, function(err, b) { - db.close(); + b.save(function(err) { assert.ifError(err); - assert.ok(Array.isArray(b.comments)); - assert.equal(2, b.comments.length); - assert.equal('a', b.comments[0].body); - assert.equal('changed', b.comments[1].body); - done(); + + B.findById(b._id, function(err, b) { + db.close(); + assert.ifError(err); + assert.ok(Array.isArray(b.comments)); + assert.equal(b.comments.length, 2); + assert.equal(b.comments[0].body, 'a'); + assert.equal(b.comments[1].body, 'changed'); + done(); + }); }); }); }); - }); }); }); describe('backward compatibility', function() { it('with conflicted data in db', function(done) { var db = start(); - var M = db.model('backwardDataConflict', new Schema({ namey: { first: String, last: String }})); - var m = new M({ namey: "[object Object]" }); - m.namey = { first: 'GI', last: 'Joe' };// <-- should overwrite the string + var M = db.model('backwardDataConflict', new Schema({namey: {first: String, last: String}})); + var m = new M({namey: '[object Object]'}); + m.namey = {first: 'GI', last: 'Joe'};// <-- should overwrite the string m.save(function(err) { db.close(); assert.strictEqual(err, null); @@ -4551,18 +4547,18 @@ describe('Model', function() { it('with positional notation on path not existing in schema (gh-1048)', function(done) { var db = start(); - var M = db.model('backwardCompat-gh-1048', Schema({ name: 'string' })); + var M = db.model('backwardCompat-gh-1048', Schema({name: 'string'})); db.on('open', function() { var o = { - name: 'gh-1048' - , _id: new mongoose.Types.ObjectId - , databases: { - 0: { keys: 100, expires: 0} - , 15: {keys:1,expires:0} + name: 'gh-1048', + _id: new mongoose.Types.ObjectId, + databases: { + 0: {keys: 100, expires: 0}, + 15: {keys: 1, expires: 0} } }; - M.collection.insert(o, { safe: true }, function(err) { + M.collection.insert(o, {safe: true}, function(err) { assert.ifError(err); M.findById(o._id, function(err, doc) { db.close(); @@ -4571,7 +4567,7 @@ describe('Model', function() { assert.ok(doc._doc.databases); assert.ok(doc._doc.databases['0']); assert.ok(doc._doc.databases['15']); - assert.equal(undefined, doc.databases); + assert.equal(doc.databases, undefined); done(); }); }); @@ -4581,14 +4577,14 @@ describe('Model', function() { describe('non-schema adhoc property assignments', function() { it('are not saved', function(done) { - var db = start() - , B = db.model('BlogPost', collection); + var db = start(), + B = db.model('BlogPost', collection); var b = new B; b.whateveriwant = 10; b.save(function(err) { assert.ifError(err); - B.collection.findOne({ _id: b._id }, function(err, doc) { + B.collection.findOne({_id: b._id}, function(err, doc) { db.close(); assert.ifError(err); assert.ok(!('whateveriwant' in doc)); @@ -4600,9 +4596,9 @@ describe('Model', function() { it('should not throw range error when using Number _id and saving existing doc (gh-691)', function(done) { var db = start(); - var T = new Schema({ _id: Number, a: String }); + var T = new Schema({_id: Number, a: String}); var D = db.model('Testing691', T, 'asdf' + random()); - var d = new D({ _id: 1 }); + var d = new D({_id: 1}); d.save(function(err) { assert.ifError(err); @@ -4623,10 +4619,10 @@ describe('Model', function() { it('is saved (gh-742)', function(done) { var db = start(); - var DefaultTestObject = db.model("defaultTestObject", - new Schema({ - score:{type:Number, "default":55} - }) + var DefaultTestObject = db.model('defaultTestObject', + new Schema({ + score: {type: Number, default: 55} + }) ); var myTest = new DefaultTestObject(); @@ -4662,51 +4658,51 @@ describe('Model', function() { it('path is cast to correct value when retreived from db', function(done) { var db = start(); - var schema = new Schema({ title: { type: 'string', index: true }}); + var schema = new Schema({title: {type: 'string', index: true}}); var T = db.model('T', schema); - T.collection.insert({ title: 234 }, {safe:true}, function(err) { + T.collection.insert({title: 234}, {safe: true}, function(err) { assert.ifError(err); T.findOne(function(err, doc) { db.close(); assert.ifError(err); - assert.equal('234', doc.title); + assert.equal(doc.title, '234'); done(); }); }); }); it('setting a path to undefined should retain the value as undefined', function(done) { - var db = start() - , B = db.model('BlogPost', collection + random()); + var db = start(), + B = db.model('BlogPost', collection + random()); var doc = new B; doc.title = 'css3'; - assert.equal(doc.$__delta()[1].$set.title,'css3'); + assert.equal(doc.$__delta()[1].$set.title, 'css3'); doc.title = undefined; - assert.equal(doc.$__delta()[1].$unset.title,1); + assert.equal(doc.$__delta()[1].$unset.title, 1); assert.strictEqual(undefined, doc.$__delta()[1].$set.title); doc.title = 'css3'; doc.author = 'aaron'; - doc.numbers = [3,4,5]; + doc.numbers = [3, 4, 5]; doc.meta.date = new Date; doc.meta.visitors = 89; - doc.comments = [{ title: 'thanksgiving', body: 'yuuuumm' }]; - doc.comments.push({ title: 'turkey', body: 'cranberries' }); + doc.comments = [{title: 'thanksgiving', body: 'yuuuumm'}]; + doc.comments.push({title: 'turkey', body: 'cranberries'}); doc.save(function(err) { assert.ifError(err); B.findById(doc._id, function(err, b) { assert.ifError(err); - assert.equal(b.title,'css3'); - assert.equal(b.author,'aaron'); + assert.equal(b.title, 'css3'); + assert.equal(b.author, 'aaron'); assert.equal(b.meta.date.toString(), doc.meta.date.toString()); assert.equal(b.meta.visitors.valueOf(), doc.meta.visitors.valueOf()); - assert.equal(2, b.comments.length); + assert.equal(b.comments.length, 2); assert.equal(b.comments[0].title, 'thanksgiving'); assert.equal(b.comments[0].body, 'yuuuumm'); - assert.equal(b.comments[1].title,'turkey'); - assert.equal(b.comments[1].body,'cranberries'); + assert.equal(b.comments[1].title, 'turkey'); + assert.equal(b.comments[1].body, 'cranberries'); b.title = undefined; b.author = null; b.meta.date = undefined; @@ -4724,14 +4720,14 @@ describe('Model', function() { assert.strictEqual(null, b.meta.visitors); assert.strictEqual(null, b.comments[0].title); assert.strictEqual(undefined, b.comments[0].body); - assert.equal(b.comments[1].title,'turkey'); - assert.equal(b.comments[1].body,'cranberries'); + assert.equal(b.comments[1].title, 'turkey'); + assert.equal(b.comments[1].body, 'cranberries'); b.meta = undefined; b.comments = undefined; b.save(function(err) { assert.ifError(err); - B.collection.findOne({ _id: b._id}, function(err, b) { + B.collection.findOne({_id: b._id}, function(err, b) { db.close(); assert.ifError(err); assert.strictEqual(undefined, b.meta); @@ -4748,14 +4744,14 @@ describe('Model', function() { describe('unsetting a default value', function() { it('should be ignored (gh-758)', function(done) { var db = start(); - var M = db.model('758', new Schema({ s: String, n: Number, a: Array })); - M.collection.insert({ }, { safe: true }, function(err) { + var M = db.model('758', new Schema({s: String, n: Number, a: Array})); + M.collection.insert({}, {safe: true}, function(err) { assert.ifError(err); M.findOne(function(err, m) { assert.ifError(err); m.s = m.n = m.a = undefined; - assert.equal(undefined, m.$__delta()); - done(); + assert.equal(m.$__delta(), undefined); + db.close(done); }); }); }); @@ -4763,23 +4759,23 @@ describe('Model', function() { it('allow for object passing to ref paths (gh-1606)', function(done) { var db = start(); - var schA = new Schema({ title : String }); + var schA = new Schema({title: String}); var schma = new Schema({ - thing : { type : Schema.Types.ObjectId, ref : 'A' }, - subdoc : { - some : String, - thing : [{ type : Schema.Types.ObjectId, ref : 'A' }] + thing: {type: Schema.Types.ObjectId, ref: 'A'}, + subdoc: { + some: String, + thing: [{type: Schema.Types.ObjectId, ref: 'A'}] } }); var M1 = db.model('A', schA); var M2 = db.model('A2', schma); - var a = new M1({ title : 'hihihih' }).toObject(); + var a = new M1({title: 'hihihih'}).toObject(); var thing = new M2({ - thing : a, - subdoc : { - title : 'blah', - thing : [a] + thing: a, + subdoc: { + title: 'blah', + thing: [a] } }); @@ -4805,15 +4801,23 @@ describe('Model', function() { }); var Order = db.model('order' + random(), OrderSchema); - var o = new Order({ total: null }); + var o = new Order({total: null}); assert.equal(o.total, 10); done(); }); describe('Skip setting default value for Geospatial-indexed fields (gh-1668)', function() { + var db; + + before(function() { + db = start({ noErrorListener: true }); + }); + + after(function(done) { + db.close(done); + }); it('2dsphere indexed field with value is saved', function(done) { - var db = start(); var PersonSchema = new Schema({ name: String, loc: { @@ -4823,7 +4827,7 @@ describe('Model', function() { }); var Person = db.model('Person_1', PersonSchema); - var loc = [ 0.3, 51.4 ]; + var loc = [0.3, 51.4]; var p = new Person({ name: 'Jimmy Page', loc: loc @@ -4837,14 +4841,12 @@ describe('Model', function() { assert.equal(personDoc.loc[0], loc[0]); assert.equal(personDoc.loc[1], loc[1]); - db.close(); done(); }); }); }); it('2dsphere indexed field without value is saved (gh-1668)', function(done) { - var db = start(); var PersonSchema = new Schema({ name: String, loc: { @@ -4866,16 +4868,14 @@ describe('Model', function() { assert.equal(personDoc.name, 'Jimmy Page'); assert.equal(personDoc.loc, undefined); - db.close(); done(); }); }); }); it('2dsphere indexed field in subdoc without value is saved', function(done) { - var db = start(); var PersonSchema = new Schema({ - name: { type: String, required: true }, + name: {type: String, required: true}, nested: { tag: String, loc: { @@ -4884,7 +4884,7 @@ describe('Model', function() { } }); - PersonSchema.index({ 'nested.loc': '2dsphere' }); + PersonSchema.index({'nested.loc': '2dsphere'}); var Person = db.model('Person_3', PersonSchema); var p = new Person({ @@ -4902,14 +4902,12 @@ describe('Model', function() { assert.equal(personDoc.name, 'Jimmy Page'); assert.equal(personDoc.nested.tag, 'guitarist'); assert.equal(personDoc.nested.loc, undefined); - db.close(); done(); }); }); }); it('Doc with 2dsphere indexed field without initial value can be updated', function(done) { - var db = start(); var PersonSchema = new Schema({ name: String, loc: { @@ -4928,23 +4926,21 @@ describe('Model', function() { var updates = { $set: { - loc: [ 0.3, 51.4 ] + loc: [0.3, 51.4] } }; - Person.findByIdAndUpdate(p._id, updates, { 'new': true }, function(err, personDoc) { + Person.findByIdAndUpdate(p._id, updates, {new: true}, function(err, personDoc) { assert.ifError(err); assert.equal(personDoc.loc[0], updates.$set.loc[0]); assert.equal(personDoc.loc[1], updates.$set.loc[1]); - db.close(); done(); }); }); }); it('2dsphere indexed required field without value is rejected', function(done) { - var db = start(); var PersonSchema = new Schema({ name: String, loc: { @@ -4962,14 +4958,12 @@ describe('Model', function() { p.save(function(err) { assert.ok(err instanceof MongooseError); assert.ok(err instanceof ValidationError); - db.close(); done(); }); }); it('2dsphere field without value but with schema default is saved', function(done) { - var db = start(); - var loc = [ 0, 1 ]; + var loc = [0, 1]; var PersonSchema = new Schema({ name: String, loc: { @@ -4992,14 +4986,12 @@ describe('Model', function() { assert.equal(loc[0], personDoc.loc[0]); assert.equal(loc[1], personDoc.loc[1]); - db.close(); done(); }); }); }); it('2d indexed field without value is saved', function(done) { - var db = start(); var PersonSchema = new Schema({ name: String, loc: { @@ -5019,31 +5011,29 @@ describe('Model', function() { Person.findById(p._id, function(err, personDoc) { assert.ifError(err); - assert.equal(undefined, personDoc.loc); - db.close(); + assert.equal(personDoc.loc, undefined); done(); }); }); }); it('Compound index with 2dsphere field without value is saved', function(done) { - var db = start(); var PersonSchema = new Schema({ name: String, type: String, - slug: { type: String, index: { unique: true } }, - loc: { type: [Number] }, - tags: { type: [String], index: true } + slug: {type: String, index: {unique: true}}, + loc: {type: [Number]}, + tags: {type: [String], index: true} }); - PersonSchema.index({ name: 1, loc: '2dsphere' }); + PersonSchema.index({name: 1, loc: '2dsphere'}); var Person = db.model('Person_8', PersonSchema); var p = new Person({ name: 'Jimmy Page', type: 'musician', slug: 'ledzep-1', - tags: [ 'guitarist' ] + tags: ['guitarist'] }); p.save(function(err) { @@ -5052,9 +5042,8 @@ describe('Model', function() { Person.findById(p._id, function(err, personDoc) { assert.ifError(err); - assert.equal('Jimmy Page', personDoc.name); - assert.equal(undefined, personDoc.loc); - db.close(); + assert.equal(personDoc.name, 'Jimmy Page'); + assert.equal(personDoc.loc, undefined); done(); }); }); @@ -5062,24 +5051,23 @@ describe('Model', function() { it('Compound index on field earlier declared with 2dsphere index is saved', function(done) { - var db = start(); var PersonSchema = new Schema({ name: String, type: String, - slug: { type: String, index: { unique: true } }, - loc: { type: [Number] }, - tags: { type: [String], index: true } + slug: {type: String, index: {unique: true}}, + loc: {type: [Number]}, + tags: {type: [String], index: true} }); - PersonSchema.index({ loc: '2dsphere' }); - PersonSchema.index({ name: 1, loc: -1 }); + PersonSchema.index({loc: '2dsphere'}); + PersonSchema.index({name: 1, loc: -1}); var Person = db.model('Person_9', PersonSchema); var p = new Person({ name: 'Jimmy Page', type: 'musician', slug: 'ledzep-1', - tags: [ 'guitarist' ] + tags: ['guitarist'] }); p.save(function(err) { @@ -5088,24 +5076,71 @@ describe('Model', function() { Person.findById(p._id, function(err, personDoc) { assert.ifError(err); - assert.equal('Jimmy Page', personDoc.name); - assert.equal(undefined, personDoc.loc); - db.close(); + assert.equal(personDoc.name, 'Jimmy Page'); + assert.equal(personDoc.loc, undefined); done(); }); }); }); }); - describe('gh-1920', function() { - it('doesnt crash', function(done) { - var db = start(); - - var parentSchema = new Schema({ - children: [new Schema({ - name: String - })] - }); + it('save max bson size error with buffering (gh-3906)', function(done) { + this.timeout(10000); + var db = start({ noErrorListener: true }); + var Test = db.model('gh3906_0', { name: Object }); + + var test = new Test({ + name: { + data: (new Array(16 * 1024 * 1024)).join('x') + } + }); + + test.save(function(error) { + assert.ok(error); + assert.equal(error.toString(), + 'MongoError: document is larger than the maximum size 16777216'); + db.close(done); + }); + }); + + it('reports max bson size error in save (gh-3906)', function(done) { + this.timeout(10000); + var db = start({ noErrorListener: true }); + var Test = db.model('gh3906', { name: Object }); + + var test = new Test({ + name: { + data: (new Array(16 * 1024 * 1024)).join('x') + } + }); + + db.on('connected', function() { + test.save(function(error) { + assert.ok(error); + assert.equal(error.toString(), + 'MongoError: document is larger than the maximum size 16777216'); + db.close(done); + }); + }); + }); + + describe('bug fixes', function() { + var db; + + before(function() { + db = start({ noErrorListener: true }); + }); + + after(function(done) { + db.close(done); + }); + + it('doesnt crash (gh-1920)', function(done) { + var parentSchema = new Schema({ + children: [new Schema({ + name: String + })] + }); var Parent = db.model('gh-1920', parentSchema); @@ -5114,18 +5149,14 @@ describe('Model', function() { parent.save(function(err, it) { assert.ifError(err); parent.children.push({name: 'another child'}); - Parent.findByIdAndUpdate(it._id, { $set: { children: parent.children } }, function(err) { + Parent.findByIdAndUpdate(it._id, {$set: {children: parent.children}}, function(err) { assert.ifError(err); - db.close(done); + done(); }); }); }); - }); - describe('save failure', function() { it('doesnt reset "modified" status for fields', function(done) { - var db = start(); - var UniqueSchema = new Schema({ changer: String, unique: { @@ -5138,12 +5169,12 @@ describe('Model', function() { var u1 = new Unique({ changer: 'a', - unique: 5 + unique: 5 }); var u2 = new Unique({ changer: 'a', - unique: 6 + unique: 6 }); Unique.on('index', function() { @@ -5159,21 +5190,625 @@ describe('Model', function() { u2.save(function(err) { assert.ok(err); assert.ok(u2.isModified('changer')); - db.close(done); + done(); }); }); }); }); }); - }); - describe('gh-2442', function() { - it('marks array as modified when initializing non-array from db', function(done) { - var db = start(); + it('insertMany() (gh-723)', function(done) { + var schema = new Schema({ + name: String + }, { timestamps: true }); + var Movie = db.model('gh723', schema); + + var arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]; + Movie.insertMany(arr, function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 2); + assert.ok(!docs[0].isNew); + assert.ok(!docs[1].isNew); + assert.ok(docs[0].createdAt); + assert.ok(docs[1].createdAt); + assert.strictEqual(docs[0].__v, 0); + assert.strictEqual(docs[1].__v, 0); + Movie.find({}, function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 2); + assert.ok(docs[0].createdAt); + assert.ok(docs[1].createdAt); + done(); + }); + }); + }); + + it('insertMany() ordered option for constraint errors (gh-3893)', function(done) { + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + done(); + return; + } + + test(); + }); + + function test() { + var schema = new Schema({ + name: { type: String, unique: true } + }); + var Movie = db.model('gh3893', schema); + + var arr = [ + { name: 'Star Wars' }, + { name: 'Star Wars' }, + { name: 'The Empire Strikes Back' } + ]; + Movie.on('index', function(error) { + assert.ifError(error); + Movie.insertMany(arr, { ordered: false }, function(error) { + assert.equal(error.message.indexOf('E11000'), 0); + Movie.find({}).sort({ name: 1 }).exec(function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 2); + assert.equal(docs[0].name, 'Star Wars'); + assert.equal(docs[1].name, 'The Empire Strikes Back'); + done(); + }); + }); + }); + } + }); + + it('insertMany() ordered option for validation errors (gh-5068)', function(done) { + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + done(); + return; + } + + test(); + }); + + function test() { + var schema = new Schema({ + name: { type: String, required: true } + }); + var Movie = db.model('gh5068', schema); + + var arr = [ + { name: 'Star Wars' }, + { foo: 'Star Wars' }, + { name: 'The Empire Strikes Back' } + ]; + Movie.insertMany(arr, { ordered: false }, function(error) { + assert.ifError(error); + Movie.find({}).sort({ name: 1 }).exec(function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 2); + assert.equal(docs[0].name, 'Star Wars'); + assert.equal(docs[1].name, 'The Empire Strikes Back'); + done(); + }); + }); + } + }); + + it('insertMany() ordered option for single validation error', function(done) { + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + done(); + return; + } + + test(); + }); + + function test() { + var schema = new Schema({ + name: { type: String, required: true } + }); + var Movie = db.model('gh5068-2', schema); + + var arr = [ + { foo: 'Star Wars' }, + { foo: 'The Fast and the Furious' } + ]; + Movie.insertMany(arr, { ordered: false }, function(error) { + assert.ifError(error); + Movie.find({}).sort({ name: 1 }).exec(function(error, docs) { + assert.equal(docs.length, 0); + done(); + }); + }); + } + }); + + it('insertMany() hooks (gh-3846)', function(done) { + var schema = new Schema({ + name: String + }); + var calledPre = 0; + var calledPost = 0; + schema.pre('insertMany', function(next, docs) { + assert.equal(docs.length, 2); + assert.equal(docs[0].name, 'Star Wars'); + ++calledPre; + next(); + }); + schema.pre('insertMany', function(next, docs) { + assert.equal(docs.length, 2); + assert.equal(docs[0].name, 'Star Wars'); + docs[0].name = 'A New Hope'; + ++calledPre; + next(); + }); + schema.post('insertMany', function() { + ++calledPost; + }); + var Movie = db.model('gh3846', schema); + + var arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]; + Movie.insertMany(arr, function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 2); + assert.equal(calledPre, 2); + assert.equal(calledPost, 1); + Movie.find({}).sort({ name: 1 }).exec(function(error, docs) { + assert.ifError(error); + assert.equal(docs[0].name, 'A New Hope'); + assert.equal(docs[1].name, 'The Empire Strikes Back'); + done(); + }); + }); + }); + + it('insertMany() with timestamps (gh-723)', function(done) { + var schema = new Schema({ + name: String + }); + var Movie = db.model('gh723_0', schema); + + var arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]; + Movie.insertMany(arr, function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 2); + assert.ok(!docs[0].isNew); + assert.ok(!docs[1].isNew); + Movie.find({}, function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 2); + done(); + }); + }); + }); + + it('insertMany() multi validation error with ordered false (gh-5337)', function(done) { + var schema = new Schema({ + name: { type: String, required: true } + }); + var Movie = db.model('gh5337', schema); + + var arr = [ + { foo: 'The Phantom Menace' }, + { name: 'Star Wars' }, + { name: 'The Empire Strikes Back' }, + { foobar: 'The Force Awakens' } + ]; + var opts = { ordered: false, rawResult: true }; + Movie.insertMany(arr, opts, function(error, res) { + assert.ifError(error); + assert.equal(res.mongoose.validationErrors.length, 2); + assert.equal(res.mongoose.validationErrors[0].name, 'ValidationError'); + assert.equal(res.mongoose.validationErrors[1].name, 'ValidationError'); + done(); + }); + }); + + it('insertMany() depopulate (gh-4590)', function(done) { + var personSchema = new Schema({ + name: String + }); + var movieSchema = new Schema({ + name: String, + leadActor: { + type: Schema.Types.ObjectId, + ref: 'gh4590' + } + }); + + var Person = db.model('gh4590', personSchema); + var Movie = db.model('gh4590_0', movieSchema); + + var arnold = new Person({ name: 'Arnold Schwarzenegger' }); + var movies = [{ name: 'Predator', leadActor: arnold }]; + Movie.insertMany(movies, function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 1); + Movie.findOne({ name: 'Predator' }, function(error, doc) { + assert.ifError(error); + assert.equal(doc.leadActor.toHexString(), arnold._id.toHexString()); + done(); + }); + }); + }); + + it('insertMany() with promises (gh-4237)', function(done) { + var schema = new Schema({ + name: String + }); + var Movie = db.model('gh4237', schema); + + var arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]; + Movie.insertMany(arr).then(function(docs) { + assert.equal(docs.length, 2); + assert.ok(!docs[0].isNew); + assert.ok(!docs[1].isNew); + Movie.find({}, function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 2); + done(); + }); + }); + }); + + it('method with same name as prop should throw (gh-4475)', function(done) { + var testSchema = new mongoose.Schema({ + isPaid: Boolean + }); + testSchema.methods.isPaid = function() { + return false; + }; + var threw = false; + try { + db.model('gh4475', testSchema); + } catch (error) { + threw = true; + assert.equal(error.message, 'You have a method and a property in ' + + 'your schema both named "isPaid"'); + } + assert.ok(threw); + done(); + }); + + it('emits errors in create cb (gh-3222) (gh-3478)', function(done) { + var schema = new Schema({ name: 'String' }); + var Movie = db.model('gh3222', schema); + + Movie.on('error', function(error) { + assert.equal(error.message, 'fail!'); + done(); + }); + + Movie.create({ name: 'Conan the Barbarian' }, function(error) { + assert.ifError(error); + throw new Error('fail!'); + }); + }); + + it('create() reuses existing doc if one passed in (gh-4449)', function(done) { + var testSchema = new mongoose.Schema({ + name: String + }); + var Test = db.model('gh4449_0', testSchema); + + var t = new Test(); + Test.create(t, function(error, t2) { + assert.ifError(error); + assert.ok(t === t2); + done(); + }); + }); + + it('emits errors correctly from exec (gh-4500)', function(done) { + var someModel = db.model('gh4500', new Schema({})); + + someModel.on('error', function(error) { + assert.equal(error.message, 'This error will not disappear'); + assert.ok(cleared); + done(); + }); + + var cleared = false; + someModel.findOne().exec(function() { + setImmediate(function() { + cleared = true; + }); + throw new Error('This error will not disappear'); + }); + }); + + it('disabling id getter with .set() (gh-5548)', function(done) { + var ChildSchema = new mongoose.Schema({ + name: String, + _id: false + }); + + ChildSchema.set('id', false); + + var ParentSchema = new mongoose.Schema({ + child: { + type: ChildSchema, + default: {} + } + }, { id: false }); + + var Parent = db.model('gh5548', ParentSchema); + + var doc = new Parent({ child: { name: 'test' } }); + assert.ok(!doc.id); + assert.ok(!doc.child.id); + + var obj = doc.toObject({ virtuals: true }); + assert.ok(!('id' in obj)); + assert.ok(!('id' in obj.child)); + + done(); + }); + + it('creates new array when initializing from existing doc (gh-4449)', function(done) { + var TodoSchema = new mongoose.Schema({ + title: String + }, { _id: false }); + + var UserSchema = new mongoose.Schema({ + name: String, + todos: [TodoSchema] + }); + var User = db.model('User', UserSchema); + + var val = new User({ name: 'Val' }); + User.create(val, function(error, val) { + assert.ifError(error); + val.todos.push({ title: 'Groceries' }); + val.save(function(error) { + assert.ifError(error); + User.findById(val, function(error, val) { + assert.ifError(error); + assert.deepEqual(val.toObject().todos, [{ title: 'Groceries' }]); + var u2 = new User(); + val.todos = u2.todos; + val.todos.push({ title: 'Cook' }); + val.save(function(error) { + assert.ifError(error); + User.findById(val, function(error, val) { + assert.ifError(error); + assert.equal(val.todos.length, 1); + assert.equal(val.todos[0].title, 'Cook'); + done(); + }); + }); + }); + }); + }); + }); + + it('bulkWrite casting (gh-3998)', function(done) { + var schema = new Schema({ + str: String, + num: Number + }); + + var M = db.model('gh3998', schema); + + var ops = [ + { + insertOne: { + document: { str: 1, num: '1' } + } + }, + { + updateOne: { + filter: { str: 1 }, + update: { + $set: { num: '2' } + } + } + } + ]; + M.bulkWrite(ops, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.strictEqual(doc.str, '1'); + assert.strictEqual(doc.num, 2); + done(); + }); + }); + }); + + it('bulkWrite with setDefaultsOnInsert (gh-5708)', function(done) { + var schema = new Schema({ + str: { type: String, default: 'test' }, + num: Number + }); + + var M = db.model('gh5708', schema); + + var ops = [ + { + updateOne: { + filter: { num: 0 }, + update: { + $inc: { num: 1 } + }, + upsert: true, + setDefaultsOnInsert: true + } + } + ]; + M.bulkWrite(ops, function(error) { + assert.ifError(error); + M.findOne({}).lean().exec(function(error, doc) { + assert.ifError(error); + assert.strictEqual(doc.str, 'test'); + assert.strictEqual(doc.num, 1); + done(); + }); + }); + }); + + it('insertMany with Decimal (gh-5190)', function(done) { + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + done(); + return; + } + + test(); + }); + + function test() { + var schema = new mongoose.Schema({ + amount : mongoose.Schema.Types.Decimal + }); + var Money = db.model('gh5190', schema); + + Money.insertMany([{ amount : '123.45' }], function(error) { + assert.ifError(error); + done(); + }); + } + }); + + it('remove with cast error (gh-5323)', function(done) { + var schema = new mongoose.Schema({ + name: String + }); + + var Model = db.model('gh5323', schema); + var arr = [ + { name: 'test-1' }, + { name: 'test-2' } + ]; + + Model.create(arr, function(error) { + assert.ifError(error); + Model.remove([], function(error) { + assert.ok(error); + assert.ok(error.message.indexOf('Query filter must be an object') !== -1, + error.message); + Model.find({}, function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 2); + done(); + }); + }); + }); + }); + + it('.create() with non-object (gh-2037)', function(done) { + var schema = new mongoose.Schema({ name: String }); + + var Model = db.model('gh2037', schema); + + Model.create(1, function(error) { + assert.ok(error); + assert.equal(error.name, 'ObjectParameterError'); + done(); + }); + }); + + it('bulkWrite casting updateMany, deleteOne, deleteMany (gh-3998)', function(done) { + var schema = new Schema({ + str: String, + num: Number + }); + + var M = db.model('gh3998_0', schema); + + var ops = [ + { + insertOne: { + document: { str: 1, num: '1' } + } + }, + { + insertOne: { + document: { str: '1', num: '1' } + } + }, + { + updateMany: { + filter: { str: 1 }, + update: { + $set: { num: '2' } + } + } + }, + { + deleteMany: { + filter: { str: 1 } + } + } + ]; + M.bulkWrite(ops, function(error) { + assert.ifError(error); + M.count({}, function(error, count) { + assert.ifError(error); + assert.equal(count, 0); + done(); + }); + }); + }); + + it('bulkWrite casting replaceOne (gh-3998)', function(done) { + var schema = new Schema({ + str: String, + num: Number + }); + + var M = db.model('gh3998_1', schema); + + var ops = [ + { + insertOne: { + document: { str: 1, num: '1' } + } + }, + { + replaceOne: { + filter: { str: 1 }, + replacement: { str: 2, num: '2' } + } + } + ]; + M.bulkWrite(ops, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.strictEqual(doc.str, '2'); + assert.strictEqual(doc.num, 2); + done(); + }); + }); + }); + + it('marks array as modified when initializing non-array from db (gh-2442)', function(done) { var s1 = new Schema({ array: mongoose.Schema.Types.Mixed - }, { minimize: false }); + }, {minimize: false}); var s2 = new Schema({ array: { @@ -5191,21 +5826,21 @@ describe('Model', function() { var M1 = db.model('gh-2442-1', s1, 'gh-2442'); var M2 = db.model('gh-2442-2', s2, 'gh-2442'); - M1.create({ array: {} }, function(err, doc) { + M1.create({array: {}}, function(err, doc) { assert.ifError(err); assert.ok(doc.array); - M2.findOne({ _id: doc._id }, function(err, doc) { + M2.findOne({_id: doc._id}, function(err, doc) { assert.ifError(err); assert.equal(doc.array[0].value, 0); doc.array[0].value = 1; doc.save(function(err) { assert.ifError(err); - M2.findOne({ _id: doc._id }, function(err, doc) { + M2.findOne({_id: doc._id}, function(err, doc) { assert.ifError(err); assert.ok(!doc.isModified('array')); assert.deepEqual(doc.array[0].value, 1); - assert.equal('[{"value":1}]', JSON.stringify(doc.array)); - db.close(done); + assert.equal(JSON.stringify(doc.array), '[{"value":1}]'); + done(); }); }); }); diff --git a/test/model.translateAliases.test.js b/test/model.translateAliases.test.js new file mode 100644 index 00000000000..301b556f9fb --- /dev/null +++ b/test/model.translateAliases.test.js @@ -0,0 +1,31 @@ +/** + * Test dependencies. + */ + +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose; + +describe('model translate aliases', function() { + it('should translate correctly', function() { + var Character = mongoose.model('Character', new mongoose.Schema({ + name: { type: String, alias: '名' }, + bio: { + age: { type: Number, alias: '年齢' } + } + })); + + assert.deepEqual( + // Translate aliases + Character.translateAliases({ + '名': 'Stark', + '年齢': 30 + }), + // How translated aliases suppose to look like + { + name: 'Stark', + 'bio.age': 30 + } + ); + }); +}); diff --git a/test/model.update.test.js b/test/model.update.test.js index 8adae58b7a3..6da4b84f218 100644 --- a/test/model.update.test.js +++ b/test/model.update.test.js @@ -1,85 +1,85 @@ - /** * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ObjectId = Schema.Types.ObjectId - , DocumentObjectId = mongoose.Types.ObjectId; - -/** - * Setup. - */ - -var Comments = new Schema; - -Comments.add({ - title : String - , date : Date - , body : String - , comments : [Comments] -}); +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema, + ObjectId = Schema.Types.ObjectId, + DocumentObjectId = mongoose.Types.ObjectId; -var BlogPost = new Schema({ - title : String - , author : String - , slug : String - , date : Date - , meta : { - date : Date - , visitors : Number - } - , published : Boolean - , mixed : {} - , numbers : [Number] - , owners : [ObjectId] - , comments : [Comments] -}, { strict: false }); - -BlogPost.virtual('titleWithAuthor') - .get(function() { - return this.get('title') + ' by ' + this.get('author'); - }) - .set(function(val) { - var split = val.split(' by '); - this.set('title', split[0]); - this.set('author', split[1]); - }); +describe('model: update:', function() { + var post; + var title = 'Tobi ' + random(); + var author = 'Brian ' + random(); + var newTitle = 'Woot ' + random(); + var id0; + var id1; + var Comments; + var BlogPost; + var collection; + var strictSchema; + + before(function() { + Comments = new Schema({}); + + Comments.add({ + title: String, + date: Date, + body: String, + comments: [Comments] + }); -BlogPost.method('cool', function() { - return this; -}); + BlogPost = new Schema({ + title: String, + author: String, + slug: String, + date: Date, + meta: { + date: Date, + visitors: Number + }, + published: Boolean, + mixed: {}, + numbers: [Number], + owners: [ObjectId], + comments: [Comments] + }, {strict: false}); -BlogPost.static('woot', function() { - return this; -}); + BlogPost.virtual('titleWithAuthor') + .get(function() { + return this.get('title') + ' by ' + this.get('author'); + }) + .set(function(val) { + var split = val.split(' by '); + this.set('title', split[0]); + this.set('author', split[1]); + }); -mongoose.model('BlogPostForUpdates', BlogPost); + BlogPost.method('cool', function() { + return this; + }); -var collection = 'blogposts_' + random(); + BlogPost.static('woot', function() { + return this; + }); -var strictSchema = new Schema({ name: String, x: { nested: String }}); -strictSchema.virtual('foo').get(function() { - return 'i am a virtual FOO!'; -}); -mongoose.model('UpdateStrictSchema', strictSchema); + mongoose.model('BlogPostForUpdates', BlogPost); + collection = 'blogposts_' + random(); -describe('model: update:', function() { - var post - , title = 'Tobi ' + random() - , author = 'Brian ' + random() - , newTitle = 'Woot ' + random() - , id0 - , id1; + strictSchema = new Schema({name: String, x: {nested: String}}); + strictSchema.virtual('foo').get(function() { + return 'i am a virtual FOO!'; + }); + mongoose.model('UpdateStrictSchema', strictSchema); + }); before(function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); id0 = new DocumentObjectId; id1 = new DocumentObjectId; @@ -90,10 +90,10 @@ describe('model: update:', function() { post.meta.visitors = 0; post.date = new Date; post.published = true; - post.mixed = { x: 'ex' }; - post.numbers = [4,5,6,7]; + post.mixed = {x: 'ex'}; + post.numbers = [4, 5, 6, 7]; post.owners = [id0, id1]; - post.comments = [{ body: 'been there' }, { body: 'done that' }]; + post.comments = [{body: 'been there'}, {body: 'done that'}]; post.save(function(err) { assert.ifError(err); @@ -102,18 +102,18 @@ describe('model: update:', function() { }); it('works', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); BlogPost.findById(post._id, function(err, cf) { assert.ifError(err); - assert.equal(title, cf.title); - assert.equal(cf.author,author); - assert.equal(cf.meta.visitors.valueOf(),0); + assert.equal(cf.title, title); + assert.equal(cf.author, author); + assert.equal(cf.meta.visitors.valueOf(), 0); assert.equal(cf.date.toString(), post.date.toString()); - assert.equal(true, cf.published); - assert.equal('ex', cf.mixed.x); - assert.deepEqual(cf.numbers.toObject(), [4,5,6,7]); + assert.equal(cf.published, true); + assert.equal(cf.mixed.x, 'ex'); + assert.deepEqual(cf.numbers.toObject(), [4, 5, 6, 7]); assert.equal(cf.owners.length, 2); assert.equal(cf.owners[0].toString(), id0.toString()); assert.equal(cf.owners[1].toString(), id1.toString()); @@ -126,33 +126,33 @@ describe('model: update:', function() { assert.ok(cf.comments[1]._id instanceof DocumentObjectId); var update = { - title: newTitle // becomes $set - , $inc: { 'meta.visitors': 2 } - , $set: { date: new Date } - , published: false // becomes $set - , 'mixed': { x: 'ECKS', y: 'why' } // $set - , $pullAll: { 'numbers': [4, 6] } - , $pull: { 'owners': id0 } - , 'comments.1.body': 8 // $set + title: newTitle, // becomes $set + $inc: {'meta.visitors': 2}, + $set: {date: new Date}, + published: false, // becomes $set + mixed: {x: 'ECKS', y: 'why'}, // $set + $pullAll: {numbers: [4, 6]}, + $pull: {owners: id0}, + 'comments.1.body': 8 // $set }; - BlogPost.update({ title: title }, update, function(err) { + BlogPost.update({title: title}, update, function(err) { assert.ifError(err); BlogPost.findById(post._id, function(err, up) { assert.ifError(err); - assert.equal(up.title,newTitle); - assert.equal(up.author,author); + assert.equal(up.title, newTitle); + assert.equal(up.author, author); assert.equal(up.meta.visitors.valueOf(), 2); - assert.equal(up.date.toString(),update.$set.date.toString()); + assert.equal(up.date.toString(), update.$set.date.toString()); assert.equal(up.published, false); assert.equal(up.mixed.x, 'ECKS'); - assert.equal(up.mixed.y,'why'); - assert.deepEqual(up.numbers.toObject(), [5,7]); + assert.equal(up.mixed.y, 'why'); + assert.deepEqual(up.numbers.toObject(), [5, 7]); assert.equal(up.owners.length, 1); assert.equal(up.owners[0].toString(), id1.toString()); - assert.equal(up.comments[0].body,'been there'); - assert.equal(up.comments[1].body,'8'); + assert.equal(up.comments[0].body, 'been there'); + assert.equal(up.comments[1].body, '8'); assert.ok(up.comments[0]._id); assert.ok(up.comments[1]._id); assert.ok(up.comments[0]._id instanceof DocumentObjectId); @@ -162,7 +162,7 @@ describe('model: update:', function() { 'comments.body': 'fail' }; - BlogPost.update({ _id: post._id }, update2, function(err) { + BlogPost.update({_id: post._id}, update2, function(err) { assert.ok(err); assert.ok(err.message.length > 0); BlogPost.findById(post, function(err) { @@ -172,30 +172,30 @@ describe('model: update:', function() { $pull: 'fail' }; - BlogPost.update({ _id: post._id }, update3, function(err) { + BlogPost.update({_id: post._id}, update3, function(err) { assert.ok(err); assert.ok(/Invalid atomic update value for \$pull\. Expected an object, received string/.test(err.message)); var update4 = { - $inc: { idontexist: 1 } + $inc: {idontexist: 1} }; // should not overwrite doc when no valid paths are submitted - BlogPost.update({ _id: post._id }, update4, function(err) { + BlogPost.update({_id: post._id}, update4, function(err) { assert.ifError(err); BlogPost.findById(post._id, function(err, up) { assert.ifError(err); - assert.equal(up.title,newTitle); - assert.equal(up.author,author); - assert.equal(up.meta.visitors.valueOf(),2); - assert.equal(up.date.toString(),update.$set.date.toString()); - assert.equal(up.published,false); - assert.equal(up.mixed.x,'ECKS'); - assert.equal(up.mixed.y,'why'); - assert.deepEqual(up.numbers.toObject(),[5,7]); + assert.equal(up.title, newTitle); + assert.equal(up.author, author); + assert.equal(up.meta.visitors.valueOf(), 2); + assert.equal(up.date.toString(), update.$set.date.toString()); + assert.equal(up.published, false); + assert.equal(up.mixed.x, 'ECKS'); + assert.equal(up.mixed.y, 'why'); + assert.deepEqual(up.numbers.toObject(), [5, 7]); assert.equal(up.owners.length, 1); assert.equal(up.owners[0].toString(), id1.toString()); assert.equal(up.comments[0].body, 'been there'); @@ -215,20 +215,20 @@ describe('model: update:', function() { }); it('casts doc arrays', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); var update = { - comments: [{ body: 'worked great' }] - , $set: {'numbers.1': 100} - , $inc: { idontexist: 1 } + comments: [{body: 'worked great'}], + $set: {'numbers.1': 100}, + $inc: {idontexist: 1} }; - BlogPost.update({ _id: post._id }, update, function(err) { + BlogPost.update({_id: post._id}, update, function(err) { assert.ifError(err); // get the underlying doc - BlogPost.collection.findOne({ _id: post._id }, function(err, doc) { + BlogPost.collection.findOne({_id: post._id}, function(err, doc) { assert.ifError(err); var up = new BlogPost; @@ -236,13 +236,13 @@ describe('model: update:', function() { assert.equal(up.comments.length, 1); assert.equal(up.comments[0].body, 'worked great'); assert.strictEqual(true, !!doc.comments[0]._id); - assert.equal(2,up.meta.visitors.valueOf()); - assert.equal(up.mixed.x,'ECKS'); - assert.deepEqual(up.numbers.toObject(),[5,100]); - assert.strictEqual(up.numbers[1].valueOf(),100); + assert.equal(up.meta.visitors.valueOf(), 2); + assert.equal(up.mixed.x, 'ECKS'); + assert.deepEqual(up.numbers.toObject(), [5, 100]); + assert.strictEqual(up.numbers[1].valueOf(), 100); - assert.equal(2, doc.idontexist); - assert.equal(100, doc.numbers[1]); + assert.equal(doc.idontexist, 2); + assert.equal(doc.numbers[1], 100); db.close(done); }); @@ -250,22 +250,22 @@ describe('model: update:', function() { }); it('handles $pushAll array of docs', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); var update = { - $pushAll: { comments: [{ body: 'i am number 2' }, { body: 'i am number 3' }] } + $pushAll: {comments: [{body: 'i am number 2'}, {body: 'i am number 3'}]} }; - BlogPost.update({ _id: post._id }, update, function(err) { + BlogPost.update({_id: post._id}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(3, ret.comments.length); - assert.equal(ret.comments[1].body,'i am number 2'); + assert.equal(ret.comments.length, 3); + assert.equal(ret.comments[1].body, 'i am number 2'); assert.strictEqual(true, !!ret.comments[1]._id); assert.ok(ret.comments[1]._id instanceof DocumentObjectId); - assert.equal(ret.comments[2].body,'i am number 3'); + assert.equal(ret.comments[2].body, 'i am number 3'); assert.strictEqual(true, !!ret.comments[2]._id); assert.ok(ret.comments[2]._id instanceof DocumentObjectId); db.close(done); @@ -274,21 +274,21 @@ describe('model: update:', function() { }); it('handles $pull of object literal array of docs (gh-542)', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); var update = { - $pull: { comments: { body: 'i am number 2' } } + $pull: {comments: {body: 'i am number 2'}} }; - BlogPost.update({ _id: post._id }, update, function(err) { + BlogPost.update({_id: post._id}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(2, ret.comments.length); - assert.equal(ret.comments[0].body,'worked great'); + assert.equal(ret.comments.length, 2); + assert.equal(ret.comments[0].body, 'worked great'); assert.ok(ret.comments[0]._id instanceof DocumentObjectId); - assert.equal(ret.comments[1].body,'i am number 3'); + assert.equal(ret.comments[1].body, 'i am number 3'); assert.ok(ret.comments[1]._id instanceof DocumentObjectId); db.close(done); }); @@ -296,44 +296,48 @@ describe('model: update:', function() { }); it('makes copy of conditions and update options', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); - var conditions = { '_id': post._id.toString() }; - var update = {'$set':{'some_attrib':post._id.toString()}}; + var conditions = {'_id': post._id.toString()}; + var update = {'$set': {'some_attrib': post._id.toString()}}; BlogPost.update(conditions, update, function(err) { assert.ifError(err); - assert.equal('string', typeof conditions._id); + assert.equal(typeof conditions._id, 'string'); db.close(done); }); }); it('handles weird casting (gh-479)', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); + + function a() { + } - function a() {} - a.prototype.toString = function() { return 'MongoDB++'; }; + a.prototype.toString = function() { + return 'MongoDB++'; + }; var crazy = new a; var update = { - $addToSet: { 'comments.$.comments': { body: 'The Ring Of Power' } } - , $set: { 'comments.$.title': crazy } + $addToSet: {'comments.$.comments': {body: 'The Ring Of Power'}}, + $set: {'comments.$.title': crazy} }; - BlogPost.update({ _id: post._id, 'comments.body': 'worked great' }, update, function(err) { + BlogPost.update({_id: post._id, 'comments.body': 'worked great'}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(2, ret.comments.length); + assert.equal(ret.comments.length, 2); assert.equal(ret.comments[0].body, 'worked great'); - assert.equal(ret.comments[0].title,'MongoDB++'); + assert.equal(ret.comments[0].title, 'MongoDB++'); assert.strictEqual(true, !!ret.comments[0].comments); assert.equal(ret.comments[0].comments.length, 1); assert.strictEqual(ret.comments[0].comments[0].body, 'The Ring Of Power'); assert.ok(ret.comments[0]._id instanceof DocumentObjectId); assert.ok(ret.comments[0].comments[0]._id instanceof DocumentObjectId); - assert.equal(ret.comments[1].body,'i am number 3'); + assert.equal(ret.comments[1].body, 'i am number 3'); assert.strictEqual(undefined, ret.comments[1].title); assert.ok(ret.comments[1]._id instanceof DocumentObjectId); db.close(done); @@ -343,19 +347,19 @@ describe('model: update:', function() { var last; it('handles date casting (gh-479)', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); var update = { - $inc: { 'comments.$.newprop': '1' } - , $set: { date: (new Date).getTime() } // check for single val casting + $inc: {'comments.$.newprop': '1'}, + $set: {date: (new Date).getTime()} // check for single val casting }; - BlogPost.update({ _id: post._id, 'comments.body': 'worked great' }, update, function(err) { + BlogPost.update({_id: post._id, 'comments.body': 'worked great'}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(1, ret._doc.comments[0]._doc.newprop); + assert.equal(ret._doc.comments[0]._doc.newprop, 1); assert.strictEqual(undefined, ret._doc.comments[1]._doc.newprop); assert.ok(ret.date instanceof Date); assert.equal(ret.date.toString(), new Date(update.$set.date).toString()); @@ -367,20 +371,20 @@ describe('model: update:', function() { }); it('handles $addToSet (gh-545)', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); var owner = last.owners[0]; var update = { - $addToSet: { 'owners': owner } + $addToSet: {owners: owner} }; - BlogPost.update({ _id: post._id }, update, function(err) { + BlogPost.update({_id: post._id}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(1, ret.owners.length); + assert.equal(ret.owners.length, 1); assert.equal(ret.owners[0].toString(), owner.toString()); last = ret; @@ -390,21 +394,21 @@ describe('model: update:', function() { }); it('handles $addToSet with $each (gh-545)', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); - var owner = last.owners[0] - , newowner = new DocumentObjectId; + var owner = last.owners[0], + newowner = new DocumentObjectId; var update = { - $addToSet: { 'owners': { $each: [owner, newowner] }} + $addToSet: {owners: {$each: [owner, newowner]}} }; - BlogPost.update({ _id: post._id }, update, function(err) { + BlogPost.update({_id: post._id}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(2, ret.owners.length); + assert.equal(ret.owners.length, 2); assert.equal(ret.owners[0].toString(), owner.toString()); assert.equal(ret.owners[1].toString(), newowner.toString()); @@ -415,20 +419,20 @@ describe('model: update:', function() { }); it('handles $pop and $unset (gh-574)', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); var update = { - $pop: { 'owners': -1 } - , $unset: { title: 1 } + $pop: {owners: -1}, + $unset: {title: 1} }; - BlogPost.update({ _id: post._id }, update, function(err) { + BlogPost.update({_id: post._id}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(1, ret.owners.length); + assert.equal(ret.owners.length, 1); assert.equal(ret.owners[0].toString(), last.toString()); assert.strictEqual(undefined, ret.title); db.close(done); @@ -437,21 +441,21 @@ describe('model: update:', function() { }); it('works with nested positional notation', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); var update = { $set: { - 'comments.0.comments.0.date': '11/5/2011' - , 'comments.1.body': 9000 + 'comments.0.comments.0.date': '11/5/2011', + 'comments.1.body': 9000 } }; - BlogPost.update({ _id: post._id }, update, function(err) { + BlogPost.update({_id: post._id}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(2, ret.comments.length, 2); + assert.equal(ret.comments.length, 2, 2); assert.equal(ret.comments[0].body, 'worked great'); assert.equal(ret.comments[1].body, '9000'); assert.equal(ret.comments[0].comments[0].date.toString(), new Date('11/5/2011').toString()); @@ -462,21 +466,22 @@ describe('model: update:', function() { }); it('handles $pull with obj literal (gh-542)', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(); + var BlogPost = db.model('BlogPostForUpdates', collection); - BlogPost.findById(post, function(err, last) { + + BlogPost.findById(post, function(err, doc) { assert.ifError(err); var update = { - $pull: { comments: { _id: last.comments[0].id } } + $pull: {comments: {_id: doc.comments[0].id}} }; - BlogPost.update({ _id: post._id }, update, function(err) { + BlogPost.update({_id: post._id}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(1, ret.comments.length); + assert.equal(ret.comments.length, 1); assert.equal(ret.comments[0].body, '9000'); db.close(done); }); @@ -485,20 +490,20 @@ describe('model: update:', function() { }); it('handles $pull of obj literal and nested $in', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); BlogPost.findById(post, function(err, last) { assert.ifError(err); var update = { - $pull: { comments: { body: { $in: [last.comments[0].body] }} } + $pull: {comments: {body: {$in: [last.comments[0].body]}}} }; - BlogPost.update({ _id: post._id }, update, function(err) { + BlogPost.update({_id: post._id}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(0, ret.comments.length); + assert.equal(ret.comments.length, 0); last = ret; db.close(done); @@ -508,28 +513,28 @@ describe('model: update:', function() { }); it('handles $pull and nested $nin', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection); + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection); BlogPost.findById(post, function(err, last) { assert.ifError(err); - last.comments.push({body: 'hi'}, {body:'there'}); + last.comments.push({body: 'hi'}, {body: 'there'}); last.save(function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(2, ret.comments.length); + assert.equal(ret.comments.length, 2); var update = { - $pull: { comments: { body: { $nin: ['there'] }} } + $pull: {comments: {body: {$nin: ['there']}}} }; - BlogPost.update({ _id: ret._id }, update, function(err) { + BlogPost.update({_id: ret._id}, update, function(err) { assert.ifError(err); BlogPost.findById(post, function(err, ret) { assert.ifError(err); - assert.equal(1, ret.comments.length); + assert.equal(ret.comments.length, 1); db.close(done); }); }); @@ -539,31 +544,31 @@ describe('model: update:', function() { }); it('updates numbers atomically', function(done) { - var db = start() - , BlogPost = db.model('BlogPostForUpdates', collection) - , totalDocs = 4; + var db = start(), + BlogPost = db.model('BlogPostForUpdates', collection), + totalDocs = 4; var post = new BlogPost; post.set('meta.visitors', 5); + function complete() { + BlogPost.findOne({_id: post.get('_id')}, function(err, doc) { + db.close(); + assert.ifError(err); + assert.equal(doc.get('meta.visitors'), 9); + done(); + }); + } + post.save(function(err) { assert.ifError(err); - + function callback(err) { + assert.ifError(err); + --totalDocs || complete(); + } for (var i = 0; i < 4; ++i) { BlogPost - .update({ _id: post._id }, { $inc: { 'meta.visitors': 1 }}, function(err) { - assert.ifError(err); - --totalDocs || complete(); - }); - } - - function complete() { - BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) { - db.close(); - assert.ifError(err); - assert.equal(9, doc.get('meta.visitors')); - done(); - }); + .update({_id: post._id}, {$inc: {'meta.visitors': 1}}, callback); } }); }); @@ -573,33 +578,33 @@ describe('model: update:', function() { var db = start(); var S = db.model('UpdateStrictSchema'); - var doc = S.find()._castUpdate({ ignore: true }); - assert.equal(false, doc); - doc = S.find()._castUpdate({ $unset: {x: 1}}); - assert.equal(1, Object.keys(doc.$unset).length); + var doc = S.find()._castUpdate({ignore: true}); + assert.equal(doc, false); + doc = S.find()._castUpdate({$unset: {x: 1}}); + assert.equal(Object.keys(doc.$unset).length, 1); db.close(done); }); it('works', function(done) { var db = start(); var S = db.model('UpdateStrictSchema'); - var s = new S({ name: 'orange crush' }); + var s = new S({name: 'orange crush'}); s.save(function(err) { assert.ifError(err); - S.update({ _id: s._id }, { ignore: true }, function(err, affected) { + S.update({_id: s._id}, {ignore: true}, function(err, affected) { assert.ifError(err); - assert.equal(0, affected.n); + assert.equal(affected.n, 0); S.findById(s._id, function(err, doc) { assert.ifError(err); assert.ok(!doc.ignore); assert.ok(!doc._doc.ignore); - S.update({ _id: s._id }, { name: 'Drukqs', foo: 'fooey' }, function(err, affected) { + S.update({_id: s._id}, {name: 'Drukqs', foo: 'fooey'}, function(err, affected) { assert.ifError(err); - assert.equal(1, affected.n); + assert.equal(affected.n, 1); S.findById(s._id, function(err, doc) { db.close(); @@ -615,15 +620,15 @@ describe('model: update:', function() { }); it('passes number of affected docs', function(done) { - var db = start() - , B = db.model('BlogPostForUpdates', 'wwwwowowo' + random()); + var db = start(), + B = db.model('BlogPostForUpdates', 'wwwwowowo' + random()); - B.create({ title: 'one'},{title:'two'},{title:'three'}, function(err) { + B.create({title: 'one'}, {title: 'two'}, {title: 'three'}, function(err) { assert.ifError(err); - B.update({}, { title: 'newtitle' }, { multi: true }, function(err, affected) { + B.update({}, {title: 'newtitle'}, {multi: true}, function(err, affected) { db.close(); assert.ifError(err); - assert.equal(3, affected.n); + assert.equal(affected.n, 3); done(); }); }); @@ -632,14 +637,14 @@ describe('model: update:', function() { it('updates a number to null (gh-640)', function(done) { var db = start(); var B = db.model('BlogPostForUpdates', 'wwwwowowo' + random()); - var b = new B({ meta: { visitors: null }}); + var b = new B({meta: {visitors: null}}); b.save(function(err) { assert.ifError(err); B.findById(b, function(err, b) { assert.ifError(err); assert.strictEqual(b.meta.visitors, null); - B.update({ _id: b._id }, { meta: { visitors: null }}, function(err) { + B.update({_id: b._id}, {meta: {visitors: null}}, function(err) { db.close(); assert.strictEqual(null, err); done(); @@ -650,13 +655,13 @@ describe('model: update:', function() { it('handles $pull from Mixed arrays (gh-735)', function(done) { var db = start(); - var schema = new Schema({ comments: [] }); + var schema = new Schema({comments: []}); var M = db.model('gh-735', schema, 'gh-735_' + random()); - M.create({ comments: [{ name: 'node 0.8' }] }, function(err, doc) { + M.create({comments: [{name: 'node 0.8'}]}, function(err, doc) { assert.ifError(err); - M.update({ _id: doc._id }, { $pull: { comments: { name: 'node 0.8' }}}, function(err, affected) { + M.update({_id: doc._id}, {$pull: {comments: {name: 'node 0.8'}}}, function(err, affected) { assert.ifError(err); - assert.equal(1, affected.n); + assert.equal(affected.n, 1); db.close(); done(); }); @@ -671,55 +676,54 @@ describe('model: update:', function() { }); var componentSchema = new Schema({ - name: String - , tasks: [taskSchema] + name: String, + tasks: [taskSchema] }); var projectSchema = new Schema({ - name: String - , components: [componentSchema] + name: String, + components: [componentSchema] }); var Project = db.model('1057-project', projectSchema, '1057-' + random()); - Project.create({ name: 'my project' }, function(err, project) { + Project.create({name: 'my project'}, function(err, project) { assert.ifError(err); var pid = project.id; - var comp = project.components.create({ name: 'component' }); - Project.update({ _id: pid }, { $push: { components: comp }}, function(err) { + var comp = project.components.create({name: 'component'}); + Project.update({_id: pid}, {$push: {components: comp}}, function(err) { assert.ifError(err); - var task = comp.tasks.create({ name: 'my task' }); - Project.update({ _id: pid, 'components._id': comp._id }, { $push : { 'components.$.tasks': task }}, function(err) { + var task = comp.tasks.create({name: 'my task'}); + Project.update({_id: pid, 'components._id': comp._id}, {$push: {'components.$.tasks': task}}, function(err) { assert.ifError(err); Project.findById(pid, function(err, proj) { assert.ifError(err); assert.ok(proj); - assert.equal(1, proj.components.length); - assert.equal('component', proj.components[0].name); + assert.equal(proj.components.length, 1); + assert.equal(proj.components[0].name, 'component'); assert.equal(comp.id, proj.components[0].id); - assert.equal(1, proj.components[0].tasks.length); - assert.equal('my task', proj.components[0].tasks[0].name); + assert.equal(proj.components[0].tasks.length, 1); + assert.equal(proj.components[0].tasks[0].name, 'my task'); assert.equal(task.id, proj.components[0].tasks[0].id); db.close(done); }); }); }); }); - }); it('handles nested paths starting with numbers (gh-1062)', function(done) { var db = start(); - var schema = Schema({ counts: Schema.Types.Mixed }); + var schema = new Schema({counts: Schema.Types.Mixed}); var M = db.model('gh-1062', schema, '1062-' + random()); - M.create({ counts: {} }, function(err, m) { + M.create({counts: {}}, function(err, m) { assert.ifError(err); - M.update({}, { $inc: { 'counts.1': 1, 'counts.1a': 10 }}, function(err) { + M.update({}, {$inc: {'counts.1': 1, 'counts.1a': 10}}, function(err) { assert.ifError(err); M.findById(m, function(err, doc) { assert.ifError(err); - assert.equal(1, doc.counts['1']); - assert.equal(10, doc.counts['1a']); + assert.equal(doc.counts['1'], 1); + assert.equal(doc.counts['1a'], 10); db.close(done); }); }); @@ -729,13 +733,13 @@ describe('model: update:', function() { it('handles positional operators with referenced docs (gh-1572)', function(done) { var db = start(); - var so = new Schema({ title : String, obj : [String] }); + var so = new Schema({title: String, obj: [String]}); var Some = db.model('Some' + random(), so); - Some.create({ obj: ['a','b','c'] }, function(err, s) { + Some.create({obj: ['a', 'b', 'c']}, function(err, s) { assert.ifError(err); - Some.update({ _id: s._id, obj: 'b' }, { $set: { "obj.$" : 2 }}, function(err) { + Some.update({_id: s._id, obj: 'b'}, {$set: {'obj.$': 2}}, function(err) { assert.ifError(err); Some.findById(s._id, function(err, ss) { @@ -750,10 +754,10 @@ describe('model: update:', function() { it('use .where for update condition (gh-2170)', function(done) { var db = start(); - var so = new Schema({ num : Number }); + var so = new Schema({num: Number}); var Some = db.model('gh-2170' + random(), so); - Some.create([ {num: 1}, {num: 1} ], function(err, docs) { + Some.create([{num: 1}, {num: 1}], function(err, docs) { assert.ifError(err); assert.equal(docs.length, 2); var doc0 = docs[0]; @@ -762,13 +766,13 @@ describe('model: update:', function() { var sId1 = doc1._id; Some.where({_id: sId0}).update({}, {$set: {num: '99'}}, {multi: true}, function(err, cnt) { assert.ifError(err); - assert.equal(1, cnt.n); + assert.equal(cnt.n, 1); Some.findById(sId0, function(err, doc0_1) { assert.ifError(err); - assert.equal(99, doc0_1.num); + assert.equal(doc0_1.num, 99); Some.findById(sId1, function(err, doc1_1) { assert.ifError(err); - assert.equal(1, doc1_1.num); + assert.equal(doc1_1.num, 1); db.close(done); }); }); @@ -782,7 +786,7 @@ describe('model: update:', function() { before(function(done) { start.mongodVersion(function(err, version) { assert.ifError(err); - mongo24_or_greater = 2 < version[0] || (2 == version[0] && 4 <= version[1]); + mongo24_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 4); done(); }); }); @@ -794,26 +798,26 @@ describe('model: update:', function() { } var db = start(); - var schema = Schema({ name: String, age: Number, x: String }); + var schema = new Schema({name: String, age: Number, x: String}); var M = db.model('setoninsert-' + random(), schema); - var match = { name: 'set on insert' }; - var op = { $setOnInsert: { age: '47' }, x: 'inserted' }; - M.update(match, op, { upsert: true }, function(err) { + var match = {name: 'set on insert'}; + var op = {$setOnInsert: {age: '47'}, x: 'inserted'}; + M.update(match, op, {upsert: true}, function(err) { assert.ifError(err); M.findOne(function(err, doc) { assert.ifError(err); - assert.equal(47, doc.age); - assert.equal('set on insert', doc.name); + assert.equal(doc.age, 47); + assert.equal(doc.name, 'set on insert'); - var match = { name: 'set on insert' }; - var op = { $setOnInsert: { age: 108 }, name: 'changed' }; - M.update(match, op, { upsert: true }, function(err) { + var match = {name: 'set on insert'}; + var op = {$setOnInsert: {age: 108}, name: 'changed'}; + M.update(match, op, {upsert: true}, function(err) { assert.ifError(err); M.findOne(function(err, doc) { - assert.equal(47, doc.age); - assert.equal('changed', doc.name); + assert.equal(doc.age, 47); + assert.equal(doc.name, 'changed'); db.close(done); }); }); @@ -828,25 +832,29 @@ describe('model: update:', function() { } var db = start(); - var schema = Schema({ name: String, n: [{ x: Number }] }); + var schema = new Schema({name: String, n: [{x: Number}]}); var M = db.model('setoninsert-' + random(), schema); - M.create({ name: '2.4' }, function(err, created) { + M.create({name: '2.4'}, function(err, created) { assert.ifError(err); - var op = { $push: { n: { - $each: [{x:10},{x:4}, {x:1}] - , $slice: '-1' - , $sort: { x:1 } - }}}; + var op = { + $push: { + n: { + $each: [{x: 10}, {x: 4}, {x: 1}], + $slice: '-1', + $sort: {x: 1} + } + } + }; - M.update({ _id: created._id }, op, function(err) { + M.update({_id: created._id}, op, function(err) { assert.ifError(err); M.findById(created._id, function(err, doc) { assert.ifError(err); assert.equal(created.id, doc.id); - assert.equal(1, doc.n.length); - assert.equal(10, doc.n[0].x); + assert.equal(doc.n.length, 1); + assert.equal(doc.n[0].x, 10); op = { $push: { @@ -856,7 +864,7 @@ describe('model: update:', function() { } } }; - M.update({ _id: created._id }, op, function(err) { + M.update({_id: created._id}, op, function(err) { assert.ifError(err); M.findById(created._id, function(error, doc) { assert.ifError(error); @@ -876,7 +884,7 @@ describe('model: update:', function() { before(function(done) { start.mongodVersion(function(err, version) { assert.ifError(err); - mongo26_or_greater = 2 < version[0] || (2 == version[0] && 6 <= version[1]); + mongo26_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 6); done(); }); }); @@ -887,27 +895,27 @@ describe('model: update:', function() { } var db = start(); - var schema = Schema({ name: String, n: [{ x: Number }] }); + var schema = new Schema({name: String, n: [{x: Number}]}); var M = db.model('setoninsert-' + random(), schema); - var m = new M({ name: '2.6', n: [{ x : 0 }] }); + var m = new M({name: '2.6', n: [{x: 0}]}); m.save(function(error, m) { assert.ifError(error); - assert.equal(1, m.n.length); + assert.equal(m.n.length, 1); M.update( - { name: '2.6' }, - { $push: { n: { $each: [{x: 2}, {x: 1}], $position: 0 } } }, - function(error) { - assert.ifError(error); - M.findOne({ name: '2.6' }, function(error, m) { - assert.ifError(error); - assert.equal(3, m.n.length); - assert.equal(2, m.n[0].x); - assert.equal(1, m.n[1].x); - assert.equal(0, m.n[2].x); - db.close(done); - }); - }); + {name: '2.6'}, + {$push: {n: {$each: [{x: 2}, {x: 1}], $position: 0}}}, + function(error) { + assert.ifError(error); + M.findOne({name: '2.6'}, function(error, m) { + assert.ifError(error); + assert.equal(m.n.length, 3); + assert.equal(m.n[0].x, 2); + assert.equal(m.n[1].x, 1); + assert.equal(m.n[2].x, 0); + db.close(done); + }); + }); }); }); @@ -917,40 +925,40 @@ describe('model: update:', function() { } var db = start(); - var schema = Schema({ name: String, lastModified: Date, lastModifiedTS: Date }); + var schema = new Schema({name: String, lastModified: Date, lastModifiedTS: Date}); var M = db.model('gh-2019', schema); - var m = new M({ name: '2.6' }); + var m = new M({name: '2.6'}); m.save(function(error) { assert.ifError(error); var before = Date.now(); M.update( - { name: '2.6' }, - { $currentDate: { lastModified: true, lastModifiedTS: { $type: 'timestamp' } } }, - function(error) { - assert.ifError(error); - M.findOne({ name: '2.6' }, function(error, m) { - var after = Date.now(); - assert.ifError(error); - assert.ok(m.lastModified.getTime() >= before); - assert.ok(m.lastModified.getTime() <= after); - db.close(done); - }); - }); + {name: '2.6'}, + {$currentDate: {lastModified: true, lastModifiedTS: {$type: 'timestamp'}}}, + function(error) { + assert.ifError(error); + M.findOne({name: '2.6'}, function(error, m) { + var after = Date.now(); + assert.ifError(error); + assert.ok(m.lastModified.getTime() >= before); + assert.ok(m.lastModified.getTime() <= after); + db.close(done); + }); + }); }); }); }); - describe('{overwrite : true}', function() { + describe('{overwrite: true}', function() { it('overwrite works', function(done) { var db = start(); - var schema = new Schema({ mixed: {} }); + var schema = new Schema({mixed: {}}); var M = db.model('updatesmixed-' + random(), schema); - M.create({ mixed: 'something' }, function(err, created) { + M.create({mixed: 'something'}, function(err, created) { assert.ifError(err); - M.update({ _id: created._id }, { mixed: {} }, { overwrite : true }, function(err) { + M.update({_id: created._id}, {mixed: {}}, {overwrite: true}, function(err) { assert.ifError(err); M.findById(created._id, function(err, doc) { assert.ifError(err); @@ -965,14 +973,14 @@ describe('model: update:', function() { it('overwrites all properties', function(done) { var db = start(); - var sch = new Schema({ title : String, subdoc : { name : String, num : Number }}); + var sch = new Schema({title: String, subdoc: {name: String, num: Number}}); var M = db.model('updateover' + random(), sch); - M.create({ subdoc : { name : 'that', num : 1 } }, function(err, doc) { + M.create({subdoc: {name: 'that', num: 1}}, function(err, doc) { assert.ifError(err); - M.update({ _id : doc.id }, { title : 'something!' }, { overwrite : true }, function(err) { + M.update({_id: doc.id}, {title: 'something!'}, {overwrite: true}, function(err) { assert.ifError(err); M.findById(doc.id, function(err, doc) { assert.ifError(err); @@ -987,14 +995,14 @@ describe('model: update:', function() { it('allows users to blow it up', function(done) { var db = start(); - var sch = new Schema({ title : String, subdoc : { name : String, num : Number }}); + var sch = new Schema({title: String, subdoc: {name: String, num: Number}}); var M = db.model('updateover' + random(), sch); - M.create({ subdoc : { name : 'that', num : 1, title : 'hello' } }, function(err, doc) { + M.create({subdoc: {name: 'that', num: 1, title: 'hello'}}, function(err, doc) { assert.ifError(err); - M.update({ _id : doc.id }, {}, { overwrite : true }, function(err) { + M.update({_id: doc.id}, {}, {overwrite: true}, function(err) { assert.ifError(err); M.findById(doc.id, function(err, doc) { assert.ifError(err); @@ -1011,16 +1019,22 @@ describe('model: update:', function() { it('casts empty arrays', function(done) { var db = start(); - var so = new Schema({ arr: [] }); + var so = new Schema({arr: []}); var Some = db.model('1838-' + random(), so); - Some.create({ arr: ['a'] }, function(err, s) { - if (err) return done(err); + Some.create({arr: ['a']}, function(err, s) { + if (err) { + return done(err); + } - Some.update({ _id: s._id }, { arr: [] }, function(err) { - if (err) return done(err); + Some.update({_id: s._id}, {arr: []}, function(err) { + if (err) { + return done(err); + } Some.findById(s._id, function(err, doc) { - if (err) return done(err); + if (err) { + return done(err); + } assert.ok(Array.isArray(doc.arr)); assert.strictEqual(0, doc.arr.length); db.close(done); @@ -1030,92 +1044,143 @@ describe('model: update:', function() { }); describe('defaults and validators (gh-860)', function() { - it('applies defaults on upsert', function(done) { - var db = start(); + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); - var s = new Schema({ topping: { type: String, default: 'bacon' }, base: String }); + it('applies defaults on upsert', function(done) { + var s = new Schema({topping: {type: String, default: 'bacon'}, base: String}); var Breakfast = db.model('gh-860-0', s); - var updateOptions = { upsert: true, setDefaultsOnInsert: true }; - Breakfast.update({}, { base: 'eggs' }, updateOptions, function(error) { + var updateOptions = {upsert: true, setDefaultsOnInsert: true}; + Breakfast.update({}, {base: 'eggs'}, updateOptions, function(error) { assert.ifError(error); Breakfast.findOne({}).lean().exec(function(error, breakfast) { assert.ifError(error); - assert.equal('eggs', breakfast.base); - assert.equal('bacon', breakfast.topping); - db.close(done); + assert.equal(breakfast.base, 'eggs'); + assert.equal(breakfast.topping, 'bacon'); + done(); }); }); }); - it('doesnt set default on upsert if query sets it', function(done) { - var db = start(); + it('avoids nested paths if setting parent path (gh-4911)', function(done) { + var EmbeddedSchema = mongoose.Schema({ + embeddedField: String + }); + + var ParentSchema = mongoose.Schema({ + embedded: EmbeddedSchema + }); + + var Parent = db.model('gh4911', ParentSchema); + + var newDoc = { + _id: new mongoose.Types.ObjectId(), + embedded: null + }; + + var opts = { upsert: true, setDefaultsOnInsert: true }; - var s = new Schema({ topping: { type: String, default: 'bacon' }, base: String }); + Parent. + findOneAndUpdate({ _id: newDoc._id }, newDoc, opts). + then(function() { done(); }). + catch(done); + }); + + it('doesnt set default on upsert if query sets it', function(done) { + var s = new Schema({topping: {type: String, default: 'bacon'}, base: String}); var Breakfast = db.model('gh-860-1', s); - var updateOptions = { upsert: true, setDefaultsOnInsert: true }; - Breakfast.update({ topping: 'sausage' }, { base: 'eggs' }, updateOptions, function(error) { + var updateOptions = {upsert: true, setDefaultsOnInsert: true}; + Breakfast.update({topping: 'sausage'}, {base: 'eggs'}, updateOptions, function(error) { assert.ifError(error); Breakfast.findOne({}, function(error, breakfast) { assert.ifError(error); - assert.equal('eggs', breakfast.base); - assert.equal('sausage', breakfast.topping); - db.close(); + assert.equal(breakfast.base, 'eggs'); + assert.equal(breakfast.topping, 'sausage'); done(); }); }); }); it('properly sets default on upsert if query wont set it', function(done) { - var db = start(); - - var s = new Schema({ topping: { type: String, default: 'bacon' }, base: String }); + var s = new Schema({topping: {type: String, default: 'bacon'}, base: String}); var Breakfast = db.model('gh-860-2', s); - var updateOptions = { upsert: true, setDefaultsOnInsert: true }; - Breakfast.update({ topping: { $ne: 'sausage' } }, { base: 'eggs' }, updateOptions, function(error) { + var updateOptions = {upsert: true, setDefaultsOnInsert: true}; + Breakfast.update({topping: {$ne: 'sausage'}}, {base: 'eggs'}, updateOptions, function(error) { assert.ifError(error); Breakfast.findOne({}, function(error, breakfast) { assert.ifError(error); - assert.equal('eggs', breakfast.base); - assert.equal('bacon', breakfast.topping); - db.close(); + assert.equal(breakfast.base, 'eggs'); + assert.equal(breakfast.topping, 'bacon'); done(); }); }); }); - it('runs validators if theyre set', function(done) { - var db = start(); + it('handles defaults on document arrays (gh-4456)', function(done) { + var schema = new Schema({ + arr: { + type: [new Schema({ name: String }, { _id: false })], + default: [{ name: 'Val' }] + } + }); + + var M = db.model('gh4456', schema); + + var opts = { upsert: true, setDefaultsOnInsert: true }; + M.update({}, {}, opts, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.deepEqual(doc.toObject().arr, [{ name: 'Val' }]); + done(); + }); + }); + }); + it('runs validators if theyre set', function(done) { var s = new Schema({ - topping: { type: String, validate: function() { return false; } }, - base: { type: String, validate: function() { return true; } } + topping: { + type: String, + validate: function() { + return false; + } + }, + base: { + type: String, + validate: function() { + return true; + } + } }); var Breakfast = db.model('gh-860-3', s); - var updateOptions = { upsert: true, setDefaultsOnInsert: true, runValidators: true }; - Breakfast.update({}, { topping: 'bacon', base: 'eggs' }, updateOptions, function(error) { + var updateOptions = {upsert: true, setDefaultsOnInsert: true, runValidators: true}; + Breakfast.update({}, {topping: 'bacon', base: 'eggs'}, updateOptions, function(error) { assert.ok(!!error); - assert.equal(1, Object.keys(error.errors).length); - assert.equal('topping', Object.keys(error.errors)[0]); - assert.equal('Validator failed for path `topping` with value `bacon`', - error.errors['topping'].message); + assert.equal(Object.keys(error.errors).length, 1); + assert.equal(Object.keys(error.errors)[0], 'topping'); + assert.equal(error.errors.topping.message, 'Validator failed for path `topping` with value `bacon`'); Breakfast.findOne({}, function(error, breakfast) { assert.ifError(error); assert.ok(!breakfast); - db.close(); done(); }); }); }); it('validators handle $unset and $setOnInsert', function(done) { - var db = start(); - var s = new Schema({ - steak: { type: String, required: true }, + steak: {type: String, required: true}, eggs: { type: String, validate: function() { @@ -1126,55 +1191,45 @@ describe('model: update:', function() { }); var Breakfast = db.model('gh-860-4', s); - var updateOptions = { runValidators: true, context: 'query' }; - Breakfast.update({}, { $unset: { steak: '' }, $setOnInsert: { eggs: 'softboiled' } }, updateOptions, function(error) { + var updateOptions = {runValidators: true, context: 'query'}; + Breakfast.update({}, {$unset: {steak: ''}, $setOnInsert: {eggs: 'softboiled'}}, updateOptions, function(error) { assert.ok(!!error); - assert.equal(2, Object.keys(error.errors).length); + assert.equal(Object.keys(error.errors).length, 2); assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1); assert.ok(Object.keys(error.errors).indexOf('steak') !== -1); - assert.equal('Validator failed for path `eggs` with value `softboiled`', - error.errors['eggs'].message); - assert.equal('Path `steak` is required.', - error.errors['steak'].message); - db.close(); + assert.equal(error.errors.eggs.message, 'Validator failed for path `eggs` with value `softboiled`'); + assert.equal(error.errors.steak.message, 'Path `steak` is required.'); done(); }); }); it('min/max, enum, and regex built-in validators work', function(done) { - var db = start(); - var s = new Schema({ - steak: { type: String, enum: ['ribeye', 'sirloin'] }, - eggs: { type: Number, min: 4, max: 6 }, - bacon: { type: String, match: /strips/ } + steak: {type: String, enum: ['ribeye', 'sirloin']}, + eggs: {type: Number, min: 4, max: 6}, + bacon: {type: String, match: /strips/} }); var Breakfast = db.model('gh-860-5', s); - var updateOptions = { runValidators: true }; - Breakfast.update({}, { $set: { steak: 'ribeye', eggs: 3, bacon: '3 strips' } }, updateOptions, function(error) { + var updateOptions = {runValidators: true}; + Breakfast.update({}, {$set: {steak: 'ribeye', eggs: 3, bacon: '3 strips'}}, updateOptions, function(error) { assert.ok(!!error); - assert.equal(1, Object.keys(error.errors).length); - assert.equal('eggs', Object.keys(error.errors)[0]); - assert.equal('Path `eggs` (3) is less than minimum allowed value (4).', - error.errors['eggs'].message); + assert.equal(Object.keys(error.errors).length, 1); + assert.equal(Object.keys(error.errors)[0], 'eggs'); + assert.equal(error.errors.eggs.message, 'Path `eggs` (3) is less than minimum allowed value (4).'); - Breakfast.update({}, { $set: { steak: 'tofu', eggs: 5, bacon: '3 strips' } }, updateOptions, function(error) { + Breakfast.update({}, {$set: {steak: 'tofu', eggs: 5, bacon: '3 strips'}}, updateOptions, function(error) { assert.ok(!!error); - assert.equal(1, Object.keys(error.errors).length); - assert.equal('steak', Object.keys(error.errors)[0]); - assert.equal('`tofu` is not a valid enum value for path `steak`.', - error.errors['steak']); + assert.equal(Object.keys(error.errors).length, 1); + assert.equal(Object.keys(error.errors)[0], 'steak'); + assert.equal(error.errors.steak, '`tofu` is not a valid enum value for path `steak`.'); - - Breakfast.update({}, { $set: { steak: 'sirloin', eggs: 6, bacon: 'none' } }, updateOptions, function(error) { + Breakfast.update({}, {$set: {steak: 'sirloin', eggs: 6, bacon: 'none'}}, updateOptions, function(error) { assert.ok(!!error); - assert.equal(1, Object.keys(error.errors).length); - assert.equal('bacon', Object.keys(error.errors)[0]); - assert.equal('Path `bacon` is invalid (none).', - error.errors['bacon'].message); + assert.equal(Object.keys(error.errors).length, 1); + assert.equal(Object.keys(error.errors)[0], 'bacon'); + assert.equal(error.errors.bacon.message, 'Path `bacon` is invalid (none).'); - db.close(); done(); }); }); @@ -1182,131 +1237,146 @@ describe('model: update:', function() { }); it('multiple validation errors', function(done) { - var db = start(); - var s = new Schema({ - steak: { type: String, enum: ['ribeye', 'sirloin'] }, - eggs: { type: Number, min: 4, max: 6 }, - bacon: { type: String, match: /strips/ } + steak: {type: String, enum: ['ribeye', 'sirloin']}, + eggs: {type: Number, min: 4, max: 6}, + bacon: {type: String, match: /strips/} }); var Breakfast = db.model('gh-860-6', s); - var updateOptions = { runValidators: true }; - Breakfast.update({}, { $set: { steak: 'tofu', eggs: 2, bacon: '3 strips' } }, updateOptions, function(error) { + var updateOptions = {runValidators: true}; + Breakfast.update({}, {$set: {steak: 'tofu', eggs: 2, bacon: '3 strips'}}, updateOptions, function(error) { assert.ok(!!error); - assert.equal(2, Object.keys(error.errors).length); + assert.equal(Object.keys(error.errors).length, 2); assert.ok(Object.keys(error.errors).indexOf('steak') !== -1); assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1); - db.close(); done(); }); }); it('validators ignore $inc', function(done) { - var db = start(); - var s = new Schema({ - steak: { type: String, required: true }, - eggs: { type: Number, min: 4 } + steak: {type: String, required: true}, + eggs: {type: Number, min: 4} }); var Breakfast = db.model('gh-860-7', s); - var updateOptions = { runValidators: true }; - Breakfast.update({}, { $inc: { eggs: 1 } }, updateOptions, function(error) { + var updateOptions = {runValidators: true}; + Breakfast.update({}, {$inc: {eggs: 1}}, updateOptions, function(error) { assert.ifError(error); - db.close(); done(); }); }); it('validators handle positional operator (gh-3167)', function(done) { - var db = start(); - var s = new Schema({ - toppings: [{ name: { type: String, enum: ['bacon', 'cheese'] } }] + toppings: [{name: {type: String, enum: ['bacon', 'cheese']}}] }); var Breakfast = db.model('gh-860-8', s); - var updateOptions = { runValidators: true }; + var updateOptions = {runValidators: true}; Breakfast.update( - { 'toppings.name': 'bacon' }, - { 'toppings.$.name': 'tofu' }, - updateOptions, - function(error) { - assert.ok(error); - assert.ok(error.errors['toppings.0.name']); - db.close(done); - }); + {'toppings.name': 'bacon'}, + {'toppings.$.name': 'tofu'}, + updateOptions, + function(error) { + assert.ok(error); + assert.ok(error.errors['toppings.0.name']); + done(); + }); + }); + + it('required and single nested (gh-4479)', function(done) { + var FileSchema = new Schema({ + name: { + type: String, + required: true + } + }); + + var CompanySchema = new Schema({ + file: FileSchema + }); + + var Company = db.model('gh4479', CompanySchema); + var update = { file: { name: '' } }; + var options = { runValidators: true }; + Company.update({}, update, options, function(error) { + assert.ok(error); + assert.equal(error.errors['file.name'].message, + 'Path `name` is required.'); + done(); + }); }); }); it('works with $set and overwrite (gh-2515)', function(done) { var db = start(); - var schema = new Schema({ breakfast: String }); + var schema = new Schema({breakfast: String}); var M = db.model('gh-2515', schema); - M.create({ breakfast: 'bacon' }, function(error, doc) { + M.create({breakfast: 'bacon'}, function(error, doc) { assert.ifError(error); M.update( - { _id: doc._id }, - { $set: { breakfast: 'eggs' } }, - { overwrite: true }, - function(error) { - assert.ifError(error); - M.findOne({ _id: doc._id }, function(error, doc) { + {_id: doc._id}, + {$set: {breakfast: 'eggs'}}, + {overwrite: true}, + function(error) { assert.ifError(error); - assert.equal(doc.breakfast, 'eggs'); - db.close(done); + M.findOne({_id: doc._id}, function(error, doc) { + assert.ifError(error); + assert.equal(doc.breakfast, 'eggs'); + db.close(done); + }); }); - }); }); }); it('successfully casts set with nested mixed objects (gh-2796)', function(done) { var db = start(); - var schema = new Schema({ breakfast: {} }); + var schema = new Schema({breakfast: {}}); var M = db.model('gh-2796', schema); M.create({}, function(error, doc) { assert.ifError(error); M.update( - { _id: doc._id }, - { breakfast: { eggs: 2, bacon: 3 } }, - function(error, result) { - assert.ifError(error); - assert.ok(result.ok); - assert.equal(result.n, 1); - M.findOne({ _id: doc._id }, function(error, doc) { + {_id: doc._id}, + {breakfast: {eggs: 2, bacon: 3}}, + function(error, result) { assert.ifError(error); - assert.equal(doc.breakfast.eggs, 2); - db.close(done); + assert.ok(result.ok); + assert.equal(result.n, 1); + M.findOne({_id: doc._id}, function(error, doc) { + assert.ifError(error); + assert.equal(doc.breakfast.eggs, 2); + db.close(done); + }); }); - }); }); }); it('handles empty update with promises (gh-2796)', function(done) { var db = start(); - var schema = new Schema({ eggs: Number }); + var schema = new Schema({eggs: Number}); var M = db.model('gh-2796', schema); M.create({}, function(error, doc) { assert.ifError(error); M.update( - { _id: doc._id }, - { notInSchema: 1 }). - exec(). - then(function(data) { - assert.equal(data.ok, 0); - assert.equal(data.n, 0); - db.close(done); - }). - onReject(function(error) { - return done(error); - }); + {_id: doc._id}, + {notInSchema: 1}). + exec(). + then(function(data) { + assert.equal(data.ok, 0); + assert.equal(data.n, 0); + db.close(done); + }). + onReject(function(error) { + return done(error); + }); }); }); @@ -1316,7 +1386,7 @@ describe('model: update:', function() { var numPres = 0; var numPosts = 0; - var band = new Schema({ members: [String] }); + var band = new Schema({members: [String]}); band.pre('update', function(next) { ++numPres; next(); @@ -1326,25 +1396,25 @@ describe('model: update:', function() { }); var Band = db.model('gh-964', band); - var gnr = new Band({ members: ['Axl', 'Slash', 'Izzy', 'Duff', 'Adler' ] }); + var gnr = new Band({members: ['Axl', 'Slash', 'Izzy', 'Duff', 'Adler']}); gnr.save(function(error) { assert.ifError(error); - assert.equal(0, numPres); - assert.equal(0, numPosts); + assert.equal(numPres, 0); + assert.equal(numPosts, 0); Band.update( - { _id: gnr._id }, - { $pull: { members: 'Adler' } }, - function(error) { - assert.ifError(error); - assert.equal(1, numPres); - assert.equal(1, numPosts); - Band.findOne({ _id: gnr._id }, function(error, doc) { + {_id: gnr._id}, + {$pull: {members: 'Adler'}}, + function(error) { assert.ifError(error); - assert.deepEqual(['Axl', 'Slash', 'Izzy', 'Duff'], - doc.toObject().members); - db.close(done); + assert.equal(numPres, 1); + assert.equal(numPosts, 1); + Band.findOne({_id: gnr._id}, function(error, doc) { + assert.ifError(error); + assert.deepEqual(['Axl', 'Slash', 'Izzy', 'Duff'], + doc.toObject().members); + db.close(done); + }); }); - }); }); }); @@ -1352,62 +1422,88 @@ describe('model: update:', function() { var db = start(); var bandSchema = new Schema({ - lead: { type: String, enum: ['Axl Rose'] } + lead: {type: String, enum: ['Axl Rose']} }); bandSchema.pre('update', function() { this.options.runValidators = true; }); var Band = db.model('gh2706', bandSchema, 'gh2706'); - Band.update({}, { $set: { lead: 'Not Axl' } }, function(err) { + Band.update({}, {$set: {lead: 'Not Axl'}}, function(err) { assert.ok(err); db.close(done); }); }); - it('embedded objects (gh-2733)', function(done) { - var db = start(); + describe('objects and arrays', function() { + var db; - var bandSchema = new Schema({ - singer: { - firstName: { type: String, enum: ['Axl'] }, - lastName: { type: String, enum: ['Rose'] } - } - }); - bandSchema.pre('update', function() { - this.options.runValidators = true; + before(function() { + db = start(); }); - var Band = db.model('gh2706', bandSchema, 'gh2706'); - Band.update({}, { $set: { singer: { firstName: 'Not', lastName: 'Axl' } } }, function(err) { - assert.ok(err); + after(function(done) { db.close(done); }); - }); - it('handles document array validation (gh-2733)', function(done) { - var db = start(); + it('embedded objects (gh-2706)', function(done) { + var bandSchema = new Schema({ + singer: { + firstName: {type: String, enum: ['Axl']}, + lastName: {type: String, enum: ['Rose']} + } + }); + bandSchema.pre('update', function() { + this.options.runValidators = true; + }); + var Band = db.model('gh2706', bandSchema, 'gh2706'); - var member = new Schema({ - name: String, - role: { type: String, required: true, enum: ['singer', 'guitar', 'drums', 'bass'] } - }); - var band = new Schema({ members: [member], name: String }); - var Band = db.model('band', band, 'bands'); - var members = [ - { name: 'Axl Rose', role: 'singer' }, - { name: 'Slash', role: 'guitar' }, - { name: 'Christopher Walken', role: 'cowbell' } - ]; - - Band.findOneAndUpdate( - { name: "Guns N' Roses" }, - { $set: { members: members } }, - { runValidators: true }, - function(err) { + Band.update({}, {$set: {singer: {firstName: 'Not', lastName: 'Axl'}}}, function(err) { assert.ok(err); done(); }); + }); + + it('handles document array validation (gh-2733)', function(done) { + var member = new Schema({ + name: String, + role: {type: String, required: true, enum: ['singer', 'guitar', 'drums', 'bass']} + }); + var band = new Schema({members: [member], name: String}); + var Band = db.model('band', band, 'bands'); + var members = [ + {name: 'Axl Rose', role: 'singer'}, + {name: 'Slash', role: 'guitar'}, + {name: 'Christopher Walken', role: 'cowbell'} + ]; + + Band.findOneAndUpdate( + {name: 'Guns N\' Roses'}, + {$set: {members: members}}, + {runValidators: true}, + function(err) { + assert.ok(err); + done(); + }); + }); + + it('validators on arrays (gh-3724)', function(done) { + var schema = new Schema({ + arr: [String] + }); + + schema.path('arr').validate(function() { + return false; + }); + + var M = db.model('gh3724', schema); + var options = {runValidators: true}; + M.findOneAndUpdate({}, {arr: ['test']}, options, function(error) { + assert.ok(error); + assert.ok(/ValidationError/.test(error.toString())); + done(); + }); + }); }); }); @@ -1432,17 +1528,17 @@ describe('model: update:', function() { id: 0 }; - Book.update({}, jsonObject, { upsert: true, overwrite: true }, - function(error) { - assert.ifError(error); - Book.findOne({ id: 0 }, function(error, book) { + Book.update({}, jsonObject, {upsert: true, overwrite: true}, + function(error) { assert.ifError(error); - assert.equal(book.chapters.length, 2); - assert.ok(book.chapters[0]._id); - assert.ok(book.chapters[1]._id); - done(); + Book.findOne({id: 0}, function(error, book) { + assert.ifError(error); + assert.equal(book.chapters.length, 2); + assert.ok(book.chapters[0]._id); + assert.ok(book.chapters[1]._id); + db.close(done); + }); }); - }); }); it('works with undefined date (gh-2833)', function(done) { @@ -1454,8 +1550,8 @@ describe('model: update:', function() { var D = db.model('gh2833', dateSchema); assert.doesNotThrow(function() { - D.update({}, { d: undefined }, function() { - done(); + D.update({}, {d: undefined}, function() { + db.close(done); }); }); }); @@ -1463,14 +1559,16 @@ describe('model: update:', function() { it('does not add virtuals to update (gh-2046)', function(done) { var db = start(); - var childSchema = Schema({ foo: String }, { toObject: { getters: true } }); - var parentSchema = Schema({ children: [childSchema] }); + var childSchema = new Schema({foo: String}, {toObject: {getters: true}}); + var parentSchema = new Schema({children: [childSchema]}); - childSchema.virtual('bar').get(function() { return 'bar'; }); + childSchema.virtual('bar').get(function() { + return 'bar'; + }); var Parent = db.model('gh2046', parentSchema, 'gh2046'); - var update = Parent.update({}, { $push: { children: { foo: 'foo' } } }, { upsert: true }); + var update = Parent.update({}, {$push: {children: {foo: 'foo'}}}, {upsert: true}); assert.equal(update._update.$push.children.bar, undefined); update.exec(function(error) { @@ -1484,18 +1582,6 @@ describe('model: update:', function() { }); }); - it('can $rename (gh-1845)', function(done) { - var db = start(); - - var schema = Schema({ foo: Date, bar: Date }); - var Model = db.model('gh1845', schema, 'gh1845'); - - Model.update({}, { $rename: { foo: 'bar' } }, function(error) { - assert.ifError(error); - db.close(done); - }); - }); - it('doesnt modify original argument doc (gh-3008)', function(done) { var db = start(); var FooSchema = new mongoose.Schema({ @@ -1504,72 +1590,107 @@ describe('model: update:', function() { }); var Model = db.model('gh3008', FooSchema); - var update = { $set: { values: 2, value: 2 } }; - Model.update({ key: 1 }, update, function() { + var update = {$set: {values: 2, value: 2}}; + Model.update({key: 1}, update, function() { assert.equal(update.$set.values, 2); - done(); + db.close(done); }); }); - it('can $rename (gh-1845)', function(done) { - var db = start(); - var schema = Schema({ foo: Date, bar: Date }); - var Model = db.model('gh1845', schema, 'gh1845'); + describe('bug fixes', function() { + var db; - Model.update({}, { $rename: { foo: 'bar' } }, function(error) { - assert.ifError(error); + before(function() { + db = start(); + }); + + after(function(done) { db.close(done); }); - }); - it('allows objects with positional operator (gh-3185)', function(done) { - var db = start(); - var schema = Schema({ children: [{ _id: Number }] }); - var MyModel = db.model('gh3185', schema, 'gh3185'); + it('can $rename (gh-1845)', function(done) { + var db = start(); - MyModel.create({ children: [{ _id: 1 }] }, function(error, doc) { - assert.ifError(error); - MyModel.findOneAndUpdate( - { _id: doc._id, 'children._id': 1 }, - { $set: { 'children.$': { _id: 2 } } }, - { 'new': true }, - function(error, doc) { + var schema = new Schema({ foo: Date, bar: Date }); + var Model = db.model('gh1845', schema, 'gh1845'); + + var update = { $rename: { foo: 'bar'} }; + Model.create({ foo: Date.now() }, function(error) { + assert.ifError(error); + Model.update({}, update, { multi: true }, function(error, res) { assert.ifError(error); - assert.equal(doc.children[0]._id, 2); + assert.ok(res.ok); + assert.equal(res.nModified, 1); db.close(done); }); + }); }); - }); - it('mixed type casting (gh-3305)', function(done) { - var db = start(); + it('allows objects with positional operator (gh-3185)', function(done) { + var schema = new Schema({children: [{_id: Number}]}); + var MyModel = db.model('gh3185', schema, 'gh3185'); + + MyModel.create({children: [{_id: 1}]}, function(error, doc) { + assert.ifError(error); + MyModel.findOneAndUpdate( + {_id: doc._id, 'children._id': 1}, + {$set: {'children.$': {_id: 2}}}, + {new: true}, + function(error, doc) { + assert.ifError(error); + assert.equal(doc.children[0]._id, 2); + done(); + }); + }); + }); - var Schema = mongoose.Schema({}, { strict: false }); - var Model = db.model('gh3305', Schema); + it('mixed type casting (gh-3305)', function(done) { + var Schema = mongoose.Schema({}, {strict: false}); + var Model = db.model('gh3305', Schema); - Model.create({}, function(error, m) { - assert.ifError(error); - Model. - update({ _id: m._id }, { '$push': { 'myArr': { 'key': 'Value' } } }). + Model.create({}, function(error, m) { + assert.ifError(error); + Model. + update({_id: m._id}, {$push: {myArr: {key: 'Value'}}}). exec(function(error, res) { assert.ifError(error); assert.equal(res.n, 1); done(); }); + }); }); - }); - it('mixed nested type casting (gh-3337)', function(done) { - var db = start(); + it('replaceOne', function(done) { + var schema = mongoose.Schema({ name: String, age: Number }, { + versionKey: false + }); + var Model = db.model('gh3998_r1', schema); + + Model.create({ name: 'abc', age: 1 }, function(error, m) { + assert.ifError(error); + Model.replaceOne({ name: 'abc' }, { name: 'test' }).exec(function(err) { + assert.ifError(err); + Model.findById(m._id).exec(function(error, doc) { + assert.ifError(error); + assert.deepEqual(doc.toObject({ virtuals: false }), { + _id: m._id, + name: 'test' + }); + done(); + }); + }); + }); + }); - var Schema = mongoose.Schema({ attributes: {} }, { strict: true }); - var Model = db.model('gh3337', Schema); + it('mixed nested type casting (gh-3337)', function(done) { + var Schema = mongoose.Schema({attributes: {}}, {strict: true}); + var Model = db.model('gh3337', Schema); - Model.create({}, function(error, m) { - assert.ifError(error); - var update = { '$push': { 'attributes.scores.bar': { a: 1 } } }; - Model. - update({ _id: m._id }, update). + Model.create({}, function(error, m) { + assert.ifError(error); + var update = {$push: {'attributes.scores.bar': {a: 1}}}; + Model. + update({_id: m._id}, update). exec(function(error, res) { assert.ifError(error); assert.equal(res.n, 1); @@ -1579,33 +1700,1237 @@ describe('model: update:', function() { done(); }); }); + }); + }); + + it('with single nested (gh-3820)', function(done) { + var child = new mongoose.Schema({ + item2: { + item3: String, + item4: String + } + }); + + var parentSchema = new mongoose.Schema({ + name: String, + item1: child + }); + + var Parent = db.model('Parent', parentSchema); + + Parent.create({ name: 'test' }, function(error, doc) { + assert.ifError(error); + var update = { 'item1.item2': { item3: 'test1', item4: 'test2' } }; + doc.update(update, function(error) { + assert.ifError(error); + Parent.findOne({ _id: doc._id }, function(error, doc) { + assert.ifError(error); + assert.equal(doc.item1.item2.item3, 'test1'); + assert.equal(doc.item1.item2.item4, 'test2'); + done(); + }); + }); + }); + }); + + it('with single nested and transform (gh-4621)', function(done) { + var SubdocSchema = new Schema({ + name: String + }, { + toObject: { + transform: function(doc, ret) { + ret.id = ret._id.toString(); + delete ret._id; + } + } + }); + + var CollectionSchema = new Schema({ + field2: SubdocSchema + }); + + var Collection = db.model('gh4621', CollectionSchema); + + Collection.create({}, function(error, doc) { + assert.ifError(error); + var update = { 'field2': { name: 'test' } }; + Collection.update({ _id: doc._id }, update, function(err) { + assert.ifError(err); + Collection.collection.findOne({ _id: doc._id }, function(err, doc) { + assert.ifError(err); + assert.ok(doc.field2._id); + assert.ok(!doc.field2.id); + done(); + }); + }); + }); + + }); + + it('works with buffers (gh-3496)', function(done) { + var Schema = mongoose.Schema({myBufferField: Buffer}); + var Model = db.model('gh3496', Schema); + + Model.update({}, {myBufferField: new Buffer(1)}, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('dontThrowCastError option (gh-3512)', function(done) { + var Schema = mongoose.Schema({name: String}); + var Model = db.model('gh3412', Schema); + + var badQuery = {_id: 'foo'}; + var update = {name: 'test'}; + var options = {dontThrowCastError: true}; + Model.update(badQuery, update, options).then(null, function(error) { + assert.ok(error); + done(); + }); + }); + + it('.update(doc) (gh-3221)', function(done) { + var Schema = mongoose.Schema({name: String}); + var Model = db.model('gh3221', Schema); + + var query = Model.update({name: 'Val'}); + assert.equal(query.getUpdate().$set.name, 'Val'); + + query = Model.find().update({name: 'Val'}); + assert.equal(query.getUpdate().$set.name, 'Val'); + + done(); + }); + + it('nested schemas with strict false (gh-3883)', function(done) { + var OrderSchema = new mongoose.Schema({ + }, { strict: false, _id: false }); + + var SeasonSchema = new mongoose.Schema({ + regions: [OrderSchema] + }, { useNestedStrict: true }); + + var Season = db.model('gh3883', SeasonSchema); + var obj = { regions: [{ r: 'test', action: { order: 'hold' } }] }; + Season.create(obj, function(error) { + assert.ifError(error); + var query = { 'regions.r': 'test' }; + var update = { $set: { 'regions.$.action': { order: 'move' } } }; + var opts = { 'new': true }; + Season.findOneAndUpdate(query, update, opts, function(error, doc) { + assert.ifError(error); + assert.equal(doc.toObject().regions[0].action.order, 'move'); + done(); + }); + }); + }); + + it('middleware update with exec (gh-3549)', function(done) { + var Schema = mongoose.Schema({name: String}); + + Schema.pre('update', function(next) { + this.update({name: 'Val'}); + next(); + }); + + var Model = db.model('gh3549', Schema); + + Model.create({}, function(error, doc) { + assert.ifError(error); + Model.update({_id: doc._id}, {name: 'test'}).exec(function(error) { + assert.ifError(error); + Model.findOne({_id: doc._id}, function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Val'); + done(); + }); + }); + }); + }); + + it('casting $push with overwrite (gh-3564)', function(done) { + var schema = mongoose.Schema({ + topicId: Number, + name: String, + followers: [Number] + }); + + var doc = { + topicId: 100, + name: 'name', + followers: [500] + }; + + var M = db.model('gh-3564', schema); + + M.create(doc, function(err) { + assert.ifError(err); + + var update = {$push: {followers: 200}}; + var opts = {overwrite: true, new: true, safe: true, upsert: false, multi: false}; + + M.update({topicId: doc.topicId}, update, opts, function(err) { + assert.ifError(err); + M.findOne({topicId: doc.topicId}, function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'name'); + assert.deepEqual(doc.followers.toObject(), [500, 200]); + done(); + }); + }); + }); + }); + + it('$push with buffer doesnt throw error (gh-3890)', function(done) { + var InfoSchema = new Schema({ + prop: { type: Buffer } + }); + + var ModelASchema = new Schema({ + infoList: { type: [InfoSchema] } + }); + + var ModelA = db.model('gh3890', ModelASchema); + + var propValue = new Buffer('aa267824dc1796f265ab47870e279780', 'base64'); + + var update = { + $push: { + info_list: { prop: propValue } + } + }; + + ModelA.update({}, update, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('$set with buffer (gh-3961)', function(done) { + var schema = { + name: Buffer + }; + + var Model = db.model('gh3961', schema); + + var value = new Buffer('aa267824dc1796f265ab47870e279780', 'base64'); + var instance = new Model({ name: null }); + + instance.save(function(error) { + assert.ifError(error); + var query = { _id: instance._id }; + var update = { $set: { name: value } }; + var ok = function() { + done(); + }; + Model.update(query, update).then(ok, done); + }); + }); + + it('versioning with setDefaultsOnInsert (gh-2593)', function(done) { + var schema = new Schema({ + num: Number, + arr: [{ num: Number }] + }); + + var Model = db.model('gh2593', schema); + var update = { $inc: { num: 1 }, $push: { arr: { num: 5 } } }; + var options = { + upsert: true, + setDefaultsOnInsert: true, + new: true, + runValidators: true + }; + Model.update({}, update, options, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('updates with timestamps with $set (gh-4989)', function(done) { + var TagSchema = new Schema({ + name: String, + tags: [{ + enum: ['test1', 'test2'], + type: String + }] + }, { timestamps: true }); + + var Tag = db.model('gh4989', TagSchema); + var tagId; + + Tag.remove({}). + then(function() { return Tag.create({ name: 'test' }); }). + then(function() { return Tag.findOne(); }). + then(function(tag) { + tagId = tag._id; + return Tag.update({ _id: tagId }, { + $set: { + tags: ['test1'] + } + }); + }). + then(function() { return Tag.findById(tagId); }). + then(function(res) { + assert.deepEqual(res.tags.toObject(), ['test1']); + done(); + }). + catch(done); + }); + + it('lets $currentDate go through with updatedAt (gh-5222)', function(done) { + var testSchema = new Schema({ + name: String + }, { timestamps: true }); + + var Test = db.model('gh5222', testSchema); + + Test.create({ name: 'test' }, function(error) { + assert.ifError(error); + var u = { $currentDate: { updatedAt: true }, name: 'test2' }; + Test.update({}, u, function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('update validators on single nested (gh-4332)', function(done) { + var AreaSchema = new Schema({ + a: String + }); + + var CompanySchema = new Schema({ + area: { + type: AreaSchema, + validate: { + validator: function() { + return false; + }, + message: 'Not valid Area' + } + } + }); + + var Company = mongoose.model('Company', CompanySchema); + + var update = { + area: { + a: 'Helo' + } + }; + + var opts = { + runValidators: true + }; + + Company.update({}, update, opts, function(error) { + assert.ok(error); + assert.equal(error.errors['area'].message, 'Not valid Area'); + done(); + }); + }); + + it('updates child schema timestamps with $push (gh-4049)', function(done) { + var opts = { + timestamps: true, + toObject: { + virtuals: true + }, + toJSON: { + virtuals: true + } + }; + + var childSchema = new mongoose.Schema({ + senderId: { type: String } + }, opts); + + var parentSchema = new mongoose.Schema({ + children: [childSchema] + }, opts); + + var Parent = db.model('gh4049', parentSchema); + + var b2 = new Parent(); + b2.save(function(err, doc) { + var query = { _id: doc._id }; + var update = { $push: { children: { senderId: '234' } } }; + var opts = { 'new': true }; + Parent.findOneAndUpdate(query, update, opts).exec(function(error, res) { + assert.ifError(error); + assert.equal(res.children.length, 1); + assert.equal(res.children[0].senderId, '234'); + assert.ok(res.children[0].createdAt); + assert.ok(res.children[0].updatedAt); + done(); + }); + }); + }); + + it('updates child schema timestamps with $set (gh-4049)', function(done) { + var opts = { + timestamps: true, + toObject: { + virtuals: true + }, + toJSON: { + virtuals: true + } + }; + + var childSchema = new mongoose.Schema({ + senderId: { type: String } + }, opts); + + var parentSchema = new mongoose.Schema({ + children: [childSchema], + child: childSchema + }, opts); + + var Parent = db.model('gh4049_0', parentSchema); + + var b2 = new Parent(); + b2.save(function(err, doc) { + var query = { _id: doc._id }; + var update = { + $set: { + children: [{ senderId: '234' }], + child: { senderId: '567' } + } + }; + var opts = { 'new': true }; + Parent.findOneAndUpdate(query, update, opts).exec(function(error, res) { + assert.ifError(error); + assert.equal(res.children.length, 1); + assert.equal(res.children[0].senderId, '234'); + assert.ok(res.children[0].createdAt); + assert.ok(res.children[0].updatedAt); + + assert.ok(res.child.createdAt); + assert.ok(res.child.updatedAt); + done(); + }); + }); + }); + + it('handles positional operator with timestamps (gh-4418)', function(done) { + var schema = new Schema({ + thing: [{ + thing2: { type: String }, + test: String + }] + }, { timestamps: true }); + + var Model = db.model('gh4418', schema); + var query = { 'thing.thing2': 'test' }; + var update = { $set: { 'thing.$.test': 'test' } }; + Model.update(query, update, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('push with timestamps (gh-4514)', function(done) { + var sampleSchema = new mongoose.Schema({ + sampleArray: [{ + values: [String] + }] + }, { timestamps: true }); + + var sampleModel = db.model('gh4514', sampleSchema); + var newRecord = new sampleModel({ + sampleArray: [{ values: ['record1'] }] + }); + + newRecord.save(function(err) { + assert.ifError(err); + sampleModel.update({ 'sampleArray.values': 'record1' }, { + $push: { 'sampleArray.$.values': 'another record' } + }, + { runValidators: true }, + function(err) { + assert.ifError(err); + done(); + }); + }); + }); + + it('addToSet (gh-4953)', function(done) { + var childSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + lastName: { + type: String, + required: true + } + }); + + var parentSchema = new mongoose.Schema({ + children: [childSchema] + }); + + var Model = db.model('gh4953', parentSchema); + + var update = { + $addToSet: { children: { name: 'Test' } } + }; + var opts = { new: true, runValidators: true }; + Model.findOneAndUpdate({}, update, opts, function(error) { + assert.ok(error); + assert.ok(error.errors['children']); + done(); + }); + }); + + it('overwrite with timestamps (gh-4054)', function(done) { + var testSchema = new Schema({ + user: String, + something: Number + }, { timestamps: true }); + + var TestModel = db.model('gh4054', testSchema); + var options = { overwrite: true, upsert: true }; + var update = { + user: 'John', + something: 1 + }; + + TestModel.update({ user: 'test' }, update, options, function(error) { + assert.ifError(error); + TestModel.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(doc.createdAt); + assert.ok(doc.updatedAt); + done(); + }); + }); + }); + + it('update with buffer and exec (gh-4609)', function(done) { + var arrSchema = new Schema({ + ip: mongoose.SchemaTypes.Buffer + }); + var schema = new Schema({ + arr: [arrSchema] + }); + + var M = db.model('gh4609', schema); + + var m = new M({ arr: [{ ip: new Buffer(1) }] }); + m.save(function(error, m) { + assert.ifError(error); + m.update({ $push: { arr: { ip: new Buffer(1) } } }).exec(function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('single nested with runValidators (gh-4420)', function(done) { + var FileSchema = new Schema({ + name: String + }); + + var CompanySchema = new Schema({ + name: String, + file: FileSchema + }); + + var Company = db.model('Company', CompanySchema); + + Company.create({ name: 'Booster Fuels' }, function(error) { + assert.ifError(error); + var update = { file: { name: 'new-name' } }; + var options = { runValidators: true }; + Company.update({}, update, options, function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('single nested under doc array with runValidators (gh-4960)', function(done) { + var ProductSchema = new Schema({ + name: String + }); + + var UserSchema = new Schema({ + sell: [{ + product: { type: ProductSchema, required: true } + }] + }); + + var User = db.model('gh4960', UserSchema); + + User.create({}). + then(function(user) { + return User.update({ + _id: user._id + }, { + sell: [{ + product: { + name: 'Product 1' + } + }] + }, { + runValidators: true + }); + }). + // Should not throw + then(function() { + done(); + }). + catch(done); + }); + + it('single nested schema with geo (gh-4465)', function(done) { + var addressSchema = new Schema({ + geo: {type: [Number], index: '2dsphere'} + }, { _id : false }); + var containerSchema = new Schema({ address: addressSchema }); + var Container = db.model('gh4465', containerSchema); + + Container.update({}, { address: { geo: [-120.24, 39.21] } }). + exec(function(error) { + assert.ifError(error); + done(); + }); }); - }); - it('works with buffers (gh-3496)', function(done) { - var db = start(); + it('runs validation on Mixed properties of embedded arrays during updates (gh-4441)', function(done) { + var db = start(); + + var A = new Schema({ str: {} }); + var validateCalls = 0; + A.path('str').validate(function(val, next) { + ++validateCalls; + next(); + }); - var Schema = mongoose.Schema({ myBufferField: Buffer }); - var Model = db.model('gh3496', Schema); + var B = new Schema({a: [A]}); - Model.update({}, { myBufferField: new Buffer(1) }, function(error) { - assert.ifError(error); - db.close(done); + B = db.model('b', B); + + B.findOneAndUpdate( + {foo: 'bar'}, + {$set: {a: [{str: {somekey: 'someval'}}]}}, + {runValidators: true}, + function(err) { + assert.ifError(err); + assert.equal(validateCalls, 1); + db.close(done); + } + ); }); - }); - it('dontThrowCastError option (gh-3512)', function(done) { - var db = start(); + it('updating single nested doc property casts correctly (gh-4655)', function(done) { + var FileSchema = new Schema({}); + + var ProfileSchema = new Schema({ + images: [FileSchema], + rules: { + hours: { + begin: Date, + end: Date + } + } + }); - var Schema = mongoose.Schema({ name: String }); - var Model = db.model('gh3412', Schema); + var UserSchema = new Schema({ + email: { type: String }, + profiles: [ProfileSchema] + }); - var badQuery = { _id: 'foo' }; - var update = { name: 'test' }; - var options = { dontThrowCastError: true }; - Model.update(badQuery, update, options).then(null, function(error) { - assert.ok(error); - db.close(done); + + var User = db.model('gh4655', UserSchema); + + User.create({ profiles: [] }, function(error, user) { + assert.ifError(error); + User.update({ _id: user._id }, {$set: {'profiles.0.rules': {}}}). + exec(function(error) { + assert.ifError(error); + User.findOne({ _id: user._id }).lean().exec(function(error, doc) { + assert.ifError(error); + assert.deepEqual(doc.profiles[0], { rules: {} }); + done(); + }); + }); + }); + }); + + it('with overwrite and upsert (gh-4749) (gh-5631)', function(done) { + var schema = new Schema({ + name: String, + meta: { age: { type: Number } } + }); + var User = db.model('gh4749', schema); + + var filter = { name: 'Bar' }; + var update = { name: 'Bar', meta: { age: 33 } }; + var options = { overwrite: true, upsert: true }; + var q2 = User.update(filter, update, options); + assert.deepEqual(q2.getUpdate(), { + __v: 0, + meta: { age: 33 }, + name: 'Bar' + }); + + var q3 = User.findOneAndUpdate(filter, update, options); + assert.deepEqual(q3.getUpdate(), { + __v: 0, + meta: { age: 33 }, + name: 'Bar' + }); + + done(); + }); + + it('findOneAndUpdate with nested arrays (gh-5032)', function(done) { + var schema = Schema({ + name: String, + inputs: [ [ String ] ] // Array of Arrays of Strings + }); + + var Activity = db.model('Test', schema); + + var q = { name: 'Host Discovery' }; + var u = { inputs: [['ipRange']] }; + var o = { upsert: true }; + Activity.findOneAndUpdate(q, u, o).exec(function(error) { + assert.ifError(error); + done(); + }); + }); + + it('findOneAndUpdate with timestamps (gh-5045)', function(done) { + var schema = new Schema({ + username: String, + isDeleted: Boolean + }, { timestamps: true }); + var User = db.model('gh5045', schema); + + User.findOneAndUpdate( + { username: 'test', isDeleted: false }, + { createdAt: '2017-03-06T14:08:59+00:00' }, + { new: true, setDefaultsOnInsert: true, upsert: true }, + function(error) { + assert.ifError(error); + User.updateOne({ username: 'test' }, { createdAt: new Date() }). + exec(function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('doesnt double-call setters when updating an array (gh-5041)', function(done) { + var called = 0; + var UserSchema = new Schema({ + name: String, + foos: [{ + _id: false, + foo: { + type: Number, + get: function(val) { + return val.toString(); + }, + set: function(val) { + ++called; + return val; + } + } + }] + }); + + var User = db.model('gh5041', UserSchema); + + User.findOneAndUpdate({}, { foos: [{ foo: '13.57' }] }, function(error) { + assert.ifError(error); + assert.equal(called, 1); + done(); + }); + }); + + it('overwrite doc with update validators (gh-3556)', function(done) { + var testSchema = new Schema({ + name: { + type: String, + required: true + }, + otherName: String + }); + var Test = db.model('gh3556', testSchema); + + var opts = { overwrite: true, runValidators: true }; + Test.update({}, { otherName: 'test' }, opts, function(error) { + assert.ok(error); + assert.ok(error.errors['name']); + Test.update({}, { $set: { otherName: 'test' } }, opts, function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('does not fail if passing whole doc (gh-5088)', function(done) { + var schema = new Schema({ + username: String, + x: String + }, { timestamps: true }); + var User = db.model('gh5088', schema); + + User.create({ username: 'test' }). + then(function(user) { + user.x = 'test2'; + return User.findOneAndUpdate({ _id: user._id }, user, + { new: true }); + }). + then(function(user) { + assert.equal(user.x, 'test2'); + done(); + }). + catch(done); + }); + + it('does not fail if passing whole doc (gh-5111)', function(done) { + var schema = new Schema({ + fieldOne: String + }, { strict: true }); + var Test = db.model('gh5111', schema); + + Test.create({ fieldOne: 'Test' }). + then(function() { + var data = { fieldOne: 'Test2', fieldTwo: 'Test3' }; + var opts = { + upsert: true, + runValidators: false, + strict: false + }; + return Test.update({}, data, opts); + }). + then(function() { + return Test.findOne(); + }). + then(function(doc) { + assert.equal(doc.fieldOne, 'Test2'); + assert.equal(doc.get('fieldTwo'), 'Test3'); + done(); + }). + catch(done); + }); + + it('$pullAll with null (gh-5164)', function(done) { + var schema = new Schema({ + name: String, + arr: [{ name: String }] + }, { strict: true }); + var Test = db.model('gh5164', schema); + + var doc = new Test({ name: 'Test', arr: [null, {name: 'abc'}] }); + + doc.save(). + then(function(doc) { + return Test.update({ _id: doc._id }, { + $pullAll: { arr: [null] } + }); + }). + then(function() { + return Test.findById(doc); + }). + then(function(doc) { + assert.equal(doc.arr.length, 1); + assert.equal(doc.arr[0].name, 'abc'); + done(); + }). + catch(done); + }); + + it('$set array (gh-5403)', function(done) { + var Schema = new mongoose.Schema({ + colors: [{type: String}] + }); + + var Model = db.model('gh5403', Schema); + + Model.create({ colors: ['green'] }). + then(function() { + return Model.update({}, { $set: { colors: 'red' } }); + }). + then(function() { + return Model.collection.findOne(); + }). + then(function(doc) { + assert.deepEqual(doc.colors, ['red']); + done(); + }). + catch(done); + }); + + it('defaults with overwrite and no update validators (gh-5384)', function(done) { + var testSchema = new mongoose.Schema({ + name: String, + something: { type: Number, default: 2 } + }); + + var TestModel = db.model('gh5384', testSchema); + var options = { + overwrite: true, + upsert: true, + setDefaultsOnInsert: true + }; + + var update = { name: 'test' }; + TestModel.update({ name: 'a' }, update, options, function(error) { + assert.ifError(error); + TestModel.findOne({}, function(error, doc) { + assert.ifError(error); + assert.equal(doc.something, 2); + done(); + }); + }); + }); + + it('update validators with nested required (gh-5269)', function(done) { + var childSchema = new mongoose.Schema({ + d1: { + type: String, + required: true + }, + d2: { + type: String + } + }, { _id: false }); + + var parentSchema = new mongoose.Schema({ + d: childSchema + }); + + var Parent = db.model('gh5269', parentSchema); + + Parent.update({}, { d: { d2: 'test' } }, { runValidators: true }, function(error) { + assert.ok(error); + assert.ok(error.errors['d']); + assert.ok(error.errors['d'].message.indexOf('Path `d1` is required') !== -1, + error.errors['d'].message); + done(); + }); + }); + + it('with setOptions overwrite (gh-5413)', function(done) { + var schema = new mongoose.Schema({ + _id: String, + data: String + }, { timestamps: true }); + + var Model = db.model('gh5413', schema); + + Model. + where({ _id: 'test' }). + setOptions({ overwrite: true, upsert: true }). + update({ data: 'test2' }). + exec(). + then(function() { + done(); + }). + catch(done); + }); + + it('$push with updateValidators and top-level doc (gh-5430)', function(done) { + var notificationSchema = new mongoose.Schema({ + message: String + }); + + var Notification = db.model('gh5430_0', notificationSchema); + + var userSchema = new mongoose.Schema({ + notifications: [notificationSchema] + }); + + var User = db.model('gh5430', userSchema); + + User.update({}, { + $push: { + notifications: { + $each: [new Notification({ message: 'test' })] + } + } + }, { multi: true, runValidators: true }).exec(function(error) { + assert.ifError(error); + done(); + }); + }); + + it('$pull with updateValidators (gh-5555)', function(done) { + var notificationSchema = new mongoose.Schema({ + message: { + type: String, + maxlength: 12 + } + }); + + var userSchema = new mongoose.Schema({ + notifications: [notificationSchema] + }); + + var User = db.model('gh5555', userSchema); + + var opts = { multi: true, runValidators: true }; + var update = { + $pull: { + notifications: { + message: 'This message is wayyyyyyyyyy too long' + } + } + }; + User.create({ notifications: [{ message: 'test' }] }, function(error, doc) { + assert.ifError(error); + + User.update({}, update, opts).exec(function(error) { + assert.ok(error); + assert.ok(error.errors['notifications.message']); + + update.$pull.notifications.message = 'test'; + User.update({ _id: doc._id }, update, opts).exec(function(error) { + assert.ifError(error); + User.findById(doc._id, function(error, doc) { + assert.ifError(error); + assert.equal(doc.notifications.length, 0); + done(); + }); + }); + }); + }); + }); + + it('$pull with updateValidators and $in (gh-5744)', function(done) { + var exampleSchema = mongoose.Schema({ + subdocuments: [{ + name: String + }] + }); + var ExampleModel = db.model('gh5744', exampleSchema); + var exampleDocument = { + subdocuments: [{ name: 'First' }, { name: 'Second' }] + }; + + ExampleModel.create(exampleDocument, function(error, doc) { + assert.ifError(error); + ExampleModel.updateOne({ _id: doc._id }, { + $pull: { + subdocuments: { + _id: { $in: [doc.subdocuments[0]._id] } + } + } + }, { runValidators: true }, function(error) { + assert.ifError(error); + ExampleModel.findOne({ _id: doc._id }, function(error, doc) { + assert.ifError(error); + assert.equal(doc.subdocuments.length, 1); + done(); + }); + }); + }); + }); + + it('update with Decimal type (gh-5361)', function(done) { + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + done(); + return; + } + + test(); + }); + + function test() { + var schema = new mongoose.Schema({ + name: String, + pricing: [{ + _id: false, + program: String, + money: mongoose.Schema.Types.Decimal + }] + }); + + var Person = db.model('gh5361', schema); + + var data = { + name: 'Jack', + pricing: [ + { program: 'A', money: mongoose.Types.Decimal128.fromString('1.2') }, + { program: 'B', money: mongoose.Types.Decimal128.fromString('3.4') } + ] + }; + + Person.create(data). + then(function() { + var newData = { + name: 'Jack', + pricing: [ + { program: 'A', money: mongoose.Types.Decimal128.fromString('5.6') }, + { program: 'B', money: mongoose.Types.Decimal128.fromString('7.8') } + ] + }; + return Person.update({ name: 'Jack' }, newData); + }). + then(function() { done(); }, done); + } + }); + + it('strict false in query (gh-5453)', function(done) { + var schema = new mongoose.Schema({ + date: { type: Date, required: true } + }, { strict: true }); + + var Model = db.model('gh5453', schema); + var q = { $isolated: true }; + var u = { $set: { smth: 1 } }; + var o = { strict: false, upsert: true }; + Model.update(q, u, o).then(function() { + done(); + }).catch(done); + }); + + it('returns error if passing array as conditions (gh-3677)', function(done) { + var schema = new mongoose.Schema({ + name: String + }); + + var Model = db.model('gh3677', schema); + Model.updateMany(['foo'], { name: 'bar' }, function(error) { + assert.ok(error); + assert.equal(error.name, 'ObjectParameterError'); + var expected = 'Parameter "filter" to updateMany() must be an object'; + assert.ok(error.message.indexOf(expected) !== -1, error.message); + done(); + }); + }); + + it('upsert: 1 (gh-5839)', function(done) { + var schema = new mongoose.Schema({ + name: String + }); + + var Model = db.model('gh5839', schema); + + var opts = { upsert: 1 }; + Model.update({ name: 'Test' }, { name: 'Test2' }, opts, function(error) { + assert.ifError(error); + Model.findOne({}, function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Test2'); + done(); + }); + }); + }); + + it('update with nested id (gh-5640)', function(done) { + var testSchema = new mongoose.Schema({ + _id: { + a: String, + b: String + }, + foo: String + }, { + strict: true + }); + + var Test = db.model('gh5640', testSchema); + + var doc = { + _id: { + a: 'a', + b: 'b' + }, + foo: 'bar' + }; + + Test.create(doc, function(error, doc) { + assert.ifError(error); + doc.foo = 'baz'; + Test.update({_id: doc._id}, doc, {upsert: true}, function(error) { + assert.ifError(error); + Test.findOne({ _id: doc._id }, function(error, doc) { + assert.ifError(error); + assert.equal(doc.foo, 'baz'); + done(); + }); + }); + }); + }); + + it('cast error in update conditions (gh-5477)', function(done) { + var schema = new mongoose.Schema({ + name: String + }, { strict: true }); + + var Model = db.model('gh5477', schema); + var q = { notAField: true }; + var u = { $set: { name: 'Test' } }; + var o = { upsert: true }; + + var outstanding = 3; + + Model.update(q, u, o, function(error) { + assert.ok(error); + assert.ok(error.message.indexOf('notAField') !== -1, error.message); + assert.ok(error.message.indexOf('upsert') !== -1, error.message); + --outstanding || done(); + }); + + Model.updateOne(q, u, o, function(error) { + assert.ok(error); + assert.ok(error.message.indexOf('notAField') !== -1, error.message); + assert.ok(error.message.indexOf('upsert') !== -1, error.message); + --outstanding || done(); + }); + + Model.updateMany(q, u, o, function(error) { + assert.ok(error); + assert.ok(error.message.indexOf('notAField') !== -1, error.message); + assert.ok(error.message.indexOf('upsert') !== -1, error.message); + --outstanding || done(); + }); + }); + + it('single embedded schema under document array (gh-4519)', function(done) { + var PermissionSchema = new mongoose.Schema({ + read: { type: Boolean, required: true }, + write: Boolean + }); + var UserSchema = new mongoose.Schema({ + permission: { + type: PermissionSchema + } + }); + var GroupSchema = new mongoose.Schema({ + users: [UserSchema] + }); + + var Group = db.model('Group', GroupSchema); + var update = { + users:[{ + permission:{} + }] + }; + var opts = { + runValidators: true + }; + + Group.update({}, update, opts, function(error) { + assert.ok(error); + assert.ok(error.errors['users.0.permission']); + done(); + }); }); }); }); diff --git a/test/object.create.null.test.js b/test/object.create.null.test.js index 39f6bcdc02a..473427a95dc 100644 --- a/test/object.create.null.test.js +++ b/test/object.create.null.test.js @@ -1,28 +1,30 @@ - - /** * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , Schema = mongoose.Schema; - -var schema = new Schema({ - a: String - , b: { - c: Number - , d: [{ e: String }] - } - , f: { g: Date } - , h: {} -}); +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + Schema = mongoose.Schema; + +var schema; describe('is compatible with object created using Object.create(null) (gh-1484)', function() { var db; var M; + before(function() { + schema = new Schema({ + a: String, + b: { + c: Number, + d: [{e: String}] + }, + f: {g: Date}, + h: {} + }); + }); + before(function() { db = start(); M = db.model('1484', schema); @@ -66,12 +68,12 @@ describe('is compatible with object created using Object.create(null) (gh-1484)' var m = new M(o); - assert.equal(9, m.b.c); - assert.equal('hi i am a string', m.b.d[0].e); + assert.equal(m.b.c, 9); + assert.equal(m.b.d[0].e, 'hi i am a string'); assert.equal(date, m.f.g); - assert.equal(1, m.h.ad); - assert.equal(2, m.h.hoc); - assert.deepEqual({},m.h.obj); + assert.equal(m.h.ad, 1); + assert.equal(m.h.hoc, 2); + assert.deepEqual({}, m.h.obj); }); done(); @@ -98,8 +100,8 @@ describe('is compatible with object created using Object.create(null) (gh-1484)' thing.h = 'yes'; m.set('h.obj.thing', thing); - assert.equal(9, m.b.c); - assert.equal('hi i am a string', m.b.d[0].e); + assert.equal(m.b.c, 9); + assert.equal(m.b.d[0].e, 'hi i am a string'); assert.equal(date, m.f.g); assert.deepEqual('yes', m.h.obj.thing.h); done(); @@ -128,7 +130,7 @@ describe('is compatible with object created using Object.create(null) (gh-1484)' var o = Object.create(null); o = {}; o.name = String; - var x = { type: [o] }; + var x = {type: [o]}; s.path('works', x); }); diff --git a/test/parse.options.test.js b/test/parse.options.test.js new file mode 100644 index 00000000000..b9975e168cf --- /dev/null +++ b/test/parse.options.test.js @@ -0,0 +1,57 @@ +var mongoose = require('../'); +var assert = require('power-assert'); + +describe('parseOptions', function() { + it('should not mutate user passed options map', function() { + var db = new mongoose.Connection(); + var now = Date.now(); + + var userPassedOptionsMap = Object.create(null, { + auth: { + value: {}, + enumerable: true + }, + prop_num: { + value: now, + enumerable: true + }, + prop_obj: { + value: {}, + enumerable: true + } + }); + var ultimateOptionsMap; + + ultimateOptionsMap = db.parseOptions(userPassedOptionsMap); + + assert.notEqual(ultimateOptionsMap, userPassedOptionsMap); + assert.deepStrictEqual(userPassedOptionsMap, Object.create(null, { + auth: { + value: {}, + enumerable: true + }, + prop_num: { + value: now, + enumerable: true + }, + prop_obj: { + value: {}, + enumerable: true + } + })); + assert.notDeepStrictEqual(ultimateOptionsMap, Object.create(null, { + auth: { + value: {}, + enumerable: true + }, + prop_num: { + value: now, + enumerable: true + }, + prop_obj: { + value: {}, + enumerable: true + } + })); + }); +}); diff --git a/test/plugin.idGetter.test.js b/test/plugin.idGetter.test.js new file mode 100644 index 00000000000..f68d930db14 --- /dev/null +++ b/test/plugin.idGetter.test.js @@ -0,0 +1,59 @@ + +/** + * Module dependencies. + */ + +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema; + +describe('id virtual getter', function() { + it('should work as expected with an ObjectId', function(done) { + var db = start(); + + var schema = new Schema({}); + + var S = db.model('Basic', schema); + S.create({}, function(err, s) { + assert.ifError(err); + + // Comparing with virtual getter + assert.equal(s._id.toString(), s.id); + done(); + }); + }); + + it('should be turned off when `id` option is set to false', function(done) { + var db = start(); + + var schema = new Schema({}, {id: false}); + + var S = db.model('NoIdGetter', schema); + S.create({}, function(err, s) { + assert.ifError(err); + + // Comparing with virtual getter + assert.equal(s.id, undefined); + done(); + }); + }); + + + it('should be turned off when the schema has a set `id` path', function(done) { + var db = start(); + + var schema = new Schema({ + id: String + }); + + var S = db.model('NoIdGetter', schema); + S.create({ id: 'test'}, function(err, s) { + assert.ifError(err); + + // Comparing with expected value + assert.equal(s.id, 'test'); + done(); + }); + }); +}); diff --git a/test/promise.test.js b/test/promise.test.js index 7d9c92a38e0..4efc55d15ed 100644 --- a/test/promise.test.js +++ b/test/promise.test.js @@ -1,9 +1,8 @@ - /** * Module dependencies. */ -var assert = require('assert'); +var assert = require('power-assert'); var Promise = require('../lib/promise'); @@ -13,8 +12,8 @@ var Promise = require('../lib/promise'); describe('Promise', function() { it('events fire right after complete()', function(done) { - var promise = new Promise() - , called = 0; + var promise = new Promise(), + called = 0; promise.on('fulfill', function(a, b) { assert.equal(a, '1'); @@ -30,13 +29,13 @@ describe('Promise', function() { called++; }); - assert.equal(2, called); + assert.equal(called, 2); done(); }); it('events fire right after error()', function(done) { - var promise = new Promise() - , called = 0; + var promise = new Promise(), + called = 0; promise.on('reject', function(err) { assert.ok(err instanceof Error); @@ -50,27 +49,27 @@ describe('Promise', function() { called++; }); - assert.equal(2, called); + assert.equal(called, 2); done(); }); it('events fire right after reject()', function(done) { - var promise = new Promise() - , called = 0; + var promise = new Promise(), + called = 0; promise.on('reject', function(err) { - assert.equal(9, err); + assert.equal(err, 9); called++; }); promise.reject(9); promise.on('reject', function(err) { - assert.equal(9, err); + assert.equal(err, 9); called++; }); - assert.equal(2, called); + assert.equal(called, 2); done(); }); @@ -85,18 +84,18 @@ describe('Promise', function() { promise.reject(new Error('dawg')); - assert.equal(1, called); + assert.equal(called, 1); done(); }); it('after fulfill()', function(done) { - var promise = new Promise() - , called = 0; + var promise = new Promise(), + called = 0; promise.fulfill('woot'); promise.onResolve(function(err, data) { - assert.equal(data,'woot'); + assert.equal(data, 'woot'); called++; }); @@ -105,13 +104,13 @@ describe('Promise', function() { called++; }); - assert.equal(2, called); + assert.equal(called, 2); done(); }); it('after error()', function(done) { - var promise = new Promise() - , called = 0; + var promise = new Promise(), + called = 0; promise.error(new Error('woot')); @@ -124,15 +123,15 @@ describe('Promise', function() { assert.ok(err instanceof Error); called++; }); - assert.equal(2, called); + assert.equal(called, 2); done(); }); }); describe('onFulfill() shortcut', function() { it('works', function(done) { - var promise = new Promise() - , called = 0; + var promise = new Promise(), + called = 0; promise.onFulfill(function(woot) { assert.strictEqual(woot, undefined); @@ -141,15 +140,15 @@ describe('Promise', function() { promise.fulfill(); - assert.equal(1, called); + assert.equal(called, 1); done(); }); }); describe('onReject shortcut', function() { it('works', function(done) { - var promise = new Promise() - , called = 0; + var promise = new Promise(), + called = 0; promise.onReject(function(err) { assert.ok(err instanceof Error); @@ -157,7 +156,7 @@ describe('Promise', function() { }); promise.reject(new Error); - assert.equal(1, called); + assert.equal(called, 1); done(); }); }); @@ -191,7 +190,7 @@ describe('Promise', function() { it('casts arguments to Error', function(done) { var p = new Promise(function(err) { assert.ok(err instanceof Error); - assert.equal('3', err.message); + assert.equal(err.message, '3'); done(); }); @@ -202,7 +201,7 @@ describe('Promise', function() { describe('reject()', function() { it('does not cast arguments to Error', function(done) { var p = new Promise(function(err) { - assert.equal(3, err); + assert.equal(err, 3); done(); }); @@ -214,9 +213,26 @@ describe('Promise', function() { it('doesnt swallow exceptions (gh-3222)', function(done) { assert.throws(function() { new Promise.ES6(function() { - throw 'bacon'; + throw new Error('bacon'); }); }); done(); }); + + it('.catch() works correctly (gh-4189)', function(done) { + var promise = new Promise.ES6(function(resolve, reject) { + reject(new Error('error1')); + }); + promise. + catch(function(error) { + assert.ok(error); + return new Promise.ES6(function(resolve, reject) { + reject(new Error('error2')); + }); + }). + catch(function(error) { + assert.equal(error.message, 'error2'); + done(); + }); + }); }); diff --git a/test/promise_provider.test.js b/test/promise_provider.test.js index baff19046ca..f60e36a995e 100644 --- a/test/promise_provider.test.js +++ b/test/promise_provider.test.js @@ -1,10 +1,8 @@ -/* eslint no-unused-vars: 1 */ - /** * Module dependencies. */ -var assert = require('assert'); +var assert = require('power-assert'); var bluebird = require('bluebird'); var q = require('q'); var start = require('./common'); @@ -12,30 +10,34 @@ var start = require('./common'); var PromiseProvider = require('../lib/promise_provider'); var Schema = require('../lib/schema'); -var Promise; var db; -var testSchema = new Schema({ test: { type: String, required: true } }); -testSchema.pre('save', function(next) { - if (this.$__saveSucceeds === false) { - return next(new Error('fail')); - } - next(); -}); -testSchema.pre('validate', function(next) { - if (this.$__validateSucceeds === false) { - return next(new Error('validation failed')); - } - next(); -}); -testSchema.pre('findOne', function(next) { - if (this.$__findOneSucceeds === false) { - return next(new Error('findOne failed')); - } - next(); -}); -var MyModel; describe('ES6 promises: ', function() { + var testSchema; + var MyModel; + + before(function() { + testSchema = new Schema({test: {type: String, required: true}}); + testSchema.pre('save', function(next) { + if (this.$__saveSucceeds === false) { + return next(new Error('fail')); + } + next(); + }); + testSchema.pre('validate', function(next) { + if (this.$__validateSucceeds === false) { + return next(new Error('validation failed')); + } + next(); + }); + testSchema.pre('findOne', function(next) { + if (this.$__findOneSucceeds === false) { + return next(new Error('findOne failed')); + } + next(); + }); + }); + describe('native: ', function() { if (!global.Promise) { return; @@ -43,7 +45,6 @@ describe('ES6 promises: ', function() { before(function() { PromiseProvider.set(global.Promise); - Promise = PromiseProvider.get(); }); before(function() { @@ -51,8 +52,9 @@ describe('ES6 promises: ', function() { MyModel = db.model('es6promise', testSchema); }); - after(function() { + after(function(done) { PromiseProvider.reset(); + db.close(done); }); afterEach(function(done) { @@ -60,7 +62,7 @@ describe('ES6 promises: ', function() { }); it('save()', function(done) { - var m = new MyModel({ test: '123' }); + var m = new MyModel({test: '123'}); var promise = m.save(); assert.equal(promise.constructor, global.Promise); promise.then(function(doc) { @@ -79,13 +81,13 @@ describe('ES6 promises: ', function() { }). catch(function(err) { assert.ok(err); - assert.ok(err.errors['test']); + assert.ok(err.errors.test); done(); }); }); it('save() with middleware error', function(done) { - var m = new MyModel({ test: '123' }); + var m = new MyModel({test: '123'}); m.$__saveSucceeds = false; var promise = m.save(); assert.equal(promise.constructor, global.Promise); @@ -101,7 +103,7 @@ describe('ES6 promises: ', function() { }); it('save() with validation middleware error', function(done) { - var m = new MyModel({ test: '123' }); + var m = new MyModel({test: '123'}); m.$__validateSucceeds = false; var promise = m.save(); assert.equal(promise.constructor, global.Promise); @@ -126,16 +128,16 @@ describe('ES6 promises: ', function() { }). catch(function(err) { assert.ok(err); - assert.ok(err.errors['test']); + assert.ok(err.errors.test); done(); }); }); it('queries', function(done) { - MyModel.create({ test: '123' }, function(error) { + MyModel.create({test: '123'}, function(error) { assert.ifError(error); - var promise = MyModel.findOne({ test: '123' }).exec(); + var promise = MyModel.findOne({test: '123'}).exec(); assert.equal(promise.constructor, global.Promise); promise.then(function(doc) { @@ -146,10 +148,10 @@ describe('ES6 promises: ', function() { }); it('queries with errors', function(done) { - MyModel.create({ test: '123' }, function(error) { + MyModel.create({test: '123'}, function(error) { assert.ifError(error); - var query = MyModel.findOne({ test: '123' }); + var query = MyModel.findOne({test: '123'}); query.$__findOneSucceeds = false; var promise = query.exec(); assert.equal(promise.constructor, global.Promise); @@ -167,7 +169,7 @@ describe('ES6 promises: ', function() { }); it('create', function(done) { - var promise = MyModel.create({ test: '123' }); + var promise = MyModel.create({test: '123'}); assert.equal(promise.constructor, global.Promise); promise.then(function() { done(); @@ -178,7 +180,6 @@ describe('ES6 promises: ', function() { describe('bluebird: ', function() { before(function() { PromiseProvider.set(bluebird); - Promise = PromiseProvider.get(); }); before(function() { @@ -186,8 +187,9 @@ describe('ES6 promises: ', function() { MyModel = db.model('es6promise_bluebird', testSchema); }); - after(function() { + after(function(done) { PromiseProvider.reset(); + db.close(done); }); afterEach(function(done) { @@ -200,7 +202,13 @@ describe('ES6 promises: ', function() { assert.equal(promise.constructor, bluebird); promise.then(function(doc) { assert.equal(m, doc); - done(); + m.test = '456'; + m.save(function(error, doc, numAffected) { + assert.ifError(error); + assert.ok(doc); + assert.equal(numAffected, 1); + done(); + }); }); }); @@ -214,7 +222,7 @@ describe('ES6 promises: ', function() { }). catch(function(err) { assert.ok(err); - assert.ok(err.errors['test']); + assert.ok(err.errors.test); done(); }); }); @@ -231,12 +239,18 @@ describe('ES6 promises: ', function() { catch(function(err) { assert.ok(err); assert.equal(err.toString(), 'Error: fail'); - done(); + + // Shouldn't log an unhandled rejection error + m.save(function(err) { + assert.ok(err); + assert.equal(err.toString(), 'Error: fail'); + done(); + }); }); }); it('save() with validation middleware error', function(done) { - var m = new MyModel({ test: '123' }); + var m = new MyModel({test: '123'}); m.$__validateSucceeds = false; var promise = m.save(); assert.equal(promise.constructor, bluebird); @@ -261,16 +275,16 @@ describe('ES6 promises: ', function() { }). catch(function(err) { assert.ok(err); - assert.ok(err.errors['test']); + assert.ok(err.errors.test); done(); }); }); it('queries', function(done) { - MyModel.create({ test: '123' }, function(error) { + MyModel.create({test: '123'}, function(error) { assert.ifError(error); - var promise = MyModel.findOne({ test: '123' }).exec(); + var promise = MyModel.findOne({test: '123'}).exec(); assert.equal(promise.constructor, bluebird); promise.then(function(doc) { @@ -281,10 +295,10 @@ describe('ES6 promises: ', function() { }); it('queries with errors', function(done) { - MyModel.create({ test: '123' }, function(error) { + MyModel.create({test: '123'}, function(error) { assert.ifError(error); - var query = MyModel.findOne({ test: '123' }); + var query = MyModel.findOne({test: '123'}); query.$__findOneSucceeds = false; var promise = query.exec(); assert.equal(promise.constructor, bluebird); @@ -301,11 +315,19 @@ describe('ES6 promises: ', function() { }); }); + it('no unhandled rejection on query w/ cb (gh-4379)', function(done) { + var query = MyModel.findOne({test: '123'}); + query.$__findOneSucceeds = false; + query.exec(function(error) { + assert.ok(error); + done(); + }); + }); + it('create', function(done) { - var promise = MyModel.create({ test: '123' }); + var promise = MyModel.create({test: '123'}); assert.equal(promise.constructor, bluebird); promise.then(function() { - var p = MyModel.create({}); p.catch(function(error) { assert.ok(error); @@ -313,12 +335,88 @@ describe('ES6 promises: ', function() { }); }); }); + + it('subdocument validation (gh-3681)', function(done) { + var subSchema = new Schema({name: {type: String, required: true}}); + var parentSchema = new Schema({sub: [subSchema]}); + var Parent = db.model('gh3681', parentSchema); + + Parent.create({sub: [{}]}).catch(function() { + done(); + }); + }); + + it('Model.populate (gh-3734)', function(done) { + var doc = new MyModel({}); + var promise = MyModel.populate(doc, 'test'); + assert.equal(promise.constructor, bluebird); + done(); + }); + + it('gh-4177', function(done) { + var subSchema = new Schema({ + name: { type: String, required: true } + }); + + var mainSchema = new Schema({ + name: String, + type: String, + children: [subSchema] + }); + + mainSchema.index({ name: 1, account: 1 }, { unique: true }); + + var Main = db.model('gh4177', mainSchema); + + Main.on('index', function(error) { + assert.ifError(error); + + var data = { + name: 'foo', + type: 'bar', + children: [{ name: 'child' }] + }; + + var firstSucceeded = false; + new Main(data). + save(). + then(function() { + firstSucceeded = true; + return new Main(data).save(); + }). + catch(function(error) { + assert.ok(firstSucceeded); + assert.ok(error.toString().indexOf('E11000') !== -1); + done(); + }); + }); + }); + + it('subdoc pre doesnt cause unhandled rejection (gh-3669)', function(done) { + var nestedSchema = new Schema({ + name: {type: String, required: true} + }); + + nestedSchema.pre('validate', function(next) { + next(); + }); + + var schema = new Schema({ + items: [nestedSchema] + }); + + var MyModel = db.model('gh3669', schema); + + MyModel.create({items: [{name: null}]}).catch(function(error) { + assert.ok(error); + done(); + }); + }); }); describe('q: ', function() { before(function() { PromiseProvider.set(q.Promise); - Promise = PromiseProvider.get(); }); before(function() { @@ -326,8 +424,9 @@ describe('ES6 promises: ', function() { MyModel = db.model('es6promise_q', testSchema); }); - after(function() { + after(function(done) { PromiseProvider.reset(); + db.close(done); }); afterEach(function(done) { @@ -335,7 +434,7 @@ describe('ES6 promises: ', function() { }); it('save()', function(done) { - var m = new MyModel({ test: '123' }); + var m = new MyModel({test: '123'}); var promise = m.save(); assert.ok(promise instanceof q.makePromise); promise.then(function(doc) { @@ -354,13 +453,13 @@ describe('ES6 promises: ', function() { }). catch(function(err) { assert.ok(err); - assert.ok(err.errors['test']); + assert.ok(err.errors.test); done(); }); }); it('save() with middleware error', function(done) { - var m = new MyModel({ test: '123' }); + var m = new MyModel({test: '123'}); m.$__saveSucceeds = false; var promise = m.save(); assert.ok(promise instanceof q.makePromise); @@ -376,7 +475,7 @@ describe('ES6 promises: ', function() { }); it('save() with validation middleware error', function(done) { - var m = new MyModel({ test: '123' }); + var m = new MyModel({test: '123'}); m.$__validateSucceeds = false; var promise = m.save(); assert.ok(promise instanceof q.makePromise); @@ -401,16 +500,16 @@ describe('ES6 promises: ', function() { }). catch(function(err) { assert.ok(err); - assert.ok(err.errors['test']); + assert.ok(err.errors.test); done(); }); }); it('queries', function(done) { - MyModel.create({ test: '123' }, function(error) { + MyModel.create({test: '123'}, function(error) { assert.ifError(error); - var promise = MyModel.findOne({ test: '123' }).exec(); + var promise = MyModel.findOne({test: '123'}).exec(); assert.ok(promise instanceof q.makePromise); promise.then(function(doc) { @@ -421,10 +520,10 @@ describe('ES6 promises: ', function() { }); it('queries with errors', function(done) { - MyModel.create({ test: '123' }, function(error) { + MyModel.create({test: '123'}, function(error) { assert.ifError(error); - var query = MyModel.findOne({ test: '123' }); + var query = MyModel.findOne({test: '123'}); query.$__findOneSucceeds = false; var promise = query.exec(); assert.ok(promise instanceof q.makePromise); @@ -442,7 +541,7 @@ describe('ES6 promises: ', function() { }); it('create', function(done) { - var promise = MyModel.create({ test: '123' }); + var promise = MyModel.create({test: '123'}); assert.ok(promise instanceof q.makePromise); promise.then(function() { done(); diff --git a/test/query.cursor.test.js b/test/query.cursor.test.js new file mode 100644 index 00000000000..926d5d22b1d --- /dev/null +++ b/test/query.cursor.test.js @@ -0,0 +1,435 @@ +/** + * Module dependencies. + */ + +var assert = require('assert'); +var start = require('./common'); + +var mongoose = start.mongoose; +var Schema = mongoose.Schema; + +describe('QueryCursor', function() { + var db; + var Model; + + before(function(done) { + db = start(); + + var schema = new Schema({ name: String }); + schema.virtual('test').get(function() { return 'test'; }); + + Model = db.model('gh1907_0', schema); + + Model.create({ name: 'Axl' }, { name: 'Slash' }, function(error) { + assert.ifError(error); + done(); + }); + }); + + after(function(done) { + db.close(done); + }); + + describe('#next()', function() { + it('with callbacks', function(done) { + var cursor = Model.find().sort({ name: 1 }).cursor(); + cursor.next(function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Axl'); + assert.equal(doc.test, 'test'); + cursor.next(function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Slash'); + assert.equal(doc.test, 'test'); + done(); + }); + }); + }); + + it('with promises', function(done) { + var cursor = Model.find().sort({ name: 1 }).cursor(); + cursor.next().then(function(doc) { + assert.equal(doc.name, 'Axl'); + assert.equal(doc.test, 'test'); + cursor.next().then(function(doc) { + assert.equal(doc.name, 'Slash'); + assert.equal(doc.test, 'test'); + done(); + }); + }); + }); + + it('with limit (gh-4266)', function(done) { + var cursor = Model.find().limit(1).sort({ name: 1 }).cursor(); + cursor.next(function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Axl'); + cursor.next(function(error, doc) { + assert.ifError(error); + assert.ok(!doc); + done(); + }); + }); + }); + + it('with projection', function(done) { + var personSchema = new Schema({ + name: String, + born: String + }); + var Person = db.model('Person4342', personSchema); + var people = [ + { name: 'Axl Rose', born: 'William Bruce Rose' }, + { name: 'Slash', born: 'Saul Hudson' } + ]; + Person.create(people, function(error) { + assert.ifError(error); + var cursor = Person.find({}, { _id: 0, name: 1 }).sort({ name: 1 }).cursor(); + cursor.next(function(error, doc) { + assert.ifError(error); + assert.equal(doc._id, undefined); + assert.equal(doc.name, 'Axl Rose'); + assert.equal(doc.born, undefined); + cursor.next(function(error, doc) { + assert.ifError(error); + assert.equal(doc._id, undefined); + assert.equal(doc.name, 'Slash'); + assert.equal(doc.born, undefined); + done(); + }); + }); + }); + }); + + it('with populate', function(done) { + var bandSchema = new Schema({ + name: String, + members: [{ type: mongoose.Schema.ObjectId, ref: 'Person1907' }] + }); + var personSchema = new Schema({ + name: String + }); + + var Person = db.model('Person1907', personSchema); + var Band = db.model('Band1907', bandSchema); + + var people = [ + { name: 'Axl Rose' }, + { name: 'Slash' }, + { name: 'Nikki Sixx' }, + { name: 'Vince Neil' } + ]; + Person.create(people, function(error, docs) { + assert.ifError(error); + var bands = [ + { name: 'Guns N\' Roses', members: [docs[0], docs[1]] }, + { name: 'Motley Crue', members: [docs[2], docs[3]] } + ]; + Band.create(bands, function(error) { + assert.ifError(error); + var cursor = + Band.find().sort({ name: 1 }).populate('members').cursor(); + cursor.next(function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Guns N\' Roses'); + assert.equal(doc.members.length, 2); + assert.equal(doc.members[0].name, 'Axl Rose'); + assert.equal(doc.members[1].name, 'Slash'); + cursor.next(function(error, doc) { + assert.equal(doc.name, 'Motley Crue'); + assert.equal(doc.members.length, 2); + assert.equal(doc.members[0].name, 'Nikki Sixx'); + assert.equal(doc.members[1].name, 'Vince Neil'); + done(); + }); + }); + }); + }); + }); + + it('casting ObjectIds with where() (gh-4355)', function(done) { + Model.findOne(function(error, doc) { + assert.ifError(error); + assert.ok(doc); + var query = { _id: doc._id.toHexString() }; + Model.find().where(query).cursor().next(function(error, doc) { + assert.ifError(error); + assert.ok(doc); + done(); + }); + }); + }); + + it('cast errors (gh-4355)', function(done) { + Model.find().where({ _id: 'BadId' }).cursor().next(function(error) { + assert.ok(error); + assert.equal(error.name, 'CastError'); + assert.equal(error.path, '_id'); + done(); + }); + }); + + it('with pre-find hooks (gh-5096)', function(done) { + var schema = new Schema({ name: String }); + var called = 0; + schema.pre('find', function(next) { + ++called; + next(); + }); + + var Model = db.model('gh5096', schema); + Model.create({ name: 'Test' }, function(error) { + assert.ifError(error); + Model.find().cursor().next(function(error, doc) { + assert.ifError(error); + assert.equal(called, 1); + assert.equal(doc.name, 'Test'); + done(); + }); + }); + }); + }); + + it('as readable stream', function(done) { + var cursor = Model.find().sort({ name: 1 }).cursor(); + + var expectedNames = ['Axl', 'Slash']; + var cur = 0; + cursor.on('data', function(doc) { + assert.equal(doc.name, expectedNames[cur++]); + assert.equal(doc.test, 'test'); + }); + + cursor.on('error', function(error) { + done(error); + }); + + cursor.on('end', function() { + assert.equal(cur, 2); + done(); + }); + }); + + describe('`transform` option', function() { + it('transforms document', function(done) { + var cursor = Model.find().sort({ name: 1 }).cursor({ + transform: function(doc) { + doc.name += '_transform'; + return doc; + } + }); + + var expectedNames = ['Axl_transform', 'Slash_transform']; + var cur = 0; + cursor.on('data', function(doc) { + assert.equal(doc.name, expectedNames[cur++]); + assert.equal(doc.test, 'test'); + }); + + cursor.on('error', function(error) { + done(error); + }); + + cursor.on('end', function() { + assert.equal(cur, 2); + done(); + }); + }); + }); + + describe('#map', function() { + it('maps documents', function(done) { + var cursor = Model.find().sort({ name: 1 }).cursor() + .map(function(obj) { + obj.name += '_mapped'; + return obj; + }) + .map(function(obj) { + obj.name += '_mappedagain'; + return obj; + }); + + var expectedNames = ['Axl_mapped_mappedagain', 'Slash_mapped_mappedagain']; + var cur = 0; + cursor.on('data', function(doc) { + assert.equal(doc.name, expectedNames[cur++]); + assert.equal(doc.test, 'test'); + }); + + cursor.on('error', function(error) { + done(error); + }); + + cursor.on('end', function() { + assert.equal(cur, 2); + done(); + }); + }); + + it('with #next', function(done) { + var cursor = Model.find().sort({ name: 1 }).cursor() + .map(function(obj) { + obj.name += '_next'; + return obj; + }); + + cursor.next(function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Axl_next'); + assert.equal(doc.test, 'test'); + cursor.next(function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Slash_next'); + assert.equal(doc.test, 'test'); + done(); + }); + }); + }); + }); + + describe('#eachAsync()', function() { + it('iterates one-by-one, stopping for promises', function(done) { + var cursor = Model.find().sort({ name: 1 }).cursor(); + + var expectedNames = ['Axl', 'Slash']; + var cur = 0; + + var checkDoc = function(doc) { + var _cur = cur; + assert.equal(doc.name, expectedNames[cur]); + return { + then: function(onResolve) { + setTimeout(function() { + assert.equal(_cur, cur++); + onResolve(); + }, 50); + } + }; + }; + cursor.eachAsync(checkDoc).then(function() { + assert.equal(cur, 2); + done(); + }).catch(done); + }); + + it('parallelization', function(done) { + var cursor = Model.find().sort({ name: 1 }).cursor(); + + var names = []; + var startedAt = []; + var checkDoc = function(doc) { + names.push(doc.name); + startedAt.push(Date.now()); + return { + then: function(onResolve) { + setTimeout(function() { + onResolve(); + }, 100); + } + }; + }; + cursor.eachAsync(checkDoc, { parallel: 2 }).then(function() { + assert.ok(Date.now() - startedAt[1] > 100); + assert.equal(startedAt.length, 2); + assert.ok(startedAt[1] - startedAt[0] < 50); + assert.deepEqual(names.sort(), ['Axl', 'Slash']); + done(); + }).catch(done); + }); + }); + + describe('#lean()', function() { + it('lean', function(done) { + var cursor = Model.find().sort({ name: 1 }).lean().cursor(); + + var expectedNames = ['Axl', 'Slash']; + var cur = 0; + cursor.on('data', function(doc) { + assert.equal(doc.name, expectedNames[cur++]); + assert.strictEqual(false, doc instanceof mongoose.Document); + }); + + cursor.on('error', function(error) { + done(error); + }); + + cursor.on('end', function() { + assert.equal(cur, 2); + done(); + }); + + }); + }); + + describe('#close()', function() { + it('works (gh-4258)', function(done) { + var cursor = Model.find().sort({ name: 1 }).cursor(); + cursor.next(function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Axl'); + assert.equal(doc.test, 'test'); + + var closed = false; + cursor.on('close', function() { + closed = true; + }); + + cursor.close(function(error) { + assert.ifError(error); + assert.ok(closed); + cursor.next(function(error) { + assert.ok(error); + assert.equal(error.message, 'Cursor is closed'); + done(); + }); + }); + }); + }); + }); + + it('addCursorFlag (gh-4814)', function(done) { + var userSchema = new mongoose.Schema({ + name: String + }); + + var User = db.model('gh4814', userSchema); + + var cursor = User.find().cursor().addCursorFlag('noCursorTimeout', true); + + cursor.on('cursor', function() { + assert.equal(cursor.cursor.s.cmd.noCursorTimeout, true); + done(); + }); + }); + + it('data before close (gh-4998)', function(done) { + var userSchema = new mongoose.Schema({ + name: String + }); + + var User = db.model('gh4998', userSchema); + var users = []; + for (var i = 0; i < 100; i++) { + users.push({ + _id: mongoose.Types.ObjectId(), + name: 'Bob' + (i < 10 ? '0' : '') + i + }); + } + + User.insertMany(users, function(error) { + assert.ifError(error); + + var stream = User.find({}).cursor(); + var docs = []; + + stream.on('data', function(doc) { + docs.push(doc); + }); + + stream.on('close', function() { + assert.equal(docs.length, 100); + done(); + }); + }); + }); +}); diff --git a/test/query.middleware.test.js b/test/query.middleware.test.js index e7a46db7b96..cc9672de8b0 100644 --- a/test/query.middleware.test.js +++ b/test/query.middleware.test.js @@ -1,5 +1,5 @@ var start = require('./common'); -var assert = require('assert'); +var assert = require('power-assert'); var mongoose = start.mongoose; var Schema = mongoose.Schema; @@ -23,7 +23,7 @@ describe('query middleware', function() { if (error) { return done(error); } - Publisher.create({ name: 'Wiley' }, function(error, publisher) { + Publisher.create({name: 'Wiley'}, function(error, publisher) { if (error) { return done(error); } @@ -47,7 +47,7 @@ describe('query middleware', function() { schema = new Schema({ title: String, author: String, - publisher: { type: Schema.ObjectId, ref: 'gh-2138-1' }, + publisher: {type: Schema.ObjectId, ref: 'gh-2138-1'}, options: String }); @@ -75,9 +75,9 @@ describe('query middleware', function() { initializeData(function(error) { assert.ifError(error); - Author.find({ x: 1 }, function(error) { + Author.find({x: 1}, function(error) { assert.ifError(error); - assert.equal(1, count); + assert.equal(count, 1); done(); }); }); @@ -86,19 +86,19 @@ describe('query middleware', function() { it('has post find hooks', function(done) { var postCount = 0; schema.post('find', function(results, next) { - assert.equal(1, results.length); - assert.equal('Val', results[0].author); - assert.equal('bacon', results[0].options); + assert.equal(results.length, 1); + assert.equal(results[0].author, 'Val'); + assert.equal(results[0].options, 'bacon'); ++postCount; next(); }); initializeData(function(error) { assert.ifError(error); - Author.find({ title: 'Professional AngularJS' }, function(error, docs) { + Author.find({title: 'Professional AngularJS'}, function(error, docs) { assert.ifError(error); - assert.equal(1, postCount); - assert.equal(1, docs.length); + assert.equal(postCount, 1); + assert.equal(docs.length, 1); done(); }); }); @@ -113,18 +113,18 @@ describe('query middleware', function() { var postCount = 0; schema.post('find', function(results, next) { - assert.equal(1, results.length); - assert.equal('Val', results[0].author); + assert.equal(results.length, 1); + assert.equal(results[0].author, 'Val'); ++postCount; next(); }); initializeData(function() { - Author.find({ title: 'Professional AngularJS' }).exec(function(error, docs) { + Author.find({title: 'Professional AngularJS'}).exec(function(error, docs) { assert.ifError(error); - assert.equal(1, count); - assert.equal(1, postCount); - assert.equal(1, docs.length); + assert.equal(count, 1); + assert.equal(postCount, 1); + assert.equal(docs.length, 1); done(); }); }); @@ -139,17 +139,17 @@ describe('query middleware', function() { var postCount = 0; schema.post('findOne', function(result, next) { - assert.equal('Val', result.author); + assert.equal(result.author, 'Val'); ++postCount; next(); }); initializeData(function() { - Author.findOne({ title: 'Professional AngularJS' }).exec(function(error, doc) { + Author.findOne({title: 'Professional AngularJS'}).exec(function(error, doc) { assert.ifError(error); - assert.equal(1, count); - assert.equal(1, postCount); - assert.equal('Val', doc.author); + assert.equal(count, 1); + assert.equal(postCount, 1); + assert.equal(doc.author, 'Val'); done(); }); }); @@ -162,10 +162,10 @@ describe('query middleware', function() { }); initializeData(function() { - Author.findOne({ title: 'Professional AngularJS' }).exec(function(error, doc) { + Author.findOne({title: 'Professional AngularJS'}).exec(function(error, doc) { assert.ifError(error); - assert.equal('Val', doc.author); - assert.equal('Wiley', doc.publisher.name); + assert.equal(doc.author, 'Val'); + assert.equal(doc.publisher.name, 'Wiley'); done(); }); }); @@ -179,10 +179,10 @@ describe('query middleware', function() { }); initializeData(function() { - Author.findOne({ title: 'Professional AngularJS' }).exec(function(error, doc) { + Author.findOne({title: 'Professional AngularJS'}).exec(function(error, doc) { assert.ifError(error); - assert.equal('Val', doc.author); - assert.equal('Wiley', doc.publisher.name); + assert.equal(doc.author, 'Val'); + assert.equal(doc.publisher.name, 'Wiley'); done(); }); }); @@ -202,13 +202,223 @@ describe('query middleware', function() { initializeData(function(error) { assert.ifError(error); - Author.find({ title: 'Professional AngularJS' }).count(function(error, count) { + Author. + find({ title: 'Professional AngularJS' }). + count(function(error, count) { + assert.ifError(error); + assert.equal(1, count); + assert.equal(1, preCount); + assert.equal(1, postCount); + done(); + }); + }); + }); + + it('updateOne() (gh-3997)', function(done) { + var preCount = 0; + var postCount = 0; + + schema.pre('updateOne', function() { + ++preCount; + }); + + schema.post('updateOne', function() { + ++postCount; + }); + + initializeData(function(error) { + assert.ifError(error); + Author. + updateOne({}, { author: 'updatedOne' }). + exec(function(error) { + assert.ifError(error); + assert.equal(preCount, 1); + assert.equal(postCount, 1); + Author.find({ author: 'updatedOne' }, function(error, res) { + assert.ifError(error); + assert.equal(res.length, 1); + done(); + }); + }); + }); + }); + + it('updateMany() (gh-3997)', function(done) { + var preCount = 0; + var postCount = 0; + + schema.pre('updateMany', function() { + ++preCount; + }); + + schema.post('updateMany', function() { + ++postCount; + }); + + initializeData(function(error) { + assert.ifError(error); + + Author.create({ author: 'test' }, function(error) { + assert.ifError(error); + Author. + updateMany({}, { author: 'updatedMany' }). + exec(function(error) { + assert.ifError(error); + assert.equal(preCount, 1); + assert.equal(postCount, 1); + Author.find({}, function(error, res) { + assert.ifError(error); + assert.ok(res.length > 1); + res.forEach(function(doc) { + assert.equal(doc.author, 'updatedMany'); + }); + done(); + }); + }); + }); + }); + }); + + it('error handlers (gh-2284)', function(done) { + var testSchema = new Schema({ title: { type: String, unique: true } }); + + testSchema.post('update', function(error, res, next) { + assert.ok(error); + assert.ok(!res); + next(new Error('woops')); + }); + + var Book = db.model('gh2284', testSchema); + + Book.on('index', function(error) { + assert.ifError(error); + var books = [ + { title: 'Professional AngularJS' }, + { title: 'The 80/20 Guide to ES2015 Generators' } + ]; + Book.create(books, function(error, books) { assert.ifError(error); - assert.equal(1, count); - assert.equal(1, preCount); - assert.equal(1, postCount); + var query = { _id: books[1]._id }; + var update = { title: 'Professional AngularJS' }; + Book.update(query, update, function(error) { + assert.equal(error.message, 'woops'); + done(); + }); + }); + }); + }); + + it('error handlers for validate (gh-4885)', function(done) { + var testSchema = new Schema({ title: { type: String, required: true } }); + + var called = 0; + testSchema.post('validate', function(error, doc, next) { + ++called; + next(error); + }); + + var Test = db.model('gh4885', testSchema); + + Test.create({}, function(error) { + assert.ok(error); + assert.equal(called, 1); + done(); + }); + }); + + it('error handlers with findOneAndUpdate and passRawResult (gh-4836)', function(done) { + var schema = new Schema({name: {type: String}}); + + var called = false; + var errorHandler = function(err, res, next) { + called = true; + next(); + }; + + schema.post('findOneAndUpdate', errorHandler); + + var Person = db.model('Person', schema); + + Person. + findOneAndUpdate({name: 'name'}, {}, {upsert: true, passRawResult: true}). + exec(function(error) { + assert.ifError(error); + assert.ok(!called); + done(); + }); + }); + + it('error handlers with findOneAndUpdate error and passRawResult (gh-4836)', function(done) { + var schema = new Schema({name: {type: String}}); + + var called = false; + var errorHandler = function(err, res, next) { + called = true; + next(); + }; + + schema.post('findOneAndUpdate', errorHandler); + + var Person = db.model('Person', schema); + + Person. + findOneAndUpdate({}, {_id: 'test'}, {upsert: true, passRawResult: true}). + exec(function(error) { + assert.ok(error); + assert.ok(called); done(); }); + }); + + it('error handlers with error from pre hook (gh-4927)', function(done) { + var schema = new Schema({}); + var called = false; + + schema.pre('find', function(next) { + next(new Error('test')); + }); + + schema.post('find', function(res, next) { + called = true; + next(); + }); + + schema.post('find', function(error, res, next) { + assert.equal(error.message, 'test'); + next(new Error('test2')); + }); + + var Test = db.model('gh4927', schema); + + Test.find().exec(function(error) { + assert.equal(error.message, 'test2'); + assert.ok(!called); + done(); + }); + }); + + it('with clone() (gh-5153)', function(done) { + var schema = new Schema({}); + var calledPre = 0; + var calledPost = 0; + + schema.pre('find', function(next) { + ++calledPre; + next(); + }); + + schema.post('find', function(res, next) { + ++calledPost; + next(); + }); + + var Test = db.model('gh5153', schema.clone()); + + Test.find().exec(function(error) { + assert.ifError(error); + assert.equal(calledPre, 1); + assert.equal(calledPost, 1); + done(); }); }); }); diff --git a/test/query.test.js b/test/query.test.js index b9a960ac259..7e273b24a15 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -1,42 +1,47 @@ - /** * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , DocumentObjectId = mongoose.Types.ObjectId - , Schema = mongoose.Schema - , assert = require('assert') - , random = require('../lib/utils').random - , Query = require('../lib/query'); - -var Comment = new Schema({ - text: String -}); - -var Product = new Schema({ - tags: {} // mixed - , array: Array - , ids: [Schema.ObjectId] - , strings: [String] - , numbers: [Number] - , comments: [Comment] -}); - -mongoose.model('Product', Product); -mongoose.model('Comment', Comment); -var p1; +var start = require('./common'); +var mongoose = start.mongoose; +var DocumentObjectId = mongoose.Types.ObjectId; +var Schema = mongoose.Schema; +var assert = require('power-assert'); +var random = require('../lib/utils').random; +var Query = require('../lib/query'); /** * Test. */ describe('Query', function() { + var Comment; + var Product; + var p1; + + before(function() { + Comment = new Schema({ + text: String + }); + + Product = new Schema({ + tags: {}, // mixed + array: Array, + ids: [Schema.ObjectId], + strings: [String], + numbers: [Number], + comments: [Comment] + }); + + mongoose.model('Product', Product); + mongoose.model('Comment', Comment); + }); + before(function() { var Prod = mongoose.model('Product'); p1 = new Prod(); }); + describe('constructor', function() { it('should not corrupt options', function(done) { var opts = {}; @@ -50,14 +55,14 @@ describe('Query', function() { it('(object)', function(done) { var query = new Query({}, {}, null, p1.collection); query.select({a: 1, b: 1, c: 0}); - assert.deepEqual(query._fields,{a: 1, b: 1, c: 0}); + assert.deepEqual(query._fields, {a: 1, b: 1, c: 0}); done(); }); it('(string)', function(done) { var query = new Query({}, {}, null, p1.collection); - query.select(" a b -c "); - assert.deepEqual(query._fields,{a: 1, b: 1, c: 0}); + query.select(' a b -c '); + assert.deepEqual(query._fields, {a: 1, b: 1, c: 0}); done(); }); @@ -69,24 +74,16 @@ describe('Query', function() { done(); }); - it('["a","b","c"]', function(done) { - assert.throws(function() { - var query = new Query({}, {}, null, p1.collection); - query.select(['a', 'b', 'c']); - }, /Invalid select/); - done(); - }); - it('should not overwrite fields set in prior calls', function(done) { var query = new Query({}, {}, null, p1.collection); query.select('a'); - assert.deepEqual(query._fields,{a: 1}); + assert.deepEqual(query._fields, {a: 1}); query.select('b'); - assert.deepEqual(query._fields,{a: 1, b: 1}); - query.select({ c: 0 }); - assert.deepEqual(query._fields,{a: 1, b: 1, c: 0}); + assert.deepEqual(query._fields, {a: 1, b: 1}); + query.select({c: 0}); + assert.deepEqual(query._fields, {a: 1, b: 1, c: 0}); query.select('-d'); - assert.deepEqual(query._fields,{a: 1, b: 1, c: 0, d: 0}); + assert.deepEqual(query._fields, {a: 1, b: 1, c: 0, d: 0}); done(); }); }); @@ -138,7 +135,7 @@ describe('Query', function() { }); it('with 1 arg', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").gte(18); + query.where('age').gte(18); assert.deepEqual(query._conditions, {age: {$gte: 18}}); done(); }); @@ -147,7 +144,7 @@ describe('Query', function() { describe('gt', function() { it('with 1 arg', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").gt(17); + query.where('age').gt(17); assert.deepEqual(query._conditions, {age: {$gt: 17}}); done(); }); @@ -162,7 +159,7 @@ describe('Query', function() { describe('lte', function() { it('with 1 arg', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").lte(65); + query.where('age').lte(65); assert.deepEqual(query._conditions, {age: {$lte: 65}}); done(); }); @@ -177,7 +174,7 @@ describe('Query', function() { describe('lt', function() { it('with 1 arg', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").lt(66); + query.where('age').lt(66); assert.deepEqual(query._conditions, {age: {$lt: 66}}); done(); }); @@ -193,7 +190,7 @@ describe('Query', function() { describe('lt and gt', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").lt(66).gt(17); + query.where('age').lt(66).gt(17); assert.deepEqual(query._conditions, {age: {$lt: 66, $gt: 17}}); done(); }); @@ -204,8 +201,8 @@ describe('Query', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); query - .where("age").lt(66) - .where("height").gt(5); + .where('age').lt(66) + .where('height').gt(5); assert.deepEqual(query._conditions, {age: {$lt: 66}, height: {$gt: 5}}); done(); }); @@ -214,7 +211,7 @@ describe('Query', function() { describe('ne', function() { it('with 1 arg', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").ne(21); + query.where('age').ne(21); assert.deepEqual(query._conditions, {age: {$ne: 21}}); done(); }); @@ -229,7 +226,7 @@ describe('Query', function() { describe('in', function() { it('with 1 arg', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").in([21, 25, 30]); + query.where('age').in([21, 25, 30]); assert.deepEqual(query._conditions, {age: {$in: [21, 25, 30]}}); done(); }); @@ -256,7 +253,7 @@ describe('Query', function() { describe('nin', function() { it('with 1 arg', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").nin([21, 25, 30]); + query.where('age').nin([21, 25, 30]); assert.deepEqual(query._conditions, {age: {$nin: [21, 25, 30]}}); done(); }); @@ -295,13 +292,13 @@ describe('Query', function() { }); it('via where, where [a, b] param', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").mod([5, 2]); + query.where('age').mod([5, 2]); assert.deepEqual(query._conditions, {age: {$mod: [5, 2]}}); done(); }); it('via where, where a and b params', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("age").mod(5, 2); + query.where('age').mod(5, 2); assert.deepEqual(query._conditions, {age: {$mod: [5, 2]}}); done(); }); @@ -310,7 +307,7 @@ describe('Query', function() { describe('near', function() { it('via where, where { center :[lat, long]} param', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where('checkin').near({ center : [40, -72]}); + query.where('checkin').near({center: [40, -72]}); assert.deepEqual(query._conditions, {checkin: {$near: [40, -72]}}); done(); }); @@ -340,8 +337,8 @@ describe('Query', function() { }); it('via where, where GeoJSON param', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where('numbers').near({ center : { type : 'Point', coordinates : [40, -72 ]}}); - assert.deepEqual(query._conditions, {numbers: {$near: { $geometry : { type : 'Point', coordinates : [40, -72] }}}}); + query.where('numbers').near({center: {type: 'Point', coordinates: [40, -72]}}); + assert.deepEqual(query._conditions, {numbers: {$near: {$geometry: {type: 'Point', coordinates: [40, -72]}}}}); assert.doesNotThrow(function() { query.cast(p1.constructor); }); @@ -349,8 +346,8 @@ describe('Query', function() { }); it('with path, where GeoJSON param', function(done) { var query = new Query({}, {}, null, p1.collection); - query.near('loc', { center : { type : 'Point', coordinates : [40, -72 ]}}); - assert.deepEqual(query._conditions, {loc: {$near: { $geometry : { type : 'Point', coordinates : [40, -72] }}}}); + query.near('loc', {center: {type: 'Point', coordinates: [40, -72]}}); + assert.deepEqual(query._conditions, {loc: {$near: {$geometry: {type: 'Point', coordinates: [40, -72]}}}}); done(); }); }); @@ -383,15 +380,15 @@ describe('Query', function() { it('via where, with object', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where('checkin').nearSphere({ center: [20,23], maxDistance: 2 }); - assert.deepEqual(query._conditions, {checkin: {$nearSphere: [20,23],$maxDistance:2}}); + query.where('checkin').nearSphere({center: [20, 23], maxDistance: 2}); + assert.deepEqual(query._conditions, {checkin: {$nearSphere: [20, 23], $maxDistance: 2}}); done(); }); it('via where, where GeoJSON param', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where('numbers').nearSphere({ center : { type : 'Point', coordinates : [40, -72 ]}}); - assert.deepEqual(query._conditions, {numbers: {$nearSphere: { $geometry : { type : 'Point', coordinates : [40, -72] }}}}); + query.where('numbers').nearSphere({center: {type: 'Point', coordinates: [40, -72]}}); + assert.deepEqual(query._conditions, {numbers: {$nearSphere: {$geometry: {type: 'Point', coordinates: [40, -72]}}}}); assert.doesNotThrow(function() { query.cast(p1.constructor); }); @@ -400,8 +397,8 @@ describe('Query', function() { it('with path, with GeoJSON', function(done) { var query = new Query({}, {}, null, p1.collection); - query.nearSphere('numbers', { center : { type : 'Point', coordinates : [40, -72 ]}}); - assert.deepEqual(query._conditions, {numbers: {$nearSphere: { $geometry : { type : 'Point', coordinates : [40, -72] }}}}); + query.nearSphere('numbers', {center: {type: 'Point', coordinates: [40, -72]}}); + assert.deepEqual(query._conditions, {numbers: {$nearSphere: {$geometry: {type: 'Point', coordinates: [40, -72]}}}}); assert.doesNotThrow(function() { query.cast(p1.constructor); }); @@ -426,7 +423,7 @@ describe('Query', function() { var match = {gps: {$within: {$box: [[5, 25], [10, 30]]}}}; if (Query.use$geoWithin) { match.gps.$geoWithin = match.gps.$within; - delete match.gps["$within"]; + delete match.gps.$within; } assert.deepEqual(query._conditions, match); done(); @@ -437,7 +434,7 @@ describe('Query', function() { var match = {gps: {$within: {$box: [[5, 25], [10, 30]]}}}; if (Query.use$geoWithin) { match.gps.$geoWithin = match.gps.$within; - delete match.gps["$within"]; + delete match.gps.$within; } assert.deepEqual(query._conditions, match); done(); @@ -451,7 +448,7 @@ describe('Query', function() { var match = {gps: {$within: {$center: [[5, 25], 5]}}}; if (Query.use$geoWithin) { match.gps.$geoWithin = match.gps.$within; - delete match.gps["$within"]; + delete match.gps.$within; } assert.deepEqual(query._conditions, match); done(); @@ -465,7 +462,7 @@ describe('Query', function() { var match = {gps: {$within: {$centerSphere: [[5, 25], 5]}}}; if (Query.use$geoWithin) { match.gps.$geoWithin = match.gps.$within; - delete match.gps["$within"]; + delete match.gps.$within; } assert.deepEqual(query._conditions, match); done(); @@ -475,11 +472,11 @@ describe('Query', function() { describe('polygon', function() { it('via where', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where('gps').within().polygon({ a: { x: 10, y: 20 }, b: { x: 15, y: 25 }, c: { x: 20, y: 20 }}); - var match = {gps: {$within: {$polygon: [{ a: { x: 10, y: 20 }, b: { x: 15, y: 25 }, c: { x: 20, y: 20 }}] }}}; + query.where('gps').within().polygon({a: {x: 10, y: 20}, b: {x: 15, y: 25}, c: {x: 20, y: 20}}); + var match = {gps: {$within: {$polygon: [{a: {x: 10, y: 20}, b: {x: 15, y: 25}, c: {x: 20, y: 20}}]}}}; if (Query.use$geoWithin) { match.gps.$geoWithin = match.gps.$within; - delete match.gps["$within"]; + delete match.gps.$within; } assert.deepEqual(query._conditions, match); done(); @@ -490,13 +487,13 @@ describe('Query', function() { describe('exists', function() { it('0 args via where', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("username").exists(); + query.where('username').exists(); assert.deepEqual(query._conditions, {username: {$exists: true}}); done(); }); it('1 arg via where', function(done) { var query = new Query({}, {}, null, p1.collection); - query.where("username").exists(false); + query.where('username').exists(false); assert.deepEqual(query._conditions, {username: {$exists: false}}); done(); }); @@ -509,7 +506,7 @@ describe('Query', function() { it('where 2 args not via where', function(done) { var query = new Query({}, {}, null, p1.collection); - query.exists("username", false); + query.exists('username', false); assert.deepEqual(query._conditions, {username: {$exists: false}}); done(); }); @@ -533,7 +530,7 @@ describe('Query', function() { describe('find', function() { it('strict array equivalence condition v', function(done) { var query = new Query({}, {}, null, p1.collection); - query.find({'pets': ['dog', 'cat', 'ferret']}); + query.find({pets: ['dog', 'cat', 'ferret']}); assert.deepEqual(query._conditions, {pets: ['dog', 'cat', 'ferret']}); done(); }); @@ -550,13 +547,14 @@ describe('Query', function() { assert.ok(!threw); done(); }); + it('works with overwriting previous object args (1176)', function(done) { var q = new Query({}, {}, null, p1.collection); assert.doesNotThrow(function() { - q.find({ age: { $lt: 30 }}); - q.find({ age: 20 }); // overwrite + q.find({age: {$lt: 30}}); + q.find({age: 20}); // overwrite }); - assert.deepEqual({ age: 20 }, q._conditions); + assert.deepEqual({age: 20}, q._conditions); done(); }); }); @@ -691,9 +689,11 @@ describe('Query', function() { describe('$where', function() { it('function arg', function(done) { var query = new Query({}, {}, null, p1.collection); + function filter() { return this.lastName === this.firstName; } + query.$where(filter); assert.deepEqual(query._conditions, {$where: filter}); done(); @@ -710,7 +710,7 @@ describe('Query', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); query.limit(5); - assert.equal(query.options.limit,5); + assert.equal(query.options.limit, 5); done(); }); }); @@ -719,7 +719,7 @@ describe('Query', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); query.skip(9); - assert.equal(query.options.skip,9); + assert.equal(query.options.skip, 9); done(); }); }); @@ -728,21 +728,29 @@ describe('Query', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); query.sort('a -c b'); - assert.deepEqual(query.options.sort, {'a': 1 , 'c': -1,'b': 1}); + assert.deepEqual(query.options.sort, {a: 1, c: -1, b: 1}); query = new Query({}, {}, null, p1.collection); - query.sort({'a': 1, 'c': -1, 'b': 'asc', e: 'descending', f: 'ascending'}); - assert.deepEqual(query.options.sort, {'a': 1, 'c': -1, 'b': 1, 'e': -1, 'f': 1}); + query.sort({a: 1, c: -1, b: 'asc', e: 'descending', f: 'ascending'}); + assert.deepEqual(query.options.sort, {a: 1, c: -1, b: 1, e: -1, f: 1}); + + if (typeof global.Map !== 'undefined') { + query = new Query({}, {}, null, p1.collection); + query.sort(new global.Map().set('a', 1).set('b', 2)); + assert.equal(query.options.sort.get('a'), 1); + assert.equal(query.options.sort.get('b'), 2); + } + query = new Query({}, {}, null, p1.collection); var e; try { - query.sort(['a',1]); + query.sort(['a', 1]); } catch (err) { e = err; } assert.ok(e, 'uh oh. no error was thrown'); - assert.equal(e.message, 'Invalid sort() argument.'); + assert.equal(e.message, 'Invalid sort() argument, must be array of arrays'); e = undefined; try { @@ -759,15 +767,15 @@ describe('Query', function() { describe('or', function() { it('works', function(done) { var query = new Query; - query.find({ $or: [{x:1},{x:2}] }); + query.find({$or: [{x: 1}, {x: 2}]}); assert.equal(query._conditions.$or.length, 2); - query.or([{y:"We're under attack"}, {z:47}]); + query.or([{y: 'We\'re under attack'}, {z: 47}]); assert.equal(query._conditions.$or.length, 4); assert.equal(query._conditions.$or[3].z, 47); - query.or({z:"phew"}); + query.or({z: 'phew'}); assert.equal(query._conditions.$or.length, 5); assert.equal(query._conditions.$or[3].z, 47); - assert.equal(query._conditions.$or[4].z, "phew"); + assert.equal(query._conditions.$or[4].z, 'phew'); done(); }); }); @@ -775,18 +783,18 @@ describe('Query', function() { describe('and', function() { it('works', function(done) { var query = new Query; - query.find({ $and: [{x:1},{y:2}] }); + query.find({$and: [{x: 1}, {y: 2}]}); assert.equal(query._conditions.$and.length, 2); - query.and([{z:"We're under attack"}, {w:47}]); + query.and([{z: 'We\'re under attack'}, {w: 47}]); assert.equal(query._conditions.$and.length, 4); assert.equal(query._conditions.$and[3].w, 47); - query.and({a:"phew"}); + query.and({a: 'phew'}); assert.equal(query._conditions.$and.length, 5); assert.equal(query._conditions.$and[0].x, 1); assert.equal(query._conditions.$and[1].y, 2); - assert.equal(query._conditions.$and[2].z, "We're under attack"); + assert.equal(query._conditions.$and[2].z, 'We\'re under attack'); assert.equal(query._conditions.$and[3].w, 47); - assert.equal(query._conditions.$and[4].a, "phew"); + assert.equal(query._conditions.$and[4].a, 'phew'); done(); }); }); @@ -795,12 +803,12 @@ describe('Query', function() { it('converts to PopulateOptions objects', function(done) { var q = new Query({}, {}, null, p1.collection); var o = { - path: 'yellow.brick' - , match: { bricks: { $lt: 1000 }} - , select: undefined - , model: undefined - , options: undefined - , _docs: {} + path: 'yellow.brick', + match: {bricks: {$lt: 1000}}, + select: undefined, + model: undefined, + options: undefined, + _docs: {} }; q.populate(o); assert.deepEqual(o, q._mongooseOptions.populate['yellow.brick']); @@ -810,18 +818,18 @@ describe('Query', function() { it('overwrites duplicate paths', function(done) { var q = new Query({}, {}, null, p1.collection); var o = { - path: 'yellow.brick' - , match: { bricks: { $lt: 1000 }} - , select: undefined - , model: undefined - , options: undefined - , _docs: {} + path: 'yellow.brick', + match: {bricks: {$lt: 1000}}, + select: undefined, + model: undefined, + options: undefined, + _docs: {} }; q.populate(o); - assert.equal(1, Object.keys(q._mongooseOptions.populate).length); + assert.equal(Object.keys(q._mongooseOptions.populate).length, 1); assert.deepEqual(o, q._mongooseOptions.populate['yellow.brick']); q.populate('yellow.brick'); - assert.equal(1, Object.keys(q._mongooseOptions.populate).length); + assert.equal(Object.keys(q._mongooseOptions.populate).length, 1); o.match = undefined; assert.deepEqual(o, q._mongooseOptions.populate['yellow.brick']); done(); @@ -831,57 +839,63 @@ describe('Query', function() { var q = new Query({}, {}, null, p1.collection); q.populate('yellow.brick dirt'); var o = { - path: 'yellow.brick' - , match: undefined - , select: undefined - , model: undefined - , options: undefined - , _docs: {} + path: 'yellow.brick', + match: undefined, + select: undefined, + model: undefined, + options: undefined, + _docs: {} }; - assert.equal(2, Object.keys(q._mongooseOptions.populate).length); + assert.equal(Object.keys(q._mongooseOptions.populate).length, 2); assert.deepEqual(o, q._mongooseOptions.populate['yellow.brick']); o.path = 'dirt'; - assert.deepEqual(o, q._mongooseOptions.populate['dirt']); + assert.deepEqual(o, q._mongooseOptions.populate.dirt); done(); }); }); describe('casting', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + it('to an array of mixed', function(done) { var query = new Query({}, {}, null, p1.collection); - var db = start(); var Product = db.model('Product'); - db.close(); - var params = { _id: new DocumentObjectId, tags: { $in: [ 4, 8, 15, 16 ] }}; + var params = {_id: new DocumentObjectId, tags: {$in: [4, 8, 15, 16]}}; query.cast(Product, params); - assert.deepEqual(params.tags.$in, [4,8,15,16]); + assert.deepEqual(params.tags.$in, [4, 8, 15, 16]); done(); }); it('find $ne should not cast single value to array for schematype of Array', function(done) { var query = new Query({}, {}, null, p1.collection); - var db = start(); var Product = db.model('Product'); var Comment = db.model('Comment'); - db.close(); var id = new DocumentObjectId; - var castedComment = { _id: id, text: 'hello there' }; + var castedComment = {_id: id, text: 'hello there'}; var comment = new Comment(castedComment); var params = { - array: { $ne: 5 } - , ids: { $ne: id } - , comments: { $ne: comment } - , strings: { $ne: 'Hi there' } - , numbers: { $ne: 10000 } + array: {$ne: 5}, + ids: {$ne: id}, + comments: {$ne: comment}, + strings: {$ne: 'Hi there'}, + numbers: {$ne: 10000} }; query.cast(Product, params); - assert.equal(params.array.$ne,5); + assert.equal(params.array.$ne, 5); assert.equal(params.ids.$ne, id); params.comments.$ne._id.toHexString(); - assert.deepEqual(params.comments.$ne, castedComment); + assert.deepEqual(params.comments.$ne.toObject(), castedComment); assert.equal(params.strings.$ne, 'Hi there'); assert.equal(params.numbers.$ne, 10000); @@ -896,7 +910,7 @@ describe('Query', function() { assert.ok(params.ids.$ne instanceof Array); assert.equal(params.ids.$ne[0].toString(), id.toString()); assert.ok(params.comments.$ne instanceof Array); - assert.deepEqual(params.comments.$ne[0], castedComment); + assert.deepEqual(params.comments.$ne[0].toObject(), castedComment); assert.ok(params.strings.$ne instanceof Array); assert.equal(params.strings.$ne[0], 'Hi there'); assert.ok(params.numbers.$ne instanceof Array); @@ -904,14 +918,56 @@ describe('Query', function() { done(); }); + it('doesn\'t wipe out $in (gh-6439)', function(done) { + var embeddedSchema = new Schema({ + name: String + }, { _id: false }); + + var catSchema = new Schema({ + name: String, + props: [embeddedSchema] + }); + + var Cat = db.model('gh6439', catSchema); + var kitty = new Cat({ + name: 'Zildjian', + props: [ + { name: 'invalid' }, + { name: 'abc' }, + { name: 'def' } + ] + }); + + kitty.save(function(err) { + assert.ifError(err); + var cond = { _id: kitty._id }; + var update = { + $pull: { + props: { + $in: [ + { name: 'invalid' }, + { name: 'def' } + ] + } + } + }; + Cat.update(cond, update, function(err) { + assert.ifError(err); + Cat.findOne(cond, function(err, found) { + assert.ifError(err); + assert.strictEqual(found.props[0].name, 'abc'); + done(); + }); + }); + }); + }); + it('subdocument array with $ne: null should not throw', function(done) { var query = new Query({}, {}, null, p1.collection); - var db = start(); var Product = db.model('Product'); - db.close(); var params = { - comments: { $ne: null } + comments: {$ne: null} }; query.cast(Product, params); @@ -921,28 +977,26 @@ describe('Query', function() { it('find should not cast single value to array for schematype of Array', function(done) { var query = new Query({}, {}, null, p1.collection); - var db = start(); var Product = db.model('Product'); var Comment = db.model('Comment'); - db.close(); var id = new DocumentObjectId; - var castedComment = { _id: id, text: 'hello there' }; + var castedComment = {_id: id, text: 'hello there'}; var comment = new Comment(castedComment); var params = { - array: 5 - , ids: id - , comments: comment - , strings: 'Hi there' - , numbers: 10000 + array: 5, + ids: id, + comments: comment, + strings: 'Hi there', + numbers: 10000 }; query.cast(Product, params); - assert.equal(params.array,5); + assert.equal(params.array, 5); assert.equal(params.ids, id); params.comments._id.toHexString(); - assert.deepEqual(params.comments, castedComment); + assert.deepEqual(params.comments.toObject(), castedComment); assert.equal(params.strings, 'Hi there'); assert.equal(params.numbers, 10000); @@ -957,7 +1011,7 @@ describe('Query', function() { assert.ok(params.ids instanceof Array); assert.equal(params.ids[0].toString(), id.toString()); assert.ok(params.comments instanceof Array); - assert.deepEqual(params.comments[0], castedComment); + assert.deepEqual(params.comments[0].toObject(), castedComment); assert.ok(params.strings instanceof Array); assert.equal(params.strings[0], 'Hi there'); assert.ok(params.numbers instanceof Array); @@ -967,11 +1021,9 @@ describe('Query', function() { it('an $elemMatch with $in works (gh-1100)', function(done) { var query = new Query({}, {}, null, p1.collection); - var db = start(); var Product = db.model('Product'); - db.close(); var ids = [String(new DocumentObjectId), String(new DocumentObjectId)]; - var params = { ids: { $elemMatch: { $in: ids }}}; + var params = {ids: {$elemMatch: {$in: ids}}}; query.cast(Product, params); assert.ok(params.ids.$elemMatch.$in[0] instanceof DocumentObjectId); assert.ok(params.ids.$elemMatch.$in[1] instanceof DocumentObjectId); @@ -982,25 +1034,23 @@ describe('Query', function() { it('inequality operators for an array', function(done) { var query = new Query({}, {}, null, p1.collection); - var db = start(); var Product = db.model('Product'); var Comment = db.model('Comment'); - db.close(); var id = new DocumentObjectId; - var castedComment = { _id: id, text: 'hello there' }; + var castedComment = {_id: id, text: 'hello there'}; var comment = new Comment(castedComment); var params = { - ids: { $gt: id } - , comments: { $gt: comment } - , strings: { $gt: 'Hi there' } - , numbers: { $gt: 10000 } + ids: {$gt: id}, + comments: {$gt: comment}, + strings: {$gt: 'Hi there'}, + numbers: {$gt: 10000} }; query.cast(Product, params); assert.equal(params.ids.$gt, id); - assert.deepEqual(params.comments.$gt, castedComment); + assert.deepEqual(params.comments.$gt.toObject(), castedComment); assert.equal(params.strings.$gt, 'Hi there'); assert.equal(params.numbers.$gt, 10000); done(); @@ -1013,10 +1063,9 @@ describe('Query', function() { var Product = db.model('Product'); var prod = new Product({}); var q = new Query({}, {}, Product, prod.collection).distinct('blah', function() { - assert.equal(q.op,'distinct'); - db.close(); + assert.equal(q.op, 'distinct'); + db.close(done); }); - done(); }); }); @@ -1025,21 +1074,21 @@ describe('Query', function() { var db = start(); var Product = db.model('Product', 'update_products_' + random()); new Query(p1.collection, {}, Product).count(); - Product.create({ tags: 12345 }, function(err) { + Product.create({tags: 12345}, function(err) { assert.ifError(err); var time = 20; - Product.find({ tags: 12345 }).update({ $set: { tags: 123456 }}); + Product.find({tags: 12345}).update({$set: {tags: 123456}}); setTimeout(function() { - Product.find({ tags: 12345 }, function(err, p) { + Product.find({tags: 12345}, function(err, p) { assert.ifError(err); - assert.equal(1, p.length); + assert.equal(p.length, 1); - Product.find({ tags: 123456 }).remove(); + Product.find({tags: 123456}).remove(); setTimeout(function() { - Product.find({ tags: 123456 }, function(err, p) { + Product.find({tags: 123456}, function(err, p) { assert.ifError(err); - assert.equal(0, p.length); + assert.equal(p.length, 0); db.close(); done(); }); @@ -1059,9 +1108,9 @@ describe('Query', function() { // use a timeout here because we have to wait for the connection to start // before any ops will get set setTimeout(function() { - assert.equal(q.op,'distinct'); + assert.equal(q.op, 'distinct'); q.findOne(); - assert.equal(q.op,'findOne'); + assert.equal(q.op, 'findOne'); db.close(); done(); }, 50); @@ -1073,20 +1122,62 @@ describe('Query', function() { var promise = Product.findOne(); promise.then(function() { - done(); + db.close(done); }, function(err) { assert.ifError(err); }); }); }); + describe('deleteOne/deleteMany', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + it('handles deleteOne', function(done) { + var M = db.model('deleteOne', new Schema({ name: 'String' })); + M.create([{ name: 'Eddard Stark' }, { name: 'Robb Stark' }], function(error) { + assert.ifError(error); + M.deleteOne({ name: /Stark/ }, function(error) { + assert.ifError(error); + M.count({}, function(error, count) { + assert.ifError(error); + assert.equal(count, 1); + done(); + }); + }); + }); + }); + + it('handles deleteMany', function(done) { + var M = db.model('deleteMany', new Schema({ name: 'String' })); + M.create([{ name: 'Eddard Stark' }, { name: 'Robb Stark' }], function(error) { + assert.ifError(error); + M.deleteMany({ name: /Stark/ }, function(error) { + assert.ifError(error); + M.count({}, function(error, count) { + assert.ifError(error); + assert.equal(count, 0); + done(); + }); + }); + }); + }); + }); + describe('remove', function() { it('handles cast errors async', function(done) { var db = start(); var Product = db.model('Product'); assert.doesNotThrow(function() { - Product.where({ numbers: [[[]]] }).remove(function(err) { + Product.where({numbers: [[[]]]}).remove(function(err) { db.close(); assert.ok(err); done(); @@ -1098,9 +1189,9 @@ describe('Query', function() { var db = start(); var Product = db.model('Product'); - Product.create({ strings: ['remove-single-condition'] }).then(function() { + Product.create({strings: ['remove-single-condition']}).then(function() { db.close(); - var q = Product.where().remove({ strings: 'remove-single-condition' }); + var q = Product.where().remove({strings: 'remove-single-condition'}); assert.ok(q instanceof mongoose.Query); done(); }, done).end(); @@ -1111,10 +1202,10 @@ describe('Query', function() { var Product = db.model('Product'); var val = 'remove-single-callback'; - Product.create({ strings: [val] }).then(function() { - Product.where({ strings: val }).remove(function(err) { + Product.create({strings: [val]}).then(function() { + Product.where({strings: val}).remove(function(err) { assert.ifError(err); - Product.findOne({ strings: val }, function(err, doc) { + Product.findOne({strings: val}, function(err, doc) { db.close(); assert.ifError(err); assert.ok(!doc); @@ -1129,10 +1220,10 @@ describe('Query', function() { var Product = db.model('Product'); var val = 'remove-cond-and-callback'; - Product.create({ strings: [val] }).then(function() { - Product.where().remove({ strings: val }, function(err) { + Product.create({strings: [val]}).then(function() { + Product.where().remove({strings: val}, function(err) { assert.ifError(err); - Product.findOne({ strings: val }, function(err, doc) { + Product.findOne({strings: val}, function(err, doc) { db.close(); assert.ifError(err); assert.ok(!doc); @@ -1141,6 +1232,60 @@ describe('Query', function() { }); }, done).end(); }); + + it('single option, default', function(done) { + var db = start(); + var Test = db.model('Test_single', new Schema({ name: String })); + + Test.create([{ name: 'Eddard Stark' }, { name: 'Robb Stark' }], function(error) { + assert.ifError(error); + Test.remove({ name: /Stark/ }).exec(function(error, res) { + assert.ifError(error); + assert.equal(res.result.n, 2); + Test.count({}, function(error, count) { + assert.ifError(error); + assert.equal(count, 0); + done(); + }); + }); + }); + }); + + it('single option, false', function(done) { + var db = start(); + var Test = db.model('Test_single', new Schema({ name: String })); + + Test.create([{ name: 'Eddard Stark' }, { name: 'Robb Stark' }], function(error) { + assert.ifError(error); + Test.remove({ name: /Stark/ }).setOptions({ single: false }).exec(function(error, res) { + assert.ifError(error); + assert.equal(res.result.n, 2); + Test.count({}, function(error, count) { + assert.ifError(error); + assert.equal(count, 0); + done(); + }); + }); + }); + }); + + it('single option, true', function(done) { + var db = start(); + var Test = db.model('Test_single', new Schema({ name: String })); + + Test.create([{ name: 'Eddard Stark' }, { name: 'Robb Stark' }], function(error) { + assert.ifError(error); + Test.remove({ name: /Stark/ }).setOptions({ single: true }).exec(function(error, res) { + assert.ifError(error); + assert.equal(res.result.n, 1); + Test.count({}, function(error, count) { + assert.ifError(error); + assert.equal(count, 1); + done(); + }); + }); + }); + }); }); describe('querying/updating with model instance containing embedded docs should work (#454)', function() { @@ -1148,27 +1293,27 @@ describe('Query', function() { var db = start(); var Product = db.model('Product'); - var proddoc = { comments: [{ text: 'hello' }] }; - var prod2doc = { comments: [{ text: 'goodbye' }] }; + var proddoc = {comments: [{text: 'hello'}]}; + var prod2doc = {comments: [{text: 'goodbye'}]}; var prod = new Product(proddoc); prod.save(function(err) { assert.ifError(err); - Product.findOne(prod, function(err, product) { + Product.findOne({ _id: prod._id }, function(err, product) { assert.ifError(err); assert.equal(product.comments.length, 1); assert.equal(product.comments[0].text, 'hello'); - Product.update(product, prod2doc, function(err) { + Product.update({ _id: prod._id }, prod2doc, function(err) { assert.ifError(err); - Product.collection.findOne({ _id: product._id }, function(err, doc) { + Product.collection.findOne({_id: product._id}, function(err, doc) { assert.ifError(err); assert.equal(doc.comments.length, 1); // ensure hidden private props were not saved to db - assert.ok(!doc.comments[0].hasOwnProperty('parentArry') ); - assert.equal(doc.comments[0].text,'goodbye'); + assert.ok(!doc.comments[0].hasOwnProperty('parentArry')); + assert.equal(doc.comments[0].text, 'goodbye'); db.close(done); }); }); @@ -1180,14 +1325,14 @@ describe('Query', function() { describe('optionsForExecute', function() { it('should retain key order', function(done) { // this is important for query hints - var hint = { x: 1, y: 1, z: 1 }; - var a = JSON.stringify({ hint: hint, safe: true}); + var hint = {x: 1, y: 1, z: 1}; + var a = JSON.stringify({hint: hint, safe: true}); var q = new Query; q.hint(hint); - var options = q._optionsForExec({ schema: { options: { safe: true } }}); - assert.equal(a,JSON.stringify(options)); + var options = q._optionsForExec({schema: {options: {safe: true}}}); + assert.equal(JSON.stringify(options), a); done(); }); }); @@ -1199,7 +1344,7 @@ describe('Query', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); query.maxscan(100); - assert.equal(query.options.maxScan,100); + assert.equal(query.options.maxScan, 100); done(); }); }); @@ -1208,15 +1353,15 @@ describe('Query', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); query.slaveOk(); - assert.equal(true, query.options.slaveOk); + assert.equal(query.options.slaveOk, true); query = new Query({}, {}, null, p1.collection); query.slaveOk(true); - assert.equal(true, query.options.slaveOk); + assert.equal(query.options.slaveOk, true); query = new Query({}, {}, null, p1.collection); query.slaveOk(false); - assert.equal(false, query.options.slaveOk); + assert.equal(query.options.slaveOk, false); done(); }); }); @@ -1225,22 +1370,22 @@ describe('Query', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); query.tailable(); - assert.equal(true, query.options.tailable); + assert.equal(query.options.tailable, true); query = new Query({}, {}, null, p1.collection); query.tailable(true); - assert.equal(true, query.options.tailable); + assert.equal(query.options.tailable, true); query = new Query({}, {}, null, p1.collection); query.tailable(false); - assert.equal(false, query.options.tailable); + assert.equal(query.options.tailable, false); done(); }); it('supports passing the `await` option', function(done) { var query = new Query({}, {}, null, p1.collection); - query.tailable({ awaitdata: true }); - assert.equal(true, query.options.tailable); - assert.equal(true, query.options.awaitdata); + query.tailable({awaitdata: true}); + assert.equal(query.options.tailable, true); + assert.equal(query.options.awaitdata, true); done(); }); }); @@ -1248,9 +1393,9 @@ describe('Query', function() { describe('comment', function() { it('works', function(done) { var query = new Query; - assert.equal('function',typeof query.comment); - assert.equal(query.comment('Lowpass is more fun'),query); - assert.equal(query.options.comment,'Lowpass is more fun'); + assert.equal(typeof query.comment, 'function'); + assert.equal(query.comment('Lowpass is more fun'), query); + assert.equal(query.options.comment, 'Lowpass is more fun'); done(); }); }); @@ -1258,13 +1403,12 @@ describe('Query', function() { describe('hint', function() { it('works', function(done) { var query2 = new Query({}, {}, null, p1.collection); - query2.hint({'indexAttributeA': 1, 'indexAttributeB': -1}); - assert.deepEqual(query2.options.hint, {'indexAttributeA': 1, 'indexAttributeB': -1}); + query2.hint({indexAttributeA: 1, indexAttributeB: -1}); + assert.deepEqual(query2.options.hint, {indexAttributeA: 1, indexAttributeB: -1}); - assert.throws(function() { - var query3 = new Query({}, {}, null, p1.collection); - query3.hint('indexAttributeA'); - }, /Invalid hint./); + var query3 = new Query({}, {}, null, p1.collection); + query3.hint('indexAttributeA_1'); + assert.deepEqual(query3.options.hint, 'indexAttributeA_1'); done(); }); @@ -1274,7 +1418,7 @@ describe('Query', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); query.snapshot(true); - assert.equal(true, query.options.snapshot); + assert.equal(query.options.snapshot, true); done(); }); }); @@ -1283,7 +1427,7 @@ describe('Query', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); query.batchSize(10); - assert.equal(query.options.batchSize,10); + assert.equal(query.options.batchSize, 10); done(); }); }); @@ -1351,7 +1495,7 @@ describe('Query', function() { describe('with tags', function() { it('works', function(done) { var query = new Query({}, {}, null, p1.collection); - var tags = [{ dc: 'sf', s: 1}, { dc: 'jp', s: 2 }]; + var tags = [{dc: 'sf', s: 1}, {dc: 'jp', s: 2}]; query.read('pp', tags); assert.ok(query.options.readPreference instanceof P); @@ -1369,7 +1513,7 @@ describe('Query', function() { describe('inherits its models schema read option', function() { var schema, M, called; before(function() { - schema = new Schema({}, { read: 'p' }); + schema = new Schema({}, {read: 'p'}); M = mongoose.model('schemaOptionReadPrefWithQuery', schema); }); @@ -1389,8 +1533,8 @@ describe('Query', function() { it('and sends it though the driver', function(done) { var db = start(); - var options = { read: 'secondary', safe: { w: 'majority' }}; - var schema = Schema({ name: String }, options); + var options = {read: 'secondary', safe: {w: 'majority'}}; + var schema = new Schema({name: String}, options); var M = db.model(random(), schema); var q = M.find(); @@ -1402,20 +1546,21 @@ describe('Query', function() { var ret = getopts.call(this, model); assert.ok(ret.readPreference); - assert.equal('secondary', ret.readPreference.mode); - assert.deepEqual({ w: 'majority' }, ret.safe); + assert.equal(ret.readPreference.mode, 'secondary'); + assert.deepEqual({w: 'majority'}, ret.safe); called = true; return ret; }; q.exec(function(err) { - if (err) return done(err); + if (err) { + return done(err); + } assert.ok(called); db.close(done); }); }); - }); }); }); @@ -1423,18 +1568,18 @@ describe('Query', function() { describe('setOptions', function() { it('works', function(done) { var q = new Query; - q.setOptions({ thing: "cat" }); - q.setOptions({ populate: ['fans'] }); - q.setOptions({ batchSize: 10 }); - q.setOptions({ limit: 4 }); - q.setOptions({ skip: 3 }); - q.setOptions({ sort: '-blah' }); - q.setOptions({ sort: {'woot': -1} }); - q.setOptions({ hint: { index1: 1, index2: -1 }}); - q.setOptions({ read: ['s', [{dc:'eu'}]]}); + q.setOptions({thing: 'cat'}); + q.setOptions({populate: ['fans']}); + q.setOptions({batchSize: 10}); + q.setOptions({limit: 4}); + q.setOptions({skip: 3}); + q.setOptions({sort: '-blah'}); + q.setOptions({sort: {woot: -1}}); + q.setOptions({hint: {index1: 1, index2: -1}}); + q.setOptions({read: ['s', [{dc: 'eu'}]]}); assert.equal(q.options.thing, 'cat'); - assert.deepEqual(q._mongooseOptions.populate.fans, { path: 'fans', select: undefined, match: undefined, options: undefined, model: undefined, _docs: {} }); + assert.deepEqual(q._mongooseOptions.populate.fans, {path: 'fans', select: undefined, match: undefined, options: undefined, model: undefined, _docs: {}}); assert.equal(q.options.batchSize, 10); assert.equal(q.options.limit, 4); assert.equal(q.options.skip, 3); @@ -1449,19 +1594,25 @@ describe('Query', function() { var db = start(); var Product = db.model('Product', 'Product_setOptions_test'); Product.create( - { numbers: [3,4,5] } - , { strings: 'hi there'.split(' ') } , function(err, doc1, doc2) { - - assert.ifError(err); - - Product.find().setOptions({ limit: 1, sort: {_id: -1}, read: 'n' }).exec(function(err, docs) { - db.close(); + {numbers: [3, 4, 5]}, + {strings: 'hi there'.split(' ')}, function(err, doc1, doc2) { assert.ifError(err); - assert.equal(docs.length, 1); - assert.equal(docs[0].id, doc2.id); - done(); + Product.find().setOptions({limit: 1, sort: {_id: -1}, read: 'n'}).exec(function(err, docs) { + db.close(); + assert.ifError(err); + assert.equal(docs.length, 1); + assert.equal(docs[0].id, doc2.id); + done(); + }); }); - }); + }); + + it('populate as array in options (gh-4446)', function(done) { + var q = new Query; + q.setOptions({ populate: [{ path: 'path1' }, { path: 'path2' }] }); + assert.deepEqual(Object.keys(q._mongooseOptions.populate), + ['path1', 'path2']); + done(); }); }); @@ -1473,94 +1624,790 @@ describe('Query', function() { }); }); - describe('gh-1950', function() { - it('ignores sort when passed to count', function(done) { - var db = start(); - var Product = db.model('Product', 'Product_setOptions_test'); - Product.find().sort({ _id: 1 }).count({}).exec(function(error) { + describe('bug fixes', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + describe('collations', function() { + before(function(done) { + var _this = this; + start.mongodVersion(function(err, version) { + if (err) { + return done(err); + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + return _this.skip(); + } + + done(); + }); + }); + + it('collation support (gh-4839)', function(done) { + var schema = new Schema({ + name: String + }); + + var MyModel = db.model('gh4839', schema); + var collation = { locale: 'en_US', strength: 1 }; + + MyModel.create([{ name: 'a' }, { name: 'A' }]). + then(function() { + return MyModel.find({ name: 'a' }).collation(collation); + }). + then(function(docs) { + assert.equal(docs.length, 2); + return MyModel.find({ name: 'a' }, null, { collation: collation }); + }). + then(function(docs) { + assert.equal(docs.length, 2); + return MyModel.find({ name: 'a' }, null, { collation: collation }). + sort({ _id: -1 }). + cursor(). + next(); + }). + then(function(doc) { + assert.equal(doc.name, 'A'); + return MyModel.find({ name: 'a' }); + }). + then(function(docs) { + assert.equal(docs.length, 1); + done(); + }). + catch(done); + }); + + it('set on schema (gh-5295)', function(done) { + var schema = new Schema({ + name: String + }, { collation: { locale: 'en_US', strength: 1 } }); + + var MyModel = db.model('gh5295', schema); + + MyModel.create([{ name: 'a' }, { name: 'A' }]). + then(function() { + return MyModel.find({ name: 'a' }); + }). + then(function(docs) { + assert.equal(docs.length, 2); + done(); + }). + catch(done); + }); + }); + + describe('gh-1950', function() { + it('ignores sort when passed to count', function(done) { + var Product = db.model('Product', 'Product_setOptions_test'); + Product.find().sort({_id: 1}).count({}).exec(function(error) { + assert.ifError(error); + done(); + }); + }); + + it('ignores count when passed to sort', function(done) { + var Product = db.model('Product', 'Product_setOptions_test'); + Product.find().count({}).sort({_id: 1}).exec(function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('excludes _id when select false and inclusive mode (gh-3010)', function(done) { + var User = db.model('gh3010', { + _id: { + select: false, + type: Schema.Types.ObjectId, + default: mongoose.Types.ObjectId + }, + username: String + }); + + User.create({username: 'Val'}, function(error, user) { assert.ifError(error); - db.close(done); + User.find({_id: user._id}).select('username').exec(function(error, users) { + assert.ifError(error); + assert.equal(users.length, 1); + assert.ok(!users[0]._id); + assert.equal(users[0].username, 'Val'); + done(); + }); }); }); - it('ignores count when passed to sort', function(done) { - var db = start(); - var Product = db.model('Product', 'Product_setOptions_test'); - Product.find().count({}).sort({ _id: 1 }).exec(function(error) { + it('doesnt reverse key order for update docs (gh-3215)', function(done) { + var Test = db.model('gh3215', { + arr: [{date: Date, value: Number}] + }); + + var q = Test.update({}, { + $push: { + arr: { + $each: [{date: new Date(), value: 1}], + $sort: {value: -1, date: -1} + } + } + }); + + assert.deepEqual(Object.keys(q.getUpdate().$push.arr.$sort), + ['value', 'date']); + done(); + }); + + it('timestamps with $each (gh-4805)', function(done) { + var nestedSchema = new Schema({ value: Number }, { timestamps: true }); + var Test = db.model('gh4805', new Schema({ + arr: [nestedSchema] + }, { timestamps: true })); + + Test.update({}, { + $push: { + arr: { + $each: [{ value: 1 }] + } + } + }).exec(function(error) { assert.ifError(error); - db.close(done); + done(); }); }); - }); - it('excludes _id when select false and inclusive mode (gh-3010)', function(done) { - var db = start(); - var User = db.model('gh3010', { - _id: { - select: false, - type: Schema.Types.ObjectId, - default: mongoose.Types.ObjectId - }, - username: String + it('allows sort with count (gh-3914)', function(done) { + var Post = db.model('gh3914_0', { + title: String + }); + + Post.count({}).sort({ title: 1 }).exec(function(error, count) { + assert.ifError(error); + assert.strictEqual(count, 0); + done(); + }); }); - User.create({ username: 'Val' }, function(error, user) { - assert.ifError(error); - User.find({ _id: user._id }).select('username').exec(function(error, users) { + it('allows sort with select (gh-3914)', function(done) { + var Post = db.model('gh3914_1', { + title: String + }); + + Post.count({}).select({ _id: 0 }).exec(function(error, count) { assert.ifError(error); - assert.equal(users.length, 1); - assert.ok(!users[0]._id); - assert.equal(users[0].username, 'Val'); - db.close(done); + assert.strictEqual(count, 0); + done(); }); }); - }); - it('doesnt reverse key order for update docs (gh-3215)', function(done) { - var db = start(); - var Test = db.model('gh3215', { - arr: [{ date: Date, value: Number }] + it('handles nested $ (gh-3265)', function(done) { + var Post = db.model('gh3265', { + title: String, + answers: [{ + details: String, + stats: { + votes: Number, + count: Number + } + }] + }); + + var answersUpdate = {details: 'blah', stats: {votes: 1, count: '3'}}; + var q = Post.update( + {'answers._id': '507f1f77bcf86cd799439011'}, + {$set: {'answers.$': answersUpdate}}); + + assert.deepEqual(q.getUpdate().$set['answers.$'].stats.toObject(), + { votes: 1, count: 3 }); + done(); }); - var q = Test.update({}, { - $push: { - arr: { - $each: [{ date: new Date(), value: 1 }], - $sort: { value: -1, date: -1 } + it('$geoWithin with single nested schemas (gh-4044)', function(done) { + var locationSchema = new Schema({ + type: { type: String }, + coordinates: [] + }, { _id:false }); + + var schema = new Schema({ + title : String, + location: { type: locationSchema, required: true } + }); + schema.index({ location: '2dsphere' }); + + var Model = db.model('gh4044', schema); + + var query = { + location:{ + $geoWithin:{ + $geometry:{ + type: 'Polygon', + coordinates: [[[-1,0],[-1,3],[4,3],[4,0],[-1,0]]] + } + } } - } + }; + Model.find(query, function(error) { + assert.ifError(error); + done(); + }); }); - assert.deepEqual(Object.keys(q.getUpdate().$push.arr.$sort), - ['value', 'date']); - done(); - }); + it('setDefaultsOnInsert with empty update (gh-3825)', function(done) { + var schema = new mongoose.Schema({ + test: { type: Number, default: 8472 }, + name: String + }); + + var MyModel = db.model('gh3825', schema); - it('handles nested $ (gh-3265)', function(done) { - var db = start(); - var Post = db.model('gh3265', { - title: String, - answers: [{ - details: String, - stats: { - votes: Number, - count: Number + var opts = { setDefaultsOnInsert: true, upsert: true }; + MyModel.update({}, {}, opts, function(error) { + assert.ifError(error); + MyModel.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(doc); + assert.strictEqual(doc.test, 8472); + assert.ok(!doc.name); + done(); + }); + }); + }); + + it('custom query methods (gh-3714)', function(done) { + var schema = new mongoose.Schema({ + name: String + }); + + schema.query.byName = function(name) { + return this.find({ name: name }); + }; + + var MyModel = db.model('gh3714', schema); + + MyModel.create({ name: 'Val' }, function(error) { + assert.ifError(error); + MyModel.find().byName('Val').exec(function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 1); + assert.equal(docs[0].name, 'Val'); + done(); + }); + }); + }); + + it('string as input (gh-4378)', function(done) { + var schema = new mongoose.Schema({ + name: String + }); + + var MyModel = db.model('gh4378', schema); + + MyModel.findOne('', function(error) { + assert.ok(error); + assert.equal(error.name, 'ObjectParameterError'); + done(); + }); + }); + + it('handles geoWithin with $center and mongoose object (gh-4419)', function(done) { + var areaSchema = new Schema({ + name: String, + circle: Array + }); + var Area = db.model('gh4419', areaSchema); + + var placeSchema = new Schema({ + name: String, + geometry: { + type: { + type: String, + enum: ['Point'], + default: 'Point' + }, + coordinates: { type: [Number] } } - }] + }); + placeSchema.index({ geometry: '2dsphere' }); + var Place = db.model('gh4419_0', placeSchema); + + var tromso = new Area({ + name: 'Tromso, Norway', + circle: [[18.89, 69.62], 10 / 3963.2] + }); + tromso.save(function(error) { + assert.ifError(error); + + var airport = { + name: 'Center', + geometry: { + type: 'Point', + coordinates: [18.895, 69.67] + } + }; + Place.create(airport, function(error) { + assert.ifError(error); + var q = { + geometry: { + $geoWithin: { + $centerSphere: tromso.circle + } + } + }; + Place.find(q).exec(function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 1); + assert.equal(docs[0].name, 'Center'); + done(); + }); + }); + }); + }); + + it('$not with objects (gh-4495)', function(done) { + var schema = new Schema({ + createdAt: Date + }); + + var M = db.model('gh4495', schema); + var q = M.find({ + createdAt:{ + $not:{ + $gte: '2016/09/02 00:00:00', + $lte: '2016/09/02 23:59:59' + } + } + }); + q._castConditions(); + + assert.ok(q._conditions.createdAt.$not.$gte instanceof Date); + assert.ok(q._conditions.createdAt.$not.$lte instanceof Date); + done(); + }); + + it('geoIntersects with mongoose doc as coords (gh-4408)', function(done) { + var lineStringSchema = new Schema({ + name: String, + geo: { + type: { type: String, default: 'LineString' }, + coordinates: [[Number]] + } + }); + + var LineString = db.model('gh4408', lineStringSchema); + + var ls = { + name: 'test', + geo: { + coordinates: [ [14.59, 24.847], [28.477, 15.961] ] + } + }; + var ls2 = { + name: 'test2', + geo: { + coordinates: [ [27.528, 25.006], [14.063, 15.591] ] + } + }; + LineString.create(ls, ls2, function(error, ls1) { + assert.ifError(error); + var query = { + geo: { + $geoIntersects: { + $geometry: { + type: 'LineString', + coordinates: ls1.geo.coordinates + } + } + } + }; + LineString.find(query, function(error, results) { + assert.ifError(error); + assert.equal(results.length, 2); + done(); + }); + }); + }); + + it('string with $not (gh-4592)', function(done) { + var TestSchema = new Schema({ + test: String + }); + + var Test = db.model('gh4592', TestSchema); + + Test.findOne({ test: { $not: /test/ } }, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('runSettersOnQuery works with _id field (gh-5351)', function(done) { + var testSchema = new Schema({ + val: { type: String } + }, { runSettersOnQuery: true }); + + var Test = db.model('gh5351', testSchema); + Test.create({ val: 'A string' }). + then(function() { + return Test.findOne({}); + }). + then(function(doc) { + return Test.findOneAndUpdate({_id: doc._id}, { + $set: { + val: 'another string' + } + }, { new: true }); + }). + then(function(doc) { + assert.ok(doc); + assert.equal(doc.val, 'another string'); + }). + then(done). + catch(done); + }); + + it('$exists under $not (gh-4933)', function(done) { + var TestSchema = new Schema({ + test: String + }); + + var Test = db.model('gh4933', TestSchema); + + Test.findOne({ test: { $not: { $exists: true } } }, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('geojson underneath array (gh-5467)', function(done) { + var storySchema = new Schema({ + name: String, + gallery: [{ + src: String, + location: { + type: { type: String, enum: ['Point'] }, + coordinates: { type: [Number], default: void 0 } + }, + timestamp: Date + }] + }); + storySchema.index({ 'gallery.location': '2dsphere' }); + + var Story = db.model('gh5467', storySchema); + + var q = { + 'gallery.location': { + $near: { + $geometry: { + type: 'Point', + coordinates: [51.53377166666667, -0.1197471666666667] + }, + $maxDistance: 500 + } + } + }; + Story.once('index', function(error) { + assert.ifError(error); + Story.update(q, { name: 'test' }, { upsert: true }, function(error) { + assert.ifError(error); + done(); + }); + }); + }); + + it('slice respects schema projections (gh-5450)', function(done) { + var gameSchema = Schema({ + name: String, + developer: { + type: String, + select: false + }, + arr: [Number] + }); + var Game = db.model('gh5450', gameSchema); + + Game.create({ name: 'Mass Effect', developer: 'BioWare', arr: [1, 2, 3] }, function(error) { + assert.ifError(error); + Game.findOne({ name: 'Mass Effect' }).slice({ arr: 1 }).exec(function(error, doc) { + assert.ifError(error); + assert.equal(doc.name, 'Mass Effect'); + assert.deepEqual(doc.toObject().arr, [1]); + assert.ok(!doc.developer); + done(); + }); + }); + }); + + it('$exists for arrays and embedded docs (gh-4937)', function(done) { + var subSchema = new Schema({ + name: String + }); + var TestSchema = new Schema({ + test: [String], + sub: subSchema + }); + + var Test = db.model('gh4937', TestSchema); + + var q = { test: { $exists: true }, sub: { $exists: false } }; + Test.findOne(q, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('report error in pre hook (gh-5520)', function(done) { + var TestSchema = new Schema({ name: String }); + + var ops = [ + 'count', + 'find', + 'findOne', + 'findOneAndRemove', + 'findOneAndUpdate', + 'replaceOne', + 'update', + 'updateOne', + 'updateMany' + ]; + + ops.forEach(function(op) { + TestSchema.pre(op, function(next) { + this.error(new Error(op + ' error')); + next(); + }); + }); + + var TestModel = db.model('gh5520', TestSchema); + + var numOps = ops.length; + + ops.forEach(function(op) { + TestModel.find({}).update({ name: 'test' })[op](function(error) { + assert.ok(error); + assert.equal(error.message, op + ' error'); + --numOps || done(); + }); + }); }); - var answersUpdate = { details: 'blah', stats: { votes: 1, count: '3' } }; - var q = Post.update( - { 'answers._id': '507f1f77bcf86cd799439011' }, - { $set: { 'answers.$': answersUpdate } }); + it('cast error with custom error (gh-5520)', function(done) { + var TestSchema = new Schema({ name: Number }); + + var TestModel = db.model('gh5520_0', TestSchema); - assert.deepEqual(q.getUpdate().$set['answers.$'].stats, - { votes: 1, count: 3 }); - done(); + TestModel. + find({ name: 'not a number' }). + error(new Error('woops')). + exec(function(error) { + assert.ok(error); + // CastError check happens **after** `.error()` + assert.equal(error.name, 'CastError'); + done(); + }); + }); + + it('change deleteOne to updateOne for soft deletes using $isDeleted (gh-4428)', function(done) { + var schema = new mongoose.Schema({ + name: String, + isDeleted: Boolean + }); + + schema.pre('remove', function(next) { + var _this = this; + this.update({ isDeleted: true }, function(error) { + // Force mongoose to consider this doc as deleted. + _this.$isDeleted(true); + next(error); + }); + }); + + var M = db.model('gh4428', schema); + + M.create({ name: 'test' }, function(error, doc) { + assert.ifError(error); + doc.remove(function(error) { + assert.ifError(error); + M.findById(doc._id, function(error, doc) { + assert.ifError(error); + assert.ok(doc); + assert.equal(doc.isDeleted, true); + done(); + }); + }); + }); + }); + + it('child schema with select: false in multiple paths (gh-5603)', function(done) { + var ChildSchema = new mongoose.Schema({ + field: { + type: String, + select: false + }, + _id: false + }, { id: false }); + + var ParentSchema = new mongoose.Schema({ + child: ChildSchema, + child2: ChildSchema + }); + var Parent = db.model('gh5603', ParentSchema); + var ogParent = new Parent(); + ogParent.child = { field: 'test' }; + ogParent.child2 = { field: 'test' }; + ogParent.save(function(error) { + assert.ifError(error); + Parent.findById(ogParent._id).exec(function(error, doc) { + assert.ifError(error); + assert.ok(!doc.child.field); + assert.ok(!doc.child2.field); + done(); + }); + }); + }); + + it('errors in post init (gh-5592)', function(done) { + var TestSchema = new Schema(); + + var count = 0; + TestSchema.post('init', function(model, next) { + return next(new Error('Failed! ' + (count++))); + }); + + var TestModel = db.model('gh5592', TestSchema); + + var docs = []; + for (var i = 0; i < 10; ++i) { + docs.push({}); + } + + TestModel.create(docs, function(error) { + assert.ifError(error); + TestModel.find({}, function(error) { + assert.ok(error); + assert.equal(error.message, 'Failed! 0'); + assert.equal(count, 10); + done(); + }); + }); + }); + + it('with non-object args (gh-1698)', function(done) { + var schema = new mongoose.Schema({ + email: String + }); + var M = db.model('gh1698', schema); + + M.find(42, function(error) { + assert.ok(error); + assert.equal(error.name, 'ObjectParameterError'); + done(); + }); + }); + + it('queries with BSON overflow (gh-5812)', function(done) { + this.timeout(10000); + + var schema = new mongoose.Schema({ + email: String + }); + + var model = db.model('gh5812', schema); + var bigData = new Array(800000); + + for (var i = 0; i < bigData.length; ++i) { + bigData[i] = 'test1234567890'; + } + + model.find({email: {$in: bigData}}).lean(). + then(function() { + done(new Error('Expected an error')); + }). + catch(function(error) { + assert.ok(error); + assert.ok(error.message !== 'Expected error'); + done(); + }); + }); + + it('handles geoWithin with mongoose docs (gh-4392)', function(done) { + var areaSchema = new Schema({ + name: {type: String}, + loc: { + type: { + type: String, + enum: ['Polygon'], + default: 'Polygon' + }, + coordinates: [[[Number]]] + } + }); + + var Area = db.model('gh4392_0', areaSchema); + + var observationSchema = new Schema({ + geometry: { + type: { + type: String, + enum: ['Point'], + default: 'Point' + }, + coordinates: { type: [Number] } + }, + properties: { + temperature: { type: Number } + } + }); + observationSchema.index({ geometry: '2dsphere' }); + + var Observation = db.model('gh4392_1', observationSchema); + + Observation.on('index', function(error) { + assert.ifError(error); + var tromso = new Area({ + name: 'Tromso, Norway', + loc: { + type: 'Polygon', + coordinates: [[ + [18.89, 69.62], + [18.89, 69.72], + [19.03, 69.72], + [19.03, 69.62], + [18.89, 69.62] + ]] + } + }); + tromso.save(function(error) { + assert.ifError(error); + var observation = { + geometry: { + type: 'Point', + coordinates: [18.895, 69.67] + } + }; + Observation.create(observation, function(error) { + assert.ifError(error); + + Observation. + find(). + where('geometry').within().geometry(tromso.loc). + exec(function(error, docs) { + assert.ifError(error); + assert.equal(docs.length, 1); + done(); + }); + }); + }); + }); + }); }); describe('handles falsy and object projections with defaults (gh-3256)', function() { - var db = start(); + var db; var MyModel; before(function(done) { @@ -1595,35 +2442,34 @@ describe('Query', function() { }); }); - after(function() { - db.close(); + after(function(done) { + db.close(done); }); it('falsy projection', function(done) { - MyModel.findOne({ name: 'John' }, { lastName: false }). - exec(function(error, person) { - assert.ifError(error); - assert.equal(person.salary, 25000); - done(); - }); + MyModel.findOne({name: 'John'}, {lastName: false}). + exec(function(error, person) { + assert.ifError(error); + assert.equal(person.salary, 25000); + done(); + }); }); it('slice projection', function(done) { - MyModel.findOne({ name: 'John' }, { dependents: { $slice: 1 } }). - exec(function(error, person) { - assert.ifError(error); - assert.equal(person.salary, 25000); - done(); - }); + MyModel.findOne({name: 'John'}, {dependents: {$slice: 1}}).exec(function(error, person) { + assert.ifError(error); + assert.equal(person.salary, 25000); + done(); + }); }); it('empty projection', function(done) { - MyModel.findOne({ name: 'John' }, {}). - exec(function(error, person) { - assert.ifError(error); - assert.equal(person.salary, 25000); - done(); - }); + MyModel.findOne({name: 'John'}, {}). + exec(function(error, person) { + assert.ifError(error); + assert.equal(person.salary, 25000); + done(); + }); }); }); }); diff --git a/test/query.toconstructor.test.js b/test/query.toconstructor.test.js index 5f43be47600..74625561026 100644 --- a/test/query.toconstructor.test.js +++ b/test/query.toconstructor.test.js @@ -1,47 +1,59 @@ +var start = require('./common'), + mongoose = start.mongoose, + Schema = mongoose.Schema, + assert = require('power-assert'), + random = require('../lib/utils').random, + Query = require('../lib/query'); -var start = require('./common') - , mongoose = start.mongoose - , Schema = mongoose.Schema - , assert = require('assert') - , random = require('../lib/utils').random - , Query = require('../lib/query'); +describe('Query:', function() { + var Comment; + var Product; + var prodName; + var cName; + + before(function() { + Comment = new Schema({ + text: String + }); + Product = new Schema({ + tags: {}, // mixed + array: Array, + ids: [Schema.ObjectId], + strings: [String], + numbers: [Number], + comments: [Comment], + title: String + }); + prodName = 'Product' + random(); + mongoose.model(prodName, Product); + cName = 'Comment' + random(); + mongoose.model(cName, Comment); + }); -var Comment = new Schema({ - text: String -}); + describe('toConstructor', function() { + var db; -var Product = new Schema({ - tags: {} // mixed - , array: Array - , ids: [Schema.ObjectId] - , strings: [String] - , numbers: [Number] - , comments: [Comment] - , title : String -}); -var prodName = 'Product' + random(); -var cName = 'Comment' + random(); -mongoose.model(prodName, Product); -mongoose.model(cName, Comment); + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); -describe('Query:', function() { - describe('toConstructor', function() { it('creates a query', function(done) { - var db = start(); var Product = db.model(prodName); - var prodQ = Product.find({ title : /test/ }).toConstructor(); + var prodQ = Product.find({title: /test/}).toConstructor(); assert.ok(prodQ() instanceof Query); - - db.close(done); + done(); }); it('copies all the right values', function(done) { - var db = start(); var Product = db.model(prodName); - var prodQ = Product.update({ title: /test/ }, { title : 'blah' }); + var prodQ = Product.update({title: /test/}, {title: 'blah'}); var prodC = prodQ.toConstructor(); @@ -54,19 +66,16 @@ describe('Query:', function() { assert.deepEqual(prodQ.model, prodC().model); assert.deepEqual(prodQ.mongooseCollection, prodC().mongooseCollection); assert.deepEqual(prodQ._mongooseOptions, prodC()._mongooseOptions); - - db.close(done); + done(); }); it('gets expected results', function(done) { - var db = start(); var Product = db.model(prodName); - Product.create({ title : 'this is a test' }, function(err, p) { + Product.create({title: 'this is a test'}, function(err, p) { assert.ifError(err); - var prodC = Product.find({ title : /test/ }).toConstructor(); + var prodC = Product.find({title: /test/}).toConstructor(); prodC().exec(function(err, results) { - db.close(); assert.ifError(err); assert.equal(results.length, 1); assert.equal(p.title, results[0].title); @@ -76,25 +85,23 @@ describe('Query:', function() { }); it('can be re-used multiple times', function(done) { - var db = start(); var Product = db.model(prodName); - Product.create([{ title : 'moar thing' }, {title : 'second thing' }], function(err, prods) { + Product.create([{title: 'moar thing'}, {title: 'second thing'}], function(err, prods) { assert.ifError(err); assert.equal(prods.length, 2); var prod = prods[0]; - var prodC = Product.find({ title : /thing/ }).toConstructor(); + var prodC = Product.find({title: /thing/}).toConstructor(); prodC().exec(function(err, results) { assert.ifError(err); assert.equal(results.length, 2); - prodC().find({ _id : prod.id }).exec(function(err, res) { + prodC().find({_id: prod.id}).exec(function(err, res) { assert.ifError(err); assert.equal(res.length, 1); prodC().exec(function(err, res) { - db.close(); assert.ifError(err); assert.equal(res.length, 2); done(); @@ -105,45 +112,40 @@ describe('Query:', function() { }); it('options get merged properly', function(done) { - var db = start(); var Product = db.model(prodName); - db.close(); - var prodC = Product.find({ title : /blah/ }).setOptions({ sort : 'title', lean : true }); + var prodC = Product.find({title: /blah/}).setOptions({sort: 'title', lean: true}); prodC = prodC.toConstructor(); - var nq = prodC(null, { limit : 3 }); - assert.deepEqual(nq._mongooseOptions, { lean : true, limit : 3 }); - assert.deepEqual(nq.options, { sort : { 'title': 1 }, limit : 3 }); + var nq = prodC(null, {limit: 3}); + assert.deepEqual(nq._mongooseOptions, {lean: true, limit: 3}); + assert.deepEqual(nq.options, {sort: {title: 1}, limit: 3, retainKeyOrder: false}); done(); }); it('options get cloned (gh-3176)', function(done) { - var db = start(); var Product = db.model(prodName); - db.close(); - var prodC = Product.find({ title : /blah/ }).setOptions({ sort : 'title', lean : true }); + var prodC = Product.find({title: /blah/}).setOptions({sort: 'title', lean: true}); prodC = prodC.toConstructor(); - var nq = prodC(null, { limit : 3 }); - assert.deepEqual(nq._mongooseOptions, { lean : true, limit : 3 }); - assert.deepEqual(nq.options, { sort : { 'title': 1 }, limit : 3 }); - var nq2 = prodC(null, { limit: 5 }); - assert.deepEqual(nq._mongooseOptions, { lean : true, limit : 3 }); - assert.deepEqual(nq2._mongooseOptions, { lean : true, limit : 5 }); + var nq = prodC(null, {limit: 3}); + assert.deepEqual(nq._mongooseOptions, {lean: true, limit: 3}); + assert.deepEqual(nq.options, {sort: {title: 1}, limit: 3, retainKeyOrder: false}); + var nq2 = prodC(null, {limit: 5}); + assert.deepEqual(nq._mongooseOptions, {lean: true, limit: 3}); + assert.deepEqual(nq2._mongooseOptions, {lean: true, limit: 5}); + done(); }); it('creates subclasses of mquery', function(done) { - var db = start(); var Product = db.model(prodName); - db.close(); - var opts = { safe: { w: 'majority' }, readPreference: 'p' }; - var match = { title: 'test', count: { $gt: 101 }}; - var select = { name: 1, count: 0 }; - var update = { $set: { title : 'thing' }}; + var opts = {safe: {w: 'majority'}, readPreference: 'p', retainKeyOrder: true}; + var match = {title: 'test', count: {$gt: 101}}; + var select = {name: 1, count: 0}; + var update = {$set: {title: 'thing'}}; var path = 'title'; var q = Product.update(match, update); @@ -164,5 +166,21 @@ describe('Query:', function() { assert.equal('find', m.op); done(); }); + + it('with findOneAndUpdate (gh-4318)', function(done) { + var Product = db.model(prodName); + + var Q = Product.where({ title: 'test' }).toConstructor(); + + var query = { 'tags.test': 1 }; + var update = { + strings: ['123'], + numbers: [1, 2, 3] + }; + Q().findOneAndUpdate(query, update, function(error) { + assert.ifError(error); + done(); + }); + }); }); }); diff --git a/test/schema.alias.test.js b/test/schema.alias.test.js new file mode 100644 index 00000000000..c2d7184d838 --- /dev/null +++ b/test/schema.alias.test.js @@ -0,0 +1,104 @@ + +/** + * Module dependencies. + */ + +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema; + +describe('schema alias option', function() { + it('works with all basic schema types', function(done) { + var db = start(); + + var schema = new Schema({ + string: { type: String, alias: 'StringAlias' }, + number: { type: Number, alias: 'NumberAlias' }, + date: { type: Date, alias: 'DateAlias' }, + buffer: { type: Buffer, alias: 'BufferAlias' }, + boolean: { type: Boolean, alias: 'BooleanAlias' }, + mixed: { type: Schema.Types.Mixed, alias: 'MixedAlias' }, + objectId: { type: Schema.Types.ObjectId, alias: 'ObjectIdAlias'}, + array: { type: [], alias: 'ArrayAlias' } + }); + + var S = db.model('AliasSchemaType', schema); + S.create({ + string: 'hello', + number: 1, + date: new Date(), + buffer: new Buffer('World'), + boolean: false, + mixed: [1, [], 'three', { four: 5 }], + objectId: new mongoose.Types.ObjectId(), + array: ['a', 'b', 'c', 'd'] + }, function(err, s) { + assert.ifError(err); + + // Comparing with aliases + assert.equal(s.string, s.StringAlias); + assert.equal(s.number, s.NumberAlias); + assert.equal(s.date, s.DateAlias); + assert.equal(s.buffer, s.BufferAlias); + assert.equal(s.boolean, s.BooleanAlias); + assert.equal(s.mixed, s.MixedAlias); + assert.equal(s.objectId, s.ObjectIdAlias); + assert.equal(s.array, s.ArrayAlias); + done(); + }); + }); + + it('works with nested schema types', function(done) { + var db = start(); + + var schema = new Schema({ + nested: { + string: { type: String, alias: 'StringAlias' }, + number: { type: Number, alias: 'NumberAlias' }, + date: { type: Date, alias: 'DateAlias' }, + buffer: { type: Buffer, alias: 'BufferAlias' }, + boolean: { type: Boolean, alias: 'BooleanAlias' }, + mixed: { type: Schema.Types.Mixed, alias: 'MixedAlias' }, + objectId: { type: Schema.Types.ObjectId, alias: 'ObjectIdAlias'}, + array: { type: [], alias: 'ArrayAlias' } + } + }); + + var S = db.model('AliasNestedSchemaType', schema); + S.create({ + nested: { + string: 'hello', + number: 1, + date: new Date(), + buffer: new Buffer('World'), + boolean: false, + mixed: [1, [], 'three', { four: 5 }], + objectId: new mongoose.Types.ObjectId(), + array: ['a', 'b', 'c', 'd'] + } + }, function(err, s) { + assert.ifError(err); + + // Comparing with aliases + assert.equal(s.nested.string, s.StringAlias); + assert.equal(s.nested.number, s.NumberAlias); + assert.equal(s.nested.date, s.DateAlias); + assert.equal(s.nested.buffer, s.BufferAlias); + assert.equal(s.nested.boolean, s.BooleanAlias); + assert.equal(s.nested.mixed, s.MixedAlias); + assert.equal(s.nested.objectId, s.ObjectIdAlias); + assert.equal(s.nested.array, s.ArrayAlias); + + done(); + }); + }); + + it('throws when alias option is invalid', function() { + assert.throws(function() { + new Schema({ + foo: { type: String, alias: 456 } + }); + }); + }); +}); diff --git a/test/schema.boolean.test.js b/test/schema.boolean.test.js index 14f374da924..59b77b916aa 100644 --- a/test/schema.boolean.test.js +++ b/test/schema.boolean.test.js @@ -3,21 +3,21 @@ * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , Schema = mongoose.Schema; +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema; describe('schematype', function() { describe('boolean', function() { it('null default is permitted (gh-523)', function(done) { - var db = start() - , s1 = new Schema({ b: { type: Boolean, default: null }}) - , M1 = db.model('NullDateDefaultIsAllowed1', s1) - , s2 = new Schema({ b: { type: Boolean, default: false }}) - , M2 = db.model('NullDateDefaultIsAllowed2', s2) - , s3 = new Schema({ b: { type: Boolean, default: true }}) - , M3 = db.model('NullDateDefaultIsAllowed3', s3); + var db = start(), + s1 = new Schema({b: {type: Boolean, default: null}}), + M1 = db.model('NullDateDefaultIsAllowed1', s1), + s2 = new Schema({b: {type: Boolean, default: false}}), + M2 = db.model('NullDateDefaultIsAllowed2', s2), + s3 = new Schema({b: {type: Boolean, default: true}}), + M3 = db.model('NullDateDefaultIsAllowed3', s3); db.close(); @@ -29,5 +29,60 @@ describe('schematype', function() { assert.strictEqual(true, m3.b); done(); }); + + it('strictBool option (gh-5211)', function(done) { + var db = start(), + s1 = new Schema({b: {type: Boolean, strictBool: true}}), + M1 = db.model('StrictBoolTrue', s1); + db.close(); + + var strictValues = [true, false, 'true', 'false', 0, 1, '0', '1']; + + var testsRemaining = strictValues.length; + strictValues.forEach(function(value) { + var doc = new M1; + doc.b = value; + doc.validate(function(error) { + if (error) { + // test fails as soon as one value fails + return done(error); + } + if (!--testsRemaining) { + return done(); + } + }); + }); + }); + + it('strictBool schema option', function(done) { + var db = start(), + s1 = new Schema({b: {type: Boolean}}, {strictBool: true}), + M1 = db.model('StrictBoolTrue', s1); + db.close(); + + var strictValues = [true, false, 'true', 'false', 0, 1, '0', '1']; + + strictValues.forEach(function(value) { + var doc = new M1; + doc.b = value; + doc.validate(function(error) { + if (error) { + // test fails as soon as one value fails + return done(error); + } + }); + }); + + var doc = new M1; + doc.b = 'Not a boolean'; + doc.validate(function(error) { + if (error) { + done(); + } else { + done(new Error('ValidationError expected')); + } + }); + }); + }); }); diff --git a/test/schema.documentarray.test.js b/test/schema.documentarray.test.js index 37811fe8e80..a69c704d54a 100644 --- a/test/schema.documentarray.test.js +++ b/test/schema.documentarray.test.js @@ -1,12 +1,11 @@ - /** * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , Schema = mongoose.Schema; +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema; /** * Test. @@ -14,22 +13,61 @@ var start = require('./common') describe('schema.documentarray', function() { it('defaults should be preserved', function(done) { - var child = new Schema({ title: String }); + var child = new Schema({title: String}); - var schema1 = new Schema({ x: { type: [child], default: [{ title: 'Prometheus'}] }}); - var schema2 = new Schema({ x: { type: [child], default: { title: 'Prometheus'} }}); - var schema3 = new Schema({ x: { type: [child], default: function() { return [{ title: 'Prometheus'}];} }}); + var schema1 = new Schema({x: {type: [child], default: [{title: 'Prometheus'}]}}); + var schema2 = new Schema({x: {type: [child], default: {title: 'Prometheus'}}}); + var schema3 = new Schema({ + x: { + type: [child], default: function() { + return [{title: 'Prometheus'}]; + } + } + }); var M = mongoose.model('DefaultDocArrays1', schema1); var N = mongoose.model('DefaultDocArrays2', schema2); var O = mongoose.model('DefaultDocArrays3', schema3); - [M,N,O].forEach(function(M) { + [M, N, O].forEach(function(M) { var m = new M; assert.ok(Array.isArray(m.x)); - assert.equal(1, m.x.length); - assert.equal('Prometheus', m.x[0].title); + assert.equal(m.x.length, 1); + assert.equal(m.x[0].title, 'Prometheus'); + }); + done(); + }); + + it('only sets if document has same schema (gh-3701)', function(done) { + var schema1 = new Schema({ + arr: [new Schema({a: Number, b: Number}, {_id: false})] + }); + var schema2 = new Schema({ + arr: [new Schema({a: Number}, {_id: false})] + }); + + var Model1 = mongoose.model('gh3701_0', schema1); + var Model2 = mongoose.model('gh3701_1', schema2); + + var source = new Model1({arr: [{a: 1, b: 1}, {a: 2, b: 2}]}); + var dest = new Model2({arr: source.arr}); + + assert.deepEqual(dest.toObject().arr, [{a: 1}, {a: 2}]); + done(); + }); + + it('sets $implicitlyCreated if created by interpretAsType (gh-4271)', function(done) { + var schema1 = new Schema({ + arr: [{ name: String }] }); + var schema2 = new Schema({ + arr: [new Schema({ name: String })] + }); + + assert.equal(schema1.childSchemas.length, 1); + assert.equal(schema2.childSchemas.length, 1); + assert.ok(schema1.childSchemas[0].schema.$implicitlyCreated); + assert.ok(!schema2.childSchemas[0].schema.$implicitlyCreated); done(); }); }); diff --git a/test/schema.mixed.test.js b/test/schema.mixed.test.js index 8697e6dcab7..43b122a0fbd 100644 --- a/test/schema.mixed.test.js +++ b/test/schema.mixed.test.js @@ -3,32 +3,32 @@ * Module dependencies. */ -var start = require('./common') - , mongoose = new start.mongoose.Mongoose - , assert = require('assert') - , Schema = mongoose.Schema; +var start = require('./common'), + mongoose = new start.mongoose.Mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema; describe('schematype mixed', function() { describe('empty object defaults (gh-1380)', function() { it('are interpreted as fns that return new empty objects', function(done) { - var s = Schema({ mix: { type: Schema.Types.Mixed, default: {} }}); + var s = new Schema({mix: {type: Schema.Types.Mixed, default: {}}}); var M = mongoose.model('M1', s); var m1 = new M; var m2 = new M; m1.mix.val = 3; - assert.equal(3, m1.mix.val); - assert.equal(undefined, m2.mix.val); + assert.equal(m1.mix.val, 3); + assert.equal(m2.mix.val, undefined); done(); }); it('can be forced to share the object between documents', function(done) { // silly but necessary for backwards compatibility - var s = Schema({ mix: { type: Schema.Types.Mixed, default: {}, shared: true }}); + var s = new Schema({mix: {type: Schema.Types.Mixed, default: {}, shared: true}}); var M = mongoose.model('M2', s); var m1 = new M; var m2 = new M; m1.mix.val = 3; - assert.equal(3, m1.mix.val); - assert.equal(3, m2.mix.val); + assert.equal(m1.mix.val, 3); + assert.equal(m2.mix.val, 3); done(); }); }); diff --git a/test/schema.onthefly.test.js b/test/schema.onthefly.test.js index 15bc872d479..78bc6486850 100644 --- a/test/schema.onthefly.test.js +++ b/test/schema.onthefly.test.js @@ -1,73 +1,74 @@ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , ObjectId = Schema.ObjectId; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema, + ObjectId = Schema.ObjectId; -/** - * Setup. - */ +describe('schema.onthefly', function() { + var DecoratedSchema; + var collection; -var DecoratedSchema = new Schema({ - title : String -}, { strict: false }); + before(function() { + DecoratedSchema = new Schema({ + title: String + }, {strict: false}); -mongoose.model('Decorated', DecoratedSchema); + mongoose.model('Decorated', DecoratedSchema); -var collection = 'decorated_' + random(); + collection = 'decorated_' + random(); + }); -describe('schema.onthefly', function() { it('setting should cache the schema type and cast values appropriately', function(done) { - var db = start() - , Decorated = db.model('Decorated', collection); + var db = start(), + Decorated = db.model('Decorated', collection); db.close(); var post = new Decorated(); post.set('adhoc', '9', Number); - assert.equal(9, post.get('adhoc').valueOf()); + assert.equal(post.get('adhoc').valueOf(), 9); done(); }); it('should be local to the particular document', function(done) { - var db = start() - , Decorated = db.model('Decorated', collection); + var db = start(), + Decorated = db.model('Decorated', collection); db.close(); var postOne = new Decorated(); postOne.set('adhoc', '9', Number); - assert.notStrictEqual(postOne.$__path('adhoc'),undefined); + assert.notStrictEqual(postOne.$__path('adhoc'), undefined); var postTwo = new Decorated(); - assert.notStrictEqual(postTwo.$__path('title'),undefined); + assert.notStrictEqual(postTwo.$__path('title'), undefined); assert.strictEqual(undefined, postTwo.$__path('adhoc')); done(); }); it('querying a document that had an on the fly schema should work', function(done) { - var db = start() - , Decorated = db.model('Decorated', collection); + var db = start(), + Decorated = db.model('Decorated', collection); var post = new Decorated({title: 'AD HOC'}); // Interpret adhoc as a Number post.set('adhoc', '9', Number); - assert.equal(9, post.get('adhoc').valueOf()); + assert.equal(post.get('adhoc').valueOf(), 9); post.save(function(err) { assert.ifError(err); assert.strictEqual(null, err); Decorated.findById(post.id, function(err, found) { db.close(); assert.strictEqual(null, err); - assert.equal(9, found.get('adhoc')); + assert.equal(found.get('adhoc'), 9); // Interpret adhoc as a String instead of a Number now - assert.equal('9', found.get('adhoc', String)); - assert.equal('9', found.get('adhoc')); + assert.equal(found.get('adhoc', String), '9'); + assert.equal(found.get('adhoc'), '9'); // set adhoc as an Object found.set('adhoc', '3', Object); - assert.equal('string', typeof found.get('adhoc')); + assert.equal(typeof found.get('adhoc'), 'string'); found.set('adhoc', 3, Object); - assert.equal('number', typeof found.get('adhoc')); + assert.equal(typeof found.get('adhoc'), 'number'); found.set('adhoc', ['hello'], Object); assert.ok(Array.isArray(found.get('adhoc'))); @@ -75,47 +76,47 @@ describe('schema.onthefly', function() { assert.ok(Array.isArray(found.get('adhoc'))); found.set('adhoc', 3, String); - assert.equal('string', typeof found.get('adhoc')); + assert.equal(typeof found.get('adhoc'), 'string'); found.set('adhoc', 3, Object); - assert.equal('number', typeof found.get('adhoc')); + assert.equal(typeof found.get('adhoc'), 'number'); done(); }); }); }); it('on the fly Embedded Array schemas should cast properly', function(done) { - var db = start() - , Decorated = db.model('Decorated', collection); + var db = start(), + Decorated = db.model('Decorated', collection); db.close(); var post = new Decorated(); post.set('moderators', [{name: 'alex trebek'}], [new Schema({name: String})]); - assert.equal(post.get('moderators')[0].name,'alex trebek'); + assert.equal(post.get('moderators')[0].name, 'alex trebek'); done(); }); it('on the fly Embedded Array schemas should get from a fresh queried document properly', function(done) { - var db = start() - , Decorated = db.model('Decorated', collection); + var db = start(), + Decorated = db.model('Decorated', collection); - var post = new Decorated() - , ModeratorSchema = new Schema({name: String, ranking: Number}); + var post = new Decorated(), + ModeratorSchema = new Schema({name: String, ranking: Number}); post.set('moderators', [{name: 'alex trebek', ranking: '1'}], [ModeratorSchema]); - assert.equal(post.get('moderators')[0].name,'alex trebek'); + assert.equal(post.get('moderators')[0].name, 'alex trebek'); post.save(function(err) { assert.ifError(err); Decorated.findById(post.id, function(err, found) { db.close(); assert.ifError(err); var rankingPreCast = found.get('moderators')[0].ranking; - assert.equal(1, rankingPreCast); + assert.equal(rankingPreCast, 1); assert.strictEqual(undefined, rankingPreCast.increment); var rankingPostCast = found.get('moderators', [ModeratorSchema])[0].ranking; - assert.equal(1, rankingPostCast); + assert.equal(rankingPostCast, 1); - var NewModeratorSchema = new Schema({ name: String, ranking: String}); + var NewModeratorSchema = new Schema({name: String, ranking: String}); rankingPostCast = found.get('moderators', [NewModeratorSchema])[0].ranking; - assert.equal(1, rankingPostCast); + assert.equal(rankingPostCast, 1); done(); }); }); @@ -125,14 +126,14 @@ describe('schema.onthefly', function() { var db = start(); var Decorated = db.model('gh2360', DecoratedSchema, 'gh2360'); - var d = new Decorated({ title: '1' }); - assert.equal('number', typeof d.get('title', 'Number')); + var d = new Decorated({title: '1'}); + assert.equal(typeof d.get('title', Number), 'number'); d.title = '000000000000000000000001'; assert.equal(d.get('title', ObjectId).constructor.name, 'ObjectID'); d.set('title', 1, Number); - assert.equal('number', typeof d.get('title')); + assert.equal(typeof d.get('title'), 'number'); db.close(done); }); diff --git a/test/schema.select.test.js b/test/schema.select.test.js index 120685ce632..3a480fb322c 100644 --- a/test/schema.select.test.js +++ b/test/schema.select.test.js @@ -1,56 +1,57 @@ - /** * Test dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = start.mongoose - , random = require('../lib/utils').random - , Schema = mongoose.Schema; +var start = require('./common'), + assert = require('power-assert'), + mongoose = start.mongoose, + random = require('../lib/utils').random, + Schema = mongoose.Schema; describe('schema select option', function() { - it('excluding paths through schematype', function(done) { var db = start(); var schema = new Schema({ - thin: Boolean - , name: { type: String, select: false} - , docs: [new Schema({ bool: Boolean, name: { type: String, select: false }})] + thin: Boolean, + name: {type: String, select: false}, + docs: [new Schema({bool: Boolean, name: {type: String, select: false}})] }); var S = db.model('ExcludingBySchemaType', schema); - S.create({ thin: true, name: 'the excluded', docs:[{bool:true, name: 'test'}] }, function(err, s) { + S.create({thin: true, name: 'the excluded', docs: [{bool: true, name: 'test'}]}, function(err, s) { assert.ifError(err); assert.equal(s.name, 'the excluded'); assert.equal(s.docs[0].name, 'test'); var pending = 5; var item = s; + function cb(err, s) { if (!--pending) { db.close(); } - if (Array.isArray(s)) s = s[0]; + if (Array.isArray(s)) { + s = s[0]; + } assert.strictEqual(null, err); - assert.equal(false, s.isSelected('name')); - assert.equal(false, s.isSelected('docs.name')); + assert.equal(s.isSelected('name'), false); + assert.equal(s.isSelected('docs.name'), false); assert.strictEqual(undefined, s.name); // we need to make sure this executes absolutely last. if (pending === 1) { - S.findOneAndRemove({ _id : item._id }, cb); + S.findOneAndRemove({_id: item._id}, cb); } - if (0 === pending) { + if (pending === 0) { done(); } } S.findById(s).select('-thin -docs.bool').exec(cb); - S.find({ _id: s._id }).select('thin docs.bool').exec(cb); + S.find({_id: s._id}).select('thin docs.bool').exec(cb); S.findById(s, cb); - S.findOneAndUpdate({ _id: s._id }, { name: 'changed' }, cb); + S.findOneAndUpdate({_id: s._id}, {name: 'changed'}, cb); }); }); @@ -58,40 +59,43 @@ describe('schema select option', function() { var db = start(); var schema = new Schema({ - thin: Boolean - , name: { type: String, select: true } - , docs: [new Schema({ bool: Boolean, name: { type: String, select: true }})] + thin: Boolean, + name: {type: String, select: true}, + docs: [new Schema({bool: Boolean, name: {type: String, select: true}})] }); var S = db.model('IncludingBySchemaType', schema); - S.create({ thin: true, name: 'the included', docs:[{bool:true, name: 'test'}] }, function(err, s) { + S.create({thin: true, name: 'the included', docs: [{bool: true, name: 'test'}]}, function(err, s) { assert.ifError(err); assert.equal(s.name, 'the included'); assert.equal(s.docs[0].name, 'test'); var pending = 4; + function cb(err, s) { if (!--pending) { db.close(); } - if (Array.isArray(s)) s = s[0]; + if (Array.isArray(s)) { + s = s[0]; + } assert.strictEqual(null, err); assert.strictEqual(true, s.isSelected('name')); assert.strictEqual(true, s.isSelected('docs.name')); assert.equal(s.name, 'the included'); - if (0 === pending) { + if (pending === 0) { done(); } } S.findById(s).select('-thin -docs.bool').exec(function(err, res) { cb(err, res); - S.find({ _id: s._id }).select('thin docs.bool').exec(function(err, res) { + S.find({_id: s._id}).select('thin docs.bool').exec(function(err, res) { cb(err, res); - S.findOneAndUpdate({ _id: s._id }, { thin: false }, function(err, s) { + S.findOneAndUpdate({_id: s._id}, {thin: false}, function(err, s) { cb(err, s); - S.findOneAndRemove({ _id: s._id }, cb); + S.findOneAndRemove({_id: s._id}, cb); }); }); }); @@ -105,14 +109,14 @@ describe('schema select option', function() { db = start(); selected = new Schema({ - thin: Boolean - , name: { type: String, select: true } - , docs: [new Schema({ name: { type: String, select: true }, bool: Boolean })] + thin: Boolean, + name: {type: String, select: true}, + docs: [new Schema({name: {type: String, select: true}, bool: Boolean})] }); excluded = new Schema({ - thin: Boolean - , name: { type: String, select: false } - , docs: [new Schema({ name: { type: String, select: false }, bool: Boolean})] + thin: Boolean, + name: {type: String, select: false}, + docs: [new Schema({name: {type: String, select: false}, bool: Boolean})] }); S = db.model('OverriddingSelectedBySchemaType', selected); @@ -127,7 +131,7 @@ describe('schema select option', function() { describe('for inclusions', function() { var s; before(function(done) { - S.create({ thin: true, name: 'the included', docs: [{name:'test',bool: true}] }, function(err, s_) { + S.create({thin: true, name: 'the included', docs: [{name: 'test', bool: true}]}, function(err, s_) { assert.ifError(err); s = s_; assert.equal(s.name, 'the included'); @@ -136,7 +140,7 @@ describe('schema select option', function() { }); }); it('with find', function(done) { - S.find({ _id: s._id }).select('thin name docs.bool docs.name').exec(function(err, s) { + S.find({_id: s._id}).select('thin name docs.bool docs.name').exec(function(err, s) { assert.ifError(err); assert.ok(s && s.length > 0, 'no document found'); s = s[0]; @@ -154,19 +158,20 @@ describe('schema select option', function() { it('for findById', function(done) { S.findById(s).select('-name -docs.name').exec(function(err, s) { assert.strictEqual(null, err); - assert.equal(false, s.isSelected('name')); - assert.equal(true, s.isSelected('thin')); - assert.equal(false, s.isSelected('docs.name')); - assert.equal(true, s.isSelected('docs.bool')); + assert.equal(s.isSelected('name'), false); + assert.equal(s.isSelected('thin'), true); + assert.equal(s.isSelected('docs.name'), false); + assert.equal(s.isSelected('docs.bool'), true); + assert.ok(s.isSelected('docs')); assert.strictEqual(undefined, s.name); assert.strictEqual(undefined, s.docs[0].name); - assert.equal(true, s.thin); - assert.equal(true, s.docs[0].bool); + assert.equal(s.thin, true); + assert.equal(s.docs[0].bool, true); done(); }); }); it('with findOneAndUpdate', function(done) { - S.findOneAndUpdate({ _id: s._id }, { name: 'changed' }, { 'new': true }).select('thin name docs.bool docs.name').exec(function(err, s) { + S.findOneAndUpdate({_id: s._id}, {name: 'changed'}, {new: true}).select('thin name docs.bool docs.name').exec(function(err, s) { assert.ifError(err); assert.strictEqual(true, s.isSelected('name')); assert.strictEqual(true, s.isSelected('thin')); @@ -180,16 +185,16 @@ describe('schema select option', function() { }); }); it('for findByIdAndUpdate', function(done) { - S.findByIdAndUpdate(s, { thin: false }, { 'new': true }).select('-name -docs.name').exec(function(err, s) { + S.findByIdAndUpdate(s, {thin: false}, {new: true}).select('-name -docs.name').exec(function(err, s) { assert.strictEqual(null, err); - assert.equal(false, s.isSelected('name')); - assert.equal(true, s.isSelected('thin')); - assert.equal(false, s.isSelected('docs.name')); - assert.equal(true, s.isSelected('docs.bool')); + assert.equal(s.isSelected('name'), false); + assert.equal(s.isSelected('thin'), true); + assert.equal(s.isSelected('docs.name'), false); + assert.equal(s.isSelected('docs.bool'), true); assert.strictEqual(undefined, s.name); assert.strictEqual(undefined, s.docs[0].name); - assert.equal(false, s.thin); - assert.equal(true, s.docs[0].bool); + assert.equal(s.thin, false); + assert.equal(s.docs[0].bool, true); done(); }); }); @@ -198,7 +203,7 @@ describe('schema select option', function() { describe('for exclusions', function() { var e; before(function(done) { - E.create({ thin: true, name: 'the excluded',docs:[{name:'test',bool:true}] }, function(err, e_) { + E.create({thin: true, name: 'the excluded', docs: [{name: 'test', bool: true}]}, function(err, e_) { e = e_; assert.ifError(err); assert.equal(e.name, 'the excluded'); @@ -207,13 +212,13 @@ describe('schema select option', function() { }); }); it('with find', function(done) { - E.find({ _id: e._id }).select('thin name docs.name docs.bool').exec(function(err, e) { + E.find({_id: e._id}).select('thin name docs.name docs.bool').exec(function(err, e) { e = e[0]; assert.strictEqual(null, err); - assert.equal(true, e.isSelected('name')); - assert.equal(true, e.isSelected('thin')); - assert.equal(true, e.isSelected('docs.name')); - assert.equal(true, e.isSelected('docs.bool')); + assert.equal(e.isSelected('name'), true); + assert.equal(e.isSelected('thin'), true); + assert.equal(e.isSelected('docs.name'), true); + assert.equal(e.isSelected('docs.bool'), true); assert.equal(e.name, 'the excluded'); assert.equal(e.docs[0].name, 'test'); assert.ok(e.thin); @@ -224,9 +229,9 @@ describe('schema select option', function() { it('with findById', function(done) { E.findById(e).select('-name -docs.name').exec(function(err, e) { assert.strictEqual(null, err); - assert.equal(e.isSelected('name'),false); + assert.equal(e.isSelected('name'), false); assert.equal(e.isSelected('thin'), true); - assert.equal(e.isSelected('docs.name'),false); + assert.equal(e.isSelected('docs.name'), false); assert.equal(e.isSelected('docs.bool'), true); assert.strictEqual(undefined, e.name); assert.strictEqual(undefined, e.docs[0].name); @@ -236,12 +241,12 @@ describe('schema select option', function() { }); }); it('with findOneAndUpdate', function(done) { - E.findOneAndUpdate({ _id: e._id }, { name: 'changed' }, { 'new': true }).select('thin name docs.name docs.bool').exec(function(err, e) { + E.findOneAndUpdate({_id: e._id}, {name: 'changed'}, {new: true}).select('thin name docs.name docs.bool').exec(function(err, e) { assert.strictEqual(null, err); - assert.equal(true, e.isSelected('name')); - assert.equal(true, e.isSelected('thin')); - assert.equal(true, e.isSelected('docs.name')); - assert.equal(true, e.isSelected('docs.bool')); + assert.equal(e.isSelected('name'), true); + assert.equal(e.isSelected('thin'), true); + assert.equal(e.isSelected('docs.name'), true); + assert.equal(e.isSelected('docs.bool'), true); assert.equal(e.name, 'changed'); assert.equal(e.docs[0].name, 'test'); assert.ok(e.thin); @@ -250,11 +255,11 @@ describe('schema select option', function() { }); }); it('with findOneAndRemove', function(done) { - E.findOneAndRemove({ _id: e._id }).select('-name -docs.name').exec(function(err, e) { + E.findOneAndRemove({_id: e._id}).select('-name -docs.name').exec(function(err, e) { assert.strictEqual(null, err); - assert.equal(e.isSelected('name'),false); + assert.equal(e.isSelected('name'), false); assert.equal(e.isSelected('thin'), true); - assert.equal(e.isSelected('docs.name'),false); + assert.equal(e.isSelected('docs.name'), false); assert.equal(e.isSelected('docs.bool'), true); assert.strictEqual(undefined, e.name); assert.strictEqual(undefined, e.docs[0].name); @@ -271,20 +276,20 @@ describe('schema select option', function() { it('works (gh-1333)', function(done) { var m = new mongoose.Mongoose(); var child = new Schema({ - name1: {type:String, select: false} - , name2: {type:String, select: true} + name1: {type: String, select: false}, + name2: {type: String, select: true} }); var selected = new Schema({ - docs: { type: [child], select: false } + docs: {type: [child], select: false} }); var M = m.model('gh-1333-deselect', selected); var query = M.findOne(); query._applyPaths(); - assert.equal(1, Object.keys(query._fields).length); - assert.equal(undefined, query._fields['docs.name1']); - assert.equal(undefined, query._fields['docs.name2']); - assert.equal(0, query._fields.docs); + assert.equal(Object.keys(query._fields).length, 1); + assert.equal(query._fields['docs.name1'], undefined); + assert.equal(query._fields['docs.name2'], undefined); + assert.equal(query._fields.docs, 0); done(); }); }); @@ -293,43 +298,43 @@ describe('schema select option', function() { it('works', function(done) { var db = start(); var excluded = new Schema({ - thin: Boolean - , name: { type: String, select: false } - , docs: [new Schema({ name: { type: String, select: false }, bool: Boolean })] + thin: Boolean, + name: {type: String, select: false}, + docs: [new Schema({name: {type: String, select: false}, bool: Boolean})] }); var M = db.model('ForcedInclusionOfPath', excluded); - M.create({ thin: false, name: '1 meter', docs:[{name:'test', bool:false}] }, function(err, d) { + M.create({thin: false, name: '1 meter', docs: [{name: 'test', bool: false}]}, function(err, d) { assert.ifError(err); M.findById(d) .select('+name +docs.name') .exec(function(err, doc) { assert.ifError(err); - assert.equal(false, doc.thin); - assert.equal('1 meter', doc.name); - assert.equal(false, doc.docs[0].bool); - assert.equal('test', doc.docs[0].name); + assert.equal(doc.thin, false); + assert.equal(doc.name, '1 meter'); + assert.equal(doc.docs[0].bool, false); + assert.equal(doc.docs[0].name, 'test'); assert.equal(d.id, doc.id); M.findById(d) .select('+name -thin +docs.name -docs.bool') .exec(function(err, doc) { assert.ifError(err); - assert.equal(undefined, doc.thin); - assert.equal('1 meter', doc.name); - assert.equal(undefined, doc.docs[0].bool); - assert.equal('test', doc.docs[0].name); + assert.equal(doc.thin, undefined); + assert.equal(doc.name, '1 meter'); + assert.equal(doc.docs[0].bool, undefined); + assert.equal(doc.docs[0].name, 'test'); assert.equal(d.id, doc.id); M.findById(d) .select('-thin +name -docs.bool +docs.name') .exec(function(err, doc) { assert.ifError(err); - assert.equal(undefined, doc.thin); - assert.equal('1 meter', doc.name); - assert.equal(undefined, doc.docs[0].bool); - assert.equal('test', doc.docs[0].name); + assert.equal(doc.thin, undefined); + assert.equal(doc.name, '1 meter'); + assert.equal(doc.docs[0].bool, undefined); + assert.equal(doc.docs[0].name, 'test'); assert.equal(d.id, doc.id); M.findById(d) @@ -337,10 +342,10 @@ describe('schema select option', function() { .exec(function(err, doc) { db.close(); assert.ifError(err); - assert.equal(undefined, doc.thin); - assert.equal(undefined, doc.name); - assert.equal(undefined, doc.docs[0].bool); - assert.equal(undefined, doc.docs[0].name); + assert.equal(doc.thin, undefined); + assert.equal(doc.name, undefined); + assert.equal(doc.docs[0].bool, undefined); + assert.equal(doc.docs[0].name, undefined); assert.equal(d.id, doc.id); done(); }); @@ -352,16 +357,20 @@ describe('schema select option', function() { it('works with query.slice (gh-1370)', function(done) { var db = start(); - var M = db.model("1370", new Schema({ many: { type: [String], select: false }})); + var M = db.model('1370', new Schema({many: {type: [String], select: false}})); - M.create({ many: ["1", "2", "3", "4", "5"] }, function(err) { - if (err) return done(err); + M.create({many: ['1', '2', '3', '4', '5']}, function(err) { + if (err) { + return done(err); + } - var query = M.findOne().select("+many").where("many").slice(2); + var query = M.findOne().select('+many').where('many').slice(2); query.exec(function(err, doc) { - if (err) return done(err); - assert.equal(2, doc.many.length); + if (err) { + return done(err); + } + assert.equal(doc.many.length, 2); db.close(done); }); }); @@ -372,30 +381,33 @@ describe('schema select option', function() { var db = start(); var schema = new Schema({ - thin: Boolean - , name: { type: String, select: true } - , conflict: { type: String, select: false} + thin: Boolean, + name: {type: String, select: true}, + conflict: {type: String, select: false} }); var S = db.model('ConflictingBySchemaType', schema); - S.create({ thin: true, name: 'bing', conflict: 'crosby' }, function(err, s) { + S.create({thin: true, name: 'bing', conflict: 'crosby'}, function(err, s) { assert.strictEqual(null, err); assert.equal(s.name, 'bing'); assert.equal(s.conflict, 'crosby'); var pending = 2; + function cb(err, s) { if (!--pending) { db.close(done); } - if (Array.isArray(s)) s = s[0]; + if (Array.isArray(s)) { + s = s[0]; + } assert.ifError(err); assert.equal(s.name, 'bing'); - assert.equal(undefined, s.conflict); + assert.equal(s.conflict, undefined); } S.findById(s).exec(cb); - S.find({ _id: s._id }).exec(cb); + S.find({_id: s._id}).exec(cb); }); }); @@ -403,7 +415,7 @@ describe('schema select option', function() { var db = start(); var schema = new Schema({ - name: { type: String, select: false} + name: {type: String, select: false} }); var M = db.model('SelectingOnly_idWithExcludedSchemaType', schema); @@ -422,7 +434,7 @@ describe('schema select option', function() { var db = start(); var schema = new Schema({ - docs: [new Schema({name: { type: String, select: false}})] + docs: [new Schema({name: {type: String, select: false}})] }); var M = db.model('SelectingOnly_idWithExcludedSchemaType', schema); @@ -442,21 +454,21 @@ describe('schema select option', function() { var coll = 'inclusiveexclusivecomboswork_' + random(); var schema = new Schema({ - name: { type: String } - , age: Number - }, { collection: coll }); + name: {type: String}, + age: Number + }, {collection: coll}); var M = db.model('InclusiveExclusiveCombosWork', schema); var schema1 = new Schema({ - name: { type: String, select: false } - , age: Number - }, { collection: coll }); + name: {type: String, select: false}, + age: Number + }, {collection: coll}); var S = db.model('InclusiveExclusiveCombosWorkWithSchemaSelectionFalse', schema1); var schema2 = new Schema({ - name: { type: String, select: true } - , age: Number - }, { collection: coll }); + name: {type: String, select: true}, + age: Number + }, {collection: coll}); var T = db.model('InclusiveExclusiveCombosWorkWithSchemaSelectionTrue', schema2); function useId(M, id, cb) { @@ -466,19 +478,19 @@ describe('schema select option', function() { M.findOne().select('-_id name').exec(function(err, d) { // mongo special case for exclude _id + include path assert.ifError(err); - assert.equal(undefined, d.id); - assert.equal('ssd', d.name); - assert.equal(undefined, d.age); + assert.equal(d.id, undefined); + assert.equal(d.name, 'ssd'); + assert.equal(d.age, undefined); M.findOne().select('-_id -name').exec(function(err, d) { assert.ifError(err); - assert.equal(undefined, d.id); - assert.equal(undefined, d.name); - assert.equal(0, d.age); + assert.equal(d.id, undefined); + assert.equal(d.name, undefined); + assert.equal(d.age, 0); M.findOne().select('_id name').exec(function(err, d) { assert.ifError(err); - assert.equal(id, d.id); - assert.equal('ssd', d.name); - assert.equal(undefined, d.age); + assert.equal(d.id, id); + assert.equal(d.name, 'ssd'); + assert.equal(d.age, undefined); cb(); }); }); @@ -495,14 +507,14 @@ describe('schema select option', function() { assert.ok(!d); M.findOne().select('-age -name').exec(function(err, d) { assert.ifError(err); - assert.equal(id, d.id); - assert.equal(undefined, d.name); - assert.equal(undefined, d.age); + assert.equal(d.id, id); + assert.equal(d.name, undefined); + assert.equal(d.age, undefined); M.findOne().select('age name').exec(function(err, d) { assert.ifError(err); - assert.equal(id, d.id); - assert.equal('ssd', d.name); - assert.equal(0, d.age); + assert.equal(d.id, id); + assert.equal(d.name, 'ssd'); + assert.equal(d.age, 0); cb(); }); }); @@ -510,7 +522,7 @@ describe('schema select option', function() { }); } - M.create({ name: 'ssd', age: 0 }, function(err, d) { + M.create({name: 'ssd', age: 0}, function(err, d) { assert.ifError(err); var id = d.id; useId(M, id, function() { @@ -530,10 +542,74 @@ describe('schema select option', function() { }); }); + it('does not set defaults for nested objects (gh-4707)', function(done) { + var db = start(); + var userSchema = new Schema({ + name: String, + age: Number, + profile: { + email: String, + flags: { type: String, default: 'bacon' } + } + }); + + var User = db.model('gh4707', userSchema); + + var obj = { + name: 'Val', + age: 28, + profile: { email: 'test@a.co', flags: 'abc' } + }; + User.create(obj). + then(function(user) { + return User.findById(user._id). + select('name profile.email'); + }). + then(function(user) { + assert.ok(!user.profile.flags); + db.close(done); + }). + catch(done); + }); + + it('does not create nested objects if not included (gh-4669)', function(done) { + var db = start(); + var schema = new Schema({ + field1: String, + field2: String, + field3: { + f1: String, + f2: { type: String, default: 'abc' } + }, + field4: { + f1: String + }, + field5: String + }, { minimize: false }); + + var M = db.model('Test', schema); + var obj = { + field1: 'a', + field2: 'b', + field3: { f1: 'c', f2: 'd' }, + field4: { f1: 'e' }, + field5: 'f' + }; + M.create(obj, function(error, doc) { + assert.ifError(error); + M.findOne({ _id: doc._id }, 'field1 field2', function(error, doc) { + assert.ifError(error); + assert.ok(!doc.toObject({ minimize: false }).field3); + assert.ok(!doc.toObject({ minimize: false }).field4); + db.close(done); + }); + }); + }); + it('initializes nested defaults with selected objects (gh-2629)', function(done) { var NestedSchema = new mongoose.Schema({ nested: { - name: { type: String, default: 'val' } + name: {type: String, default: 'val'} } }); @@ -544,9 +620,9 @@ describe('schema select option', function() { doc.nested.name = undefined; doc.save(function(error) { assert.ifError(error); - Model.findOne({}, { nested: 1 }, function(error, doc) { + Model.findOne({}, {nested: 1}, function(error, doc) { assert.ifError(error); - assert.equal('val', doc.nested.name); + assert.equal(doc.nested.name, 'val'); db.close(done); }); }); diff --git a/test/schema.test.js b/test/schema.test.js index 50db2a42084..a378e2c6543 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -2,18 +2,18 @@ * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , Schema = mongoose.Schema - , Document = mongoose.Document - , VirtualType = mongoose.VirtualType - , SchemaTypes = Schema.Types - , ObjectId = SchemaTypes.ObjectId - , Mixed = SchemaTypes.Mixed - , DocumentObjectId = mongoose.Types.ObjectId - , ReadPref = mongoose.mongo.ReadPreference - , vm = require('vm'); +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema, + Document = mongoose.Document, + VirtualType = mongoose.VirtualType, + SchemaTypes = Schema.Types, + ObjectId = SchemaTypes.ObjectId, + Mixed = SchemaTypes.Mixed, + DocumentObjectId = mongoose.Types.ObjectId, + ReadPref = mongoose.mongo.ReadPreference, + vm = require('vm'); /** * Test Document constructor. @@ -29,19 +29,17 @@ function TestDocument() { TestDocument.prototype.__proto__ = Document.prototype; -/** - * Set a dummy schema to simulate compilation. - */ - -TestDocument.prototype.$__setSchema(new Schema({ - test : String -})); - /** * Test. */ describe('schema', function() { + before(function() { + TestDocument.prototype.$__setSchema(new Schema({ + test: String + })); + }); + describe('nested fields with same name', function() { var db, NestedModel; @@ -55,7 +53,7 @@ describe('schema', function() { } }, b: {$type: String} - }, { typeKey: '$type' }); + }, {typeKey: '$type'}); NestedModel = db.model('Nested', NestedSchema); }); @@ -67,14 +65,15 @@ describe('schema', function() { var n = new NestedModel({ a: { b: { - c:'foo', - d:'bar'} - }, b:'foobar' + c: 'foo', + d: 'bar' + } + }, b: 'foobar' }); n.save(function(err) { assert.ifError(err); - NestedModel.findOne({_id :n._id}, function(err, nm) { + NestedModel.findOne({_id: n._id}, function(err, nm) { assert.ifError(err); // make sure no field has disappeared @@ -93,31 +92,37 @@ describe('schema', function() { it('can be created without the "new" keyword', function(done) { - var schema = Schema({ name: String }); + var schema = new Schema({name: String}); assert.ok(schema instanceof Schema); done(); }); + it('does expose a property for duck-typing instanceof', function(done) { + var schema = new Schema({name: String}); + assert.ok(schema.instanceOfSchema); + done(); + }); + it('supports different schematypes', function(done) { var Checkin = new Schema({ - date : Date - , location : { - lat: Number - , lng: Number + date: Date, + location: { + lat: Number, + lng: Number } }); var Ferret = new Schema({ - name : String - , owner : ObjectId - , fur : String - , color : { type: String } - , age : Number - , checkins : [Checkin] - , friends : [ObjectId] - , likes : Array - , alive : Boolean - , extra : Mixed + name: String, + owner: ObjectId, + fur: String, + color: {type: String}, + age: Number, + checkins: [Checkin], + friends: [ObjectId], + likes: Array, + alive: Boolean, + extra: Mixed }); assert.ok(Ferret.path('name') instanceof SchemaTypes.String); @@ -137,10 +142,10 @@ describe('schema', function() { // check strings var Checkin1 = new Schema({ - date : 'date' - , location : { - lat: 'number' - , lng: 'Number' + date: 'date', + location: { + lat: 'number', + lng: 'Number' } }); @@ -149,20 +154,20 @@ describe('schema', function() { assert.ok(Checkin1.path('location.lng') instanceof SchemaTypes.Number); var Ferret1 = new Schema({ - name : "string" - , owner : "oid" - , fur : { type: "string" } - , color : { type: "String" } - , checkins : [Checkin] - , friends : Array - , likes : "array" - , alive : "Bool" - , alive1 : "bool" - , alive2 : "boolean" - , extra : "mixed" - , obj : "object" - , buf : "buffer" - , Buf : "Buffer" + name: 'string', + owner: 'oid', + fur: {type: 'string'}, + color: {type: 'String'}, + checkins: [Checkin], + friends: Array, + likes: 'array', + alive: 'Bool', + alive1: 'bool', + alive2: 'boolean', + extra: 'mixed', + obj: 'object', + buf: 'buffer', + Buf: 'Buffer' }); assert.ok(Ferret1.path('name') instanceof SchemaTypes.String); @@ -170,7 +175,7 @@ describe('schema', function() { assert.ok(Ferret1.path('fur') instanceof SchemaTypes.String); assert.ok(Ferret1.path('color') instanceof SchemaTypes.String); assert.ok(Ferret1.path('checkins') instanceof SchemaTypes.DocumentArray); - assert.ok( Ferret1.path('friends') instanceof SchemaTypes.Array); + assert.ok(Ferret1.path('friends') instanceof SchemaTypes.Array); assert.ok(Ferret1.path('likes') instanceof SchemaTypes.Array); assert.ok(Ferret1.path('alive') instanceof SchemaTypes.Boolean); assert.ok(Ferret1.path('alive1') instanceof SchemaTypes.Boolean); @@ -184,19 +189,19 @@ describe('schema', function() { it('supports dot notation for path accessors', function(done) { var Racoon = new Schema({ - name : { type: String, enum: ['Edwald', 'Tobi'] } - , age : Number + name: {type: String, enum: ['Edwald', 'Tobi']}, + age: Number }); // check for global variable leak - assert.equal('undefined', typeof errorMessage); + assert.equal(typeof errorMessage, 'undefined'); var Person = new Schema({ - name : String - , raccoons : [Racoon] - , location : { - city : String - , state : String + name: String, + raccoons: [Racoon], + location: { + city: String, + state: String } }); @@ -223,21 +228,27 @@ describe('schema', function() { it('default definition', function(done) { var Test = new Schema({ - simple : { $type: String, default: 'a' } - , array : { $type: Array, default: [1,2,3,4,5] } - , arrayX : { $type: Array, default: 9 } - , arrayFn : { $type: Array, default: function() { return [8]; } } - , callback : { $type: Number, default: function() { - assert.equal('b', this.a); + simple: {$type: String, default: 'a'}, + array: {$type: Array, default: [1, 2, 3, 4, 5]}, + arrayX: {$type: Array, default: 9}, + arrayFn: { + $type: Array, default: function() { + return [8]; + } + }, + callback: { + $type: Number, default: function() { + assert.equal(this.a, 'b'); return '3'; - }} - }, { typeKey: '$type' }); + } + } + }, {typeKey: '$type'}); assert.equal(Test.path('simple').defaultValue, 'a'); assert.equal(typeof Test.path('callback').defaultValue, 'function'); assert.equal(Test.path('simple').getDefault(), 'a'); - assert.equal((+Test.path('callback').getDefault({ a: 'b' })), 3); + assert.equal((+Test.path('callback').getDefault({a: 'b'})), 3); assert.equal(typeof Test.path('array').defaultValue, 'function'); assert.equal(Test.path('array').getDefault(new TestDocument)[3], 4); assert.equal(Test.path('arrayX').getDefault(new TestDocument)[0], 9); @@ -250,8 +261,8 @@ describe('schema', function() { it('Mixed defaults can be empty arrays', function(done) { var Test = new Schema({ - mixed1 : { type: Mixed, default: [] } - , mixed2 : { type: Mixed, default: Array } + mixed1: {type: Mixed, default: []}, + mixed2: {type: Mixed, default: Array} }); assert.ok(Test.path('mixed1').getDefault() instanceof Array); @@ -268,11 +279,11 @@ describe('schema', function() { }); // test String -> Number cast - assert.equal('number', typeof Tobi.path('age').cast('0')); - assert.equal(0, (+Tobi.path('age').cast('0'))); + assert.equal(typeof Tobi.path('age').cast('0'), 'number'); + assert.equal((+Tobi.path('age').cast('0')), 0); - assert.equal('number', typeof Tobi.path('age').cast(0)); - assert.equal(0, (+Tobi.path('age').cast(0))); + assert.equal(typeof Tobi.path('age').cast(0), 'number'); + assert.equal((+Tobi.path('age').cast(0)), 0); done(); }); @@ -282,25 +293,27 @@ describe('schema', function() { nickname: String }); - function Test() {} + function Test() { + } + Test.prototype.toString = function() { return 'woot'; }; // test Number -> String cast - assert.equal('string', typeof Tobi.path('nickname').cast(0)); - assert.equal('0', Tobi.path('nickname').cast(0)); + assert.equal(typeof Tobi.path('nickname').cast(0), 'string'); + assert.equal(Tobi.path('nickname').cast(0), '0'); // test any object that implements toString - assert.equal('string', typeof Tobi.path('nickname').cast(new Test)); - assert.equal('woot', Tobi.path('nickname').cast(new Test)); + assert.equal(typeof Tobi.path('nickname').cast(new Test), 'string'); + assert.equal(Tobi.path('nickname').cast(new Test), 'woot'); done(); }); }); it('date', function(done) { var Loki = new Schema({ - birth_date: { type: Date } + birth_date: {type: Date} }); assert.ok(Loki.path('birth_date').cast(1294525628301) instanceof Date); @@ -313,34 +326,31 @@ describe('schema', function() { it('objectid', function(done) { var Loki = new Schema({ - owner: { type: ObjectId } + owner: {type: ObjectId} }); - var doc = new TestDocument() - , id = doc._id.toString(); + var doc = new TestDocument(), + id = doc._id.toString(); - assert.ok(Loki.path('owner').cast('4c54f3453e688c000000001a') - instanceof DocumentObjectId); + assert.ok(Loki.path('owner').cast('4c54f3453e688c000000001a') instanceof DocumentObjectId); - assert.ok(Loki.path('owner').cast(new DocumentObjectId()) - instanceof DocumentObjectId); + assert.ok(Loki.path('owner').cast(new DocumentObjectId()) instanceof DocumentObjectId); - assert.ok(Loki.path('owner').cast(doc) - instanceof DocumentObjectId); + assert.ok(Loki.path('owner').cast(doc) instanceof DocumentObjectId); - assert.equal(id, Loki.path('owner').cast(doc).toString()); + assert.equal(Loki.path('owner').cast(doc).toString(), id); done(); }); it('array', function(done) { var Loki = new Schema({ - oids : [ObjectId] - , dates : [Date] - , numbers : [Number] - , strings : [String] - , buffers : [Buffer] - , nocast : [] - , mixed : [Mixed] + oids: [ObjectId], + dates: [Date], + numbers: [Number], + strings: [String], + buffers: [Buffer], + nocast: [], + mixed: [Mixed] }); var oids = Loki.path('oids').cast(['4c54f3453e688c000000001a', new DocumentObjectId]); @@ -361,12 +371,12 @@ describe('schema', function() { var strings = Loki.path('strings').cast(['test', 123]); assert.equal(typeof strings[0], 'string'); - assert.equal('test',strings[0]); + assert.equal(strings[0], 'test'); assert.equal(typeof strings[1], 'string'); - assert.equal('123', strings[1]); + assert.equal(strings[1], '123'); - var buffers = Loki.path('buffers').cast(['\0\0\0', new Buffer("abc")]); + var buffers = Loki.path('buffers').cast(['\0\0\0', new Buffer('abc')]); assert.ok(buffers[0] instanceof Buffer); assert.ok(buffers[1] instanceof Buffer); @@ -374,10 +384,10 @@ describe('schema', function() { var nocasts = Loki.path('nocast').cast(['test', 123]); assert.equal(typeof nocasts[0], 'string'); - assert.equal('test', nocasts[0]); + assert.equal(nocasts[0], 'test'); assert.equal(typeof nocasts[1], 'number'); - assert.equal(123, nocasts[1]); + assert.equal(nocasts[1], 123); var mixed = Loki.path('mixed').cast(['test', 123, '123', {}, new Date, new DocumentObjectId]); @@ -387,47 +397,81 @@ describe('schema', function() { assert.equal(typeof mixed[3], 'object'); assert.ok(mixed[4] instanceof Date); assert.ok(mixed[5] instanceof DocumentObjectId); + + done(); + }); + + it('array of arrays', function(done) { + var test = new Schema({ + nums: [[Number]] + }); + var nums = test.path('nums').cast([['1', '2']]); + assert.equal(nums.length, 1); + assert.deepEqual(nums[0].toObject(), [1, 2]); + + nums = test.path('nums').cast(1); + assert.equal(nums.length, 1); + assert.deepEqual(nums[0].toObject(), [1]); + + var threw = false; + try { + test.path('nums').cast([['abcd']]); + } catch (error) { + threw = true; + assert.equal(error.name, 'CastError'); + assert.equal(error.message, + 'Cast to [[number]] failed for value "[["abcd"]]" at path "nums"'); + } + assert.ok(threw); + done(); }); it('boolean', function(done) { var Animal = new Schema({ - isFerret: { type: Boolean, required: true } + isFerret: {type: Boolean, required: true} }); assert.strictEqual(Animal.path('isFerret').cast(null), null); - assert.equal(false, Animal.path('isFerret').cast(undefined)); - assert.equal(false, Animal.path('isFerret').cast(false)); - assert.equal(false, Animal.path('isFerret').cast(0)); - assert.equal(false, Animal.path('isFerret').cast('0')); - assert.equal(false, Animal.path('isFerret').cast('false')); - assert.equal(true, Animal.path('isFerret').cast({})); - assert.equal(true, Animal.path('isFerret').cast(true)); - assert.equal(true, Animal.path('isFerret').cast(1)); - assert.equal(true, Animal.path('isFerret').cast('1')); - assert.equal(true, Animal.path('isFerret').cast('true')); + assert.equal(Animal.path('isFerret').cast(undefined), false); + assert.equal(Animal.path('isFerret').cast(false), false); + assert.equal(Animal.path('isFerret').cast(0), false); + assert.equal(Animal.path('isFerret').cast('0'), false); + assert.equal(Animal.path('isFerret').cast('false'), false); + assert.equal(Animal.path('isFerret').cast({}), true); + assert.equal(Animal.path('isFerret').cast(true), true); + assert.equal(Animal.path('isFerret').cast(1), true); + assert.equal(Animal.path('isFerret').cast('1'), true); + assert.equal(Animal.path('isFerret').cast('true'), true); done(); }); }); it('methods declaration', function(done) { var a = new Schema; - a.method('test', function() {}); + a.method('test', function() { + }); a.method({ - a: function() {} - , b: function() {} + a: function() { + }, + b: function() { + } }); - assert.equal(3, Object.keys(a.methods).length); + assert.equal(Object.keys(a.methods).length, 3); done(); }); it('static declaration', function(done) { var a = new Schema; - a.static('test', function() {}); + a.static('test', function() { + }); a.static({ - a: function() {} - , b: function() {} - , c: function() {} + a: function() { + }, + b: function() { + }, + c: function() { + } }); assert.equal(Object.keys(a.statics).length, 4); @@ -441,61 +485,61 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, set: lowercase } + name: {type: String, set: lowercase} }); - assert.equal('woot', Tobi.path('name').applySetters('WOOT')); - assert.equal(1, Tobi.path('name').setters.length); + assert.equal(Tobi.path('name').applySetters('WOOT'), 'woot'); + assert.equal(Tobi.path('name').setters.length, 1); Tobi.path('name').set(function(v) { return v + 'WOOT'; }); - assert.equal('wootwoot', Tobi.path('name').applySetters('WOOT')); - assert.equal(2, Tobi.path('name').setters.length); + assert.equal(Tobi.path('name').applySetters('WOOT'), 'wootwoot'); + assert.equal(Tobi.path('name').setters.length, 2); done(); }); it('order', function(done) { function extract(v) { return (v && v._id) - ? v._id - : v; + ? v._id + : v; } var Tobi = new Schema({ - name: { type: Schema.ObjectId, set: extract } + name: {type: Schema.ObjectId, set: extract} }); - var id = new DocumentObjectId - , sid = id.toString() - , _id = { _id: id }; + var id = new DocumentObjectId, + sid = id.toString(), + _id = {_id: id}; - assert.equal(Tobi.path('name').applySetters(sid, { a: 'b' }).toString(),sid); - assert.equal(Tobi.path('name').applySetters(_id, { a: 'b' }).toString(),sid); - assert.equal(Tobi.path('name').applySetters(id, { a: 'b' }).toString(),sid); + assert.equal(Tobi.path('name').applySetters(sid, {a: 'b'}).toString(), sid); + assert.equal(Tobi.path('name').applySetters(_id, {a: 'b'}).toString(), sid); + assert.equal(Tobi.path('name').applySetters(id, {a: 'b'}).toString(), sid); done(); }); it('scope', function(done) { function lowercase(v, self) { - assert.equal('b', this.a); - assert.equal('name', self.path); + assert.equal(this.a, 'b'); + assert.equal(self.path, 'name'); return v.toLowerCase(); } var Tobi = new Schema({ - name: { type: String, set: lowercase } + name: {type: String, set: lowercase} }); - assert.equal('what', Tobi.path('name').applySetters('WHAT', { a: 'b' })); + assert.equal(Tobi.path('name').applySetters('WHAT', {a: 'b'}), 'what'); done(); }); it('casting', function(done) { function last(v) { - assert.equal('number', typeof v); - assert.equal(0, v); + assert.equal(typeof v, 'number'); + assert.equal(v, 0); return 'last'; } @@ -504,11 +548,11 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, set: last } + name: {type: String, set: last} }); Tobi.path('name').set(first); - assert.equal('last', Tobi.path('name').applySetters('woot')); + assert.equal(Tobi.path('name').applySetters('woot'), 'last'); done(); }); @@ -528,29 +572,29 @@ describe('schema', function() { describe('string', function() { it('lowercase', function(done) { var Tobi = new Schema({ - name: { type: String, lowercase: true } + name: {type: String, lowercase: true} }); - assert.equal('what', Tobi.path('name').applySetters('WHAT')); - assert.equal('1977', Tobi.path('name').applySetters(1977)); + assert.equal(Tobi.path('name').applySetters('WHAT'), 'what'); + assert.equal(Tobi.path('name').applySetters(1977), '1977'); done(); }); it('uppercase', function(done) { var Tobi = new Schema({ - name: { type: String, uppercase: true } + name: {type: String, uppercase: true} }); - assert.equal('WHAT', Tobi.path('name').applySetters('what')); - assert.equal('1977', Tobi.path('name').applySetters(1977)); + assert.equal(Tobi.path('name').applySetters('what'), 'WHAT'); + assert.equal(Tobi.path('name').applySetters(1977), '1977'); done(); }); it('trim', function(done) { var Tobi = new Schema({ - name: { type: String, uppercase: true, trim: true } + name: {type: String, uppercase: true, trim: true} }); - assert.equal('WHAT', Tobi.path('name').applySetters(' what ')); - assert.equal('1977', Tobi.path('name').applySetters(1977)); + assert.equal(Tobi.path('name').applySetters(' what '), 'WHAT'); + assert.equal(Tobi.path('name').applySetters(1977), '1977'); done(); }); }); @@ -560,12 +604,12 @@ describe('schema', function() { name: String }); - assert.equal('woot', Tobi.path('name').applySetters('woot')); + assert.equal(Tobi.path('name').applySetters('woot'), 'woot'); done(); }); it('assignment of non-functions throw', function(done) { - var schema = new Schema({ fun: String }); + var schema = new Schema({fun: String}); var g; try { @@ -575,7 +619,7 @@ describe('schema', function() { } assert.ok(g); - assert.equal(g.message,'A setter must be a function.'); + assert.equal(g.message, 'A setter must be a function.'); done(); }); }); @@ -587,45 +631,45 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, get: woot } + name: {type: String, get: woot} }); - assert.equal(1, Tobi.path('name').getters.length); - assert.equal('test woot', Tobi.path('name').applyGetters('test')); + assert.equal(Tobi.path('name').getters.length, 1); + assert.equal(Tobi.path('name').applyGetters('test'), 'test woot'); done(); }); it('order', function(done) { function format(v) { return v - ? '$' + v - : v; + ? '$' + v + : v; } var Tobi = new Schema({ - name: { type: Number, get: format } + name: {type: Number, get: format} }); - assert.equal('$30', Tobi.path('name').applyGetters(30, { a: 'b' })); + assert.equal(Tobi.path('name').applyGetters(30, {a: 'b'}), '$30'); done(); }); it('scope', function(done) { function woot(v, self) { - assert.equal('b', this.a); - assert.equal('name', self.path); + assert.equal(this.a, 'b'); + assert.equal(self.path, 'name'); return v.toLowerCase(); } var Tobi = new Schema({ - name: { type: String, get: woot } + name: {type: String, get: woot} }); - assert.equal('yep', Tobi.path('name').applyGetters('YEP', { a: 'b' })); + assert.equal(Tobi.path('name').applyGetters('YEP', {a: 'b'}), 'yep'); done(); }); it('casting', function(done) { function last(v) { - assert.equal('number', typeof v); - assert.equal(0, v); + assert.equal(typeof v, 'number'); + assert.equal(v, 0); return 'last'; } @@ -634,11 +678,11 @@ describe('schema', function() { } var Tobi = new Schema({ - name: { type: String, get: last } + name: {type: String, get: last} }); Tobi.path('name').get(first); - assert.equal('last', Tobi.path('name').applyGetters('woot')); + assert.equal(Tobi.path('name').applyGetters('woot'), 'last'); done(); }); it('applying when none have been defined', function(done) { @@ -646,11 +690,11 @@ describe('schema', function() { name: String }); - assert.equal('woot', Tobi.path('name').applyGetters('woot')); + assert.equal(Tobi.path('name').applyGetters('woot'), 'woot'); done(); }); it('assignment of non-functions throw', function(done) { - var schema = new Schema({ fun: String }); + var schema = new Schema({fun: String}); var g; try { @@ -660,7 +704,7 @@ describe('schema', function() { } assert.ok(g); - assert.equal(g.message,'A getter must be a function.'); + assert.equal(g.message, 'A getter must be a function.'); done(); }); it('auto _id', function(done) { @@ -671,53 +715,24 @@ describe('schema', function() { schema = new Schema({ name: String - }, { _id: true }); + }, {_id: true}); assert.ok(schema.path('_id') instanceof Schema.ObjectId); schema = new Schema({ name: String - }, { _id: false }); - assert.equal(undefined, schema.path('_id')); + }, {_id: false}); + assert.equal(schema.path('_id'), undefined); // old options schema = new Schema({ name: String - }, { noId: false }); + }, {noId: false}); assert.ok(schema.path('_id') instanceof Schema.ObjectId); schema = new Schema({ name: String - }, { noId: true }); - assert.equal(undefined, schema.path('_id')); - done(); - }); - - it('auto id', function(done) { - var schema = new Schema({ - name: String - }); - assert.ok(schema.virtualpath('id') instanceof mongoose.VirtualType); - - schema = new Schema({ - name: String - }, { id: true }); - assert.ok(schema.virtualpath('id') instanceof mongoose.VirtualType); - - schema = new Schema({ - name: String - }, { id: false }); - assert.equal(undefined, schema.virtualpath('id')); - - // old options - schema = new Schema({ - name: String - }, { noVirtualId: false }); - assert.ok(schema.virtualpath('id') instanceof mongoose.VirtualType); - - schema = new Schema({ - name: String - }, { noVirtualId: true }); - assert.equal(undefined, schema.virtualpath('id')); + }, {noId: true}); + assert.equal(schema.path('_id'), undefined); done(); }); }); @@ -726,14 +741,17 @@ describe('schema', function() { it('registration', function(done) { var Tobi = new Schema(); - Tobi.pre('save', function() {}); + Tobi.pre('save', function() { + }); + assert.equal(Tobi.callQueue.length, 2); + + Tobi.post('save', function() { + }); assert.equal(Tobi.callQueue.length, 3); - Tobi.post('save', function() {}); + Tobi.pre('save', function() { + }); assert.equal(Tobi.callQueue.length, 4); - - Tobi.pre('save', function() {}); - assert.equal(Tobi.callQueue.length, 5); done(); }); }); @@ -742,83 +760,89 @@ describe('schema', function() { describe('definition', function() { it('basic', function(done) { var Tobi = new Schema({ - name: { type: String, index: true } + name: {type: String, index: true} }); - assert.equal(true, Tobi.path('name')._index); - Tobi.path('name').index({ unique: true }); - assert.deepEqual(Tobi.path('name')._index, { unique: true }); + assert.equal(Tobi.path('name')._index, true); + Tobi.path('name').index({unique: true}); + assert.deepEqual(Tobi.path('name')._index, {unique: true}); Tobi.path('name').unique(false); - assert.deepEqual(Tobi.path('name')._index, { unique: false }); + assert.deepEqual(Tobi.path('name')._index, {unique: false}); var T, i; T = new Schema({ - name: { type: String, sparse: true } + name: {type: String, sparse: true} }); - assert.deepEqual(T.path('name')._index, { sparse: true }); + assert.deepEqual(T.path('name')._index, {sparse: true}); T = new Schema({ - name: { type: String, unique: true } + name: {type: String, unique: true} }); - assert.deepEqual(T.path('name')._index, { unique: true }); + assert.deepEqual(T.path('name')._index, {unique: true}); T = new Schema({ - name: { type: Date, expires: '1.5m' } + name: {type: Date, expires: '1.5m'} }); - assert.deepEqual(T.path('name')._index, { expireAfterSeconds: 90 }); + assert.deepEqual(T.path('name')._index, {expireAfterSeconds: 90}); T = new Schema({ - name: { type: Date, expires: 200 } + name: {type: Date, expires: 200} }); - assert.deepEqual(T.path('name')._index, { expireAfterSeconds: 200 }); + assert.deepEqual(T.path('name')._index, {expireAfterSeconds: 200}); T = new Schema({ - name: { type: String, sparse: true, unique: true } + name: {type: String, sparse: true, unique: true} }); - assert.deepEqual(T.path('name')._index, { sparse: true, unique: true }); + assert.deepEqual(T.path('name')._index, {sparse: true, unique: true}); T = new Schema({ - name: { type: String, unique: true, sparse: true } + name: {type: String, unique: true, sparse: true} }); i = T.path('name')._index; - assert.equal(true, i.unique); - assert.equal(true, i.sparse); + assert.equal(i.unique, true); + assert.equal(i.sparse, true); T = new Schema({ - name: { type: String, index: { sparse: true, unique: true, expireAfterSeconds: 65 }} + name: {type: String, index: {sparse: true, unique: true, expireAfterSeconds: 65}} }); i = T.path('name')._index; - assert.equal(true, i.unique); - assert.equal(true, i.sparse); - assert.equal(65, i.expireAfterSeconds); + assert.equal(i.unique, true); + assert.equal(i.sparse, true); + assert.equal(i.expireAfterSeconds, 65); T = new Schema({ - name: { type: Date, index: { sparse: true, unique: true, expires: '24h' }} + name: {type: Date, index: {sparse: true, unique: true, expires: '24h'}} }); i = T.path('name')._index; - assert.equal(true, i.unique); - assert.equal(true, i.sparse); - assert.equal(60 * 60 * 24, i.expireAfterSeconds); + assert.equal(i.unique, true); + assert.equal(i.sparse, true); + assert.equal(i.expireAfterSeconds, 60 * 60 * 24); + + T = new Schema({ + name: {type: String, index: false, unique: false} + }); + assert.equal(T.path('name')._index, false); + assert.equal(T.indexes().length, 0); done(); }); it('compound', function(done) { var Tobi = new Schema({ - name: { type: String, index: true } - , last: { type: Number, sparse: true } - , nope: { type: String, index: { background: false }} + name: {type: String, index: true}, + last: {type: Number, sparse: true}, + nope: {type: String, index: {background: false}} }); - Tobi.index({ firstname: 1, last: 1 }, { unique: true, expires: '1h' }); - Tobi.index({ firstname: 1, nope: 1 }, { unique: true, background: false }); + Tobi.index({firstname: 1, last: 1}, {unique: true, expires: '1h'}); + Tobi.index({firstname: 1, nope: 1}, {unique: true, background: false}); assert.deepEqual(Tobi.indexes(), [ - [{ name: 1 }, { background: true }] - , [{ last: 1 }, { sparse: true, background :true }] - , [{ nope: 1 }, { background : false}] - , [{ firstname: 1, last: 1}, {unique: true, expireAfterSeconds: 60 * 60, background: true }] - , [{ firstname: 1, nope: 1 }, { unique: true, background: false }] + [{name: 1}, {background: true}], + [{last: 1}, {sparse: true, background: true}], + [{nope: 1}, {background: false}], + [{firstname: 1, last: 1}, {unique: true, expireAfterSeconds: 60 * 60, background: true}], + [{firstname: 1, nope: 1}, {unique: true, background: false}] ]); done(); @@ -828,15 +852,15 @@ describe('schema', function() { describe('plugins', function() { it('work', function(done) { - var Tobi = new Schema - , called = false; + var Tobi = new Schema, + called = false; Tobi.plugin(function(schema) { assert.equal(schema, Tobi); called = true; }); - assert.equal(true, called); + assert.equal(called, true); done(); }); }); @@ -845,168 +869,168 @@ describe('schema', function() { it('defaults are set', function(done) { var Tobi = new Schema(); - assert.equal('object', typeof Tobi.options); - assert.equal(undefined, Tobi.options.safe); - assert.equal(true, Tobi.options.strict); - assert.equal(false, Tobi.options.capped); - assert.equal('__v', Tobi.options.versionKey); - assert.equal('__t', Tobi.options.discriminatorKey); - assert.equal(null, Tobi.options.shardKey); - assert.equal(null, Tobi.options.read); - assert.equal(true, Tobi.options._id); + assert.equal(typeof Tobi.options, 'object'); + assert.equal(Tobi.options.safe, undefined); + assert.equal(Tobi.options.strict, true); + assert.equal(Tobi.options.capped, false); + assert.equal(Tobi.options.versionKey, '__v'); + assert.equal(Tobi.options.discriminatorKey, '__t'); + assert.equal(Tobi.options.shardKey, null); + assert.equal(Tobi.options.read, null); + assert.equal(Tobi.options._id, true); done(); }); it('setting', function(done) { - var Tobi = new Schema({}, { collection: 'users' }); + var Tobi = new Schema({}, {collection: 'users'}); Tobi.set('a', 'b'); Tobi.set('safe', false); - assert.equal('users', Tobi.options.collection); + assert.equal(Tobi.options.collection, 'users'); - assert.equal('b', Tobi.options.a); - assert.deepEqual(Tobi.options.safe, { w: 0 }); - assert.equal(null, Tobi.options.read); + assert.equal(Tobi.options.a, 'b'); + assert.deepEqual(Tobi.options.safe, {w: 0}); + assert.equal(Tobi.options.read, null); - var tags = [{ x: 1 }]; + var tags = [{x: 1}]; Tobi.set('read', 'n'); assert.ok(Tobi.options.read instanceof ReadPref); - assert.equal('nearest', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'nearest'); Tobi.set('read', 'n', tags); - assert.equal('nearest', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'nearest'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); Tobi.set('read', ['n', tags]); - assert.equal('nearest', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'nearest'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 'p' }); + Tobi = new Schema({}, {read: 'p'}); assert.ok(Tobi.options.read instanceof ReadPref); - assert.equal('primary', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'primary'); - Tobi = Schema({}, { read: ['p', tags] }); + Tobi = new Schema({}, {read: ['p', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); - assert.equal('primary', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'primary'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 'primary' }); + Tobi = new Schema({}, {read: 'primary'}); assert.ok(Tobi.options.read instanceof ReadPref); - assert.equal('primary', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'primary'); - Tobi = Schema({}, { read: ['primary', tags] }); + Tobi = new Schema({}, {read: ['primary', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); - assert.equal('primary', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'primary'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 's' }); + Tobi = new Schema({}, {read: 's'}); assert.ok(Tobi.options.read instanceof ReadPref); - assert.equal('secondary', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'secondary'); - Tobi = Schema({}, { read: ['s', tags] }); + Tobi = new Schema({}, {read: ['s', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); - assert.equal('secondary', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'secondary'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 'secondary' }); + Tobi = new Schema({}, {read: 'secondary'}); assert.ok(Tobi.options.read instanceof ReadPref); - assert.equal('secondary', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'secondary'); - Tobi = Schema({}, { read: ['secondary', tags] }); + Tobi = new Schema({}, {read: ['secondary', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); - assert.equal('secondary', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'secondary'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 'pp' }); + Tobi = new Schema({}, {read: 'pp'}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('primaryPreferred', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'primaryPreferred'); - Tobi = Schema({}, { read: ['pp', tags] }); + Tobi = new Schema({}, {read: ['pp', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('primaryPreferred', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'primaryPreferred'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 'primaryPreferred'}); + Tobi = new Schema({}, {read: 'primaryPreferred'}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('primaryPreferred', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'primaryPreferred'); - Tobi = Schema({}, { read: ['primaryPreferred', tags]}); + Tobi = new Schema({}, {read: ['primaryPreferred', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('primaryPreferred', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'primaryPreferred'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 'sp' }); + Tobi = new Schema({}, {read: 'sp'}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('secondaryPreferred', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'secondaryPreferred'); - Tobi = Schema({}, { read: ['sp', tags] }); + Tobi = new Schema({}, {read: ['sp', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('secondaryPreferred', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'secondaryPreferred'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 'secondaryPreferred'}); + Tobi = new Schema({}, {read: 'secondaryPreferred'}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('secondaryPreferred', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'secondaryPreferred'); - Tobi = Schema({}, { read: ['secondaryPreferred', tags]}); + Tobi = new Schema({}, {read: ['secondaryPreferred', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('secondaryPreferred', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'secondaryPreferred'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 'n'}); + Tobi = new Schema({}, {read: 'n'}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('nearest', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'nearest'); - Tobi = Schema({}, { read: ['n', tags]}); + Tobi = new Schema({}, {read: ['n', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('nearest', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'nearest'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); - Tobi = Schema({}, { read: 'nearest'}); + Tobi = new Schema({}, {read: 'nearest'}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('nearest', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'nearest'); - Tobi = Schema({}, { read: ['nearest', tags]}); + Tobi = new Schema({}, {read: ['nearest', tags]}); assert.ok(Tobi.options.read instanceof ReadPref); assert.ok(Tobi.options.read.isValid()); - assert.equal('nearest', Tobi.options.read.mode); + assert.equal(Tobi.options.read.mode, 'nearest'); assert.ok(Array.isArray(Tobi.options.read.tags)); - assert.equal(1, Tobi.options.read.tags.length); - assert.equal(1, Tobi.options.read.tags[0].x); + assert.equal(Tobi.options.read.tags.length, 1); + assert.equal(Tobi.options.read.tags[0].x, 1); done(); }); @@ -1015,8 +1039,8 @@ describe('schema', function() { describe('virtuals', function() { it('works', function(done) { var Contact = new Schema({ - firstName: String - , lastName: String + firstName: String, + lastName: String }); Contact @@ -1037,12 +1061,12 @@ describe('schema', function() { describe('id', function() { it('default creation of id can be overridden (gh-298)', function(done) { assert.doesNotThrow(function() { - new Schema({ id: String }); + new Schema({id: String}); }); done(); }); it('disabling', function(done) { - var schema = new Schema({ name: String }, { noVirtualId: true }); + var schema = new Schema({name: String}, {noVirtualId: true}); assert.strictEqual(undefined, schema.virtuals.id); done(); }); @@ -1053,12 +1077,12 @@ describe('schema', function() { var Tobi = new Schema; Tobi.virtual('name').get(function(v, self) { - assert.equal('b', this.a); - assert.equal('name', self.path); + assert.equal(this.a, 'b'); + assert.equal(self.path, 'name'); return v.toLowerCase(); }); - assert.equal('yep', Tobi.virtualpath('name').applyGetters('YEP', { a: 'b' })); + assert.equal(Tobi.virtualpath('name').applyGetters('YEP', {a: 'b'}), 'yep'); done(); }); }); @@ -1068,12 +1092,12 @@ describe('schema', function() { var Tobi = new Schema; Tobi.virtual('name').set(function(v, self) { - assert.equal('b', this.a); - assert.equal('name', self.path); + assert.equal(this.a, 'b'); + assert.equal(self.path, 'name'); return v.toLowerCase(); }); - assert.equal('yep', Tobi.virtualpath('name').applySetters('YEP', { a: 'b' })); + assert.equal(Tobi.virtualpath('name').applySetters('YEP', {a: 'b'}), 'yep'); done(); }); }); @@ -1082,17 +1106,17 @@ describe('schema', function() { describe('other contexts', function() { it('work', function(done) { var str = 'code = {' + - ' name: String' + - ', arr1: Array ' + - ', arr2: { type: [] }' + - ', date: Date ' + - ', num: { type: Number }' + - ', bool: Boolean' + - ', nest: { sub: { type: {}, required: true }}' + - '}'; + ' name: String' + + ', arr1: Array ' + + ', arr2: { type: [] }' + + ', date: Date ' + + ', num: { type: Number }' + + ', bool: Boolean' + + ', nest: { sub: { type: {}, required: true }}' + + '}'; var script = vm.createScript(str, 'testSchema.vm'); - var sandbox = { code: null }; + var sandbox = {code: null}; script.runInNewContext(sandbox); var Ferret = new Schema(sandbox.code); @@ -1109,30 +1133,30 @@ describe('schema', function() { describe('#add()', function() { it('does not polute existing paths', function(done) { - var o = { name: String }; + var o = {name: String}; var s = new Schema(o); assert.throws(function() { - s.add({ age: Number }, 'name.'); + s.add({age: Number}, 'name.'); }, /Cannot set nested path/); assert.throws(function() { - s.add({ age: { x: Number }}, 'name.'); + s.add({age: {x: Number}}, 'name.'); }, /Cannot set nested path/); - assert.equal(false, ('age' in o.name)); + assert.equal(('age' in o.name), false); - o = { name: 'string' }; + o = {name: 'string'}; s = new Schema(o); assert.throws(function() { - s.add({ age: Number }, 'name.'); + s.add({age: Number}, 'name.'); }, /Cannot set nested path/); assert.throws(function() { - s.add({ age: { x: Number }}, 'name.'); + s.add({age: {x: Number}}, 'name.'); }, /Cannot set nested path/); - assert.equal('string', o.name); + assert.equal(o.name, 'string'); done(); }); @@ -1159,8 +1183,8 @@ describe('schema', function() { var merged = new Merged({ a: { - foo: 'baz' - , b: { + foo: 'baz', + b: { bar: 'qux' } } @@ -1171,24 +1195,34 @@ describe('schema', function() { Merged.findById(merged.id, function(err, found) { db.close(); assert.ifError(err); - assert.equal(found.a.foo,'baz'); - assert.equal(found.a.b.bar,'qux'); + assert.equal(found.a.foo, 'baz'); + assert.equal(found.a.b.bar, 'qux'); done(); }); }); }); + + it('prefix (gh-1730)', function(done) { + var s = new Schema({}); + + s.add({ n: Number }, 'prefix.'); + + assert.equal(s.pathType('prefix.n'), 'real'); + assert.equal(s.pathType('prefix'), 'nested'); + done(); + }); }); it('debugging msgs', function(done) { var err; try { - new Schema({ name: { first: null } }); + new Schema({name: {first: null}}); } catch (e) { err = e; } - assert.equal(err.message,'Invalid value for schema path `name.first`'); + assert.equal(err.message, 'Invalid value for schema path `name.first`'); try { - new Schema({ age: undefined }); + new Schema({age: undefined}); } catch (e) { err = e; } @@ -1201,13 +1235,13 @@ describe('schema', function() { var goose = new mongoose.Mongoose; var s = new Schema({ arr: [ - { something: { type: String } } + {something: {type: String}} ] }); assert.ok(s.path('arr') instanceof SchemaTypes.DocumentArray); var M = goose.model('objectliteralschema', s); - var m = new M({ arr: [ { something: 'wicked this way comes' }] }); - assert.equal('wicked this way comes', m.arr[0].something); + var m = new M({arr: [{something: 'wicked this way comes'}]}); + assert.equal(m.arr[0].something, 'wicked this way comes'); assert.ok(m.arr[0]._id); done(); }); @@ -1216,58 +1250,58 @@ describe('schema', function() { var goose = new mongoose.Mongoose; var s = new Schema({ arr: [ - { type: { type: String } } + {type: {type: String}} ] }); assert.ok(s.path('arr') instanceof SchemaTypes.DocumentArray); var M = goose.model('objectliteralschema2', s); - var m = new M({ arr: [ { type: 'works' }] }); - assert.equal('works', m.arr[0].type); + var m = new M({arr: [{type: 'works'}]}); + assert.equal(m.arr[0].type, 'works'); assert.ok(m.arr[0]._id); done(); }); it('does not alter original argument (gh-1364)', function(done) { var schema = { - ids: [{ type: Schema.ObjectId, ref: 'something' }] - , a: { type: Array } - , b: Array - , c: [Date] - , d: { type: 'Boolean' } - , e: [{ a: String, b: [{ type: { type: Buffer }, x: Number }] }] + ids: [{type: Schema.ObjectId, ref: 'something'}], + a: {type: Array}, + b: Array, + c: [Date], + d: {type: 'Boolean'}, + e: [{a: String, b: [{type: {type: Buffer}, x: Number}]}] }; new Schema(schema); - assert.equal(6, Object.keys(schema).length); - assert.deepEqual([{ type: Schema.ObjectId, ref: 'something' }], schema.ids); - assert.deepEqual({ type: Array }, schema.a); + assert.equal(Object.keys(schema).length, 6); + assert.deepEqual([{type: Schema.ObjectId, ref: 'something'}], schema.ids); + assert.deepEqual({type: Array}, schema.a); assert.deepEqual(Array, schema.b); assert.deepEqual([Date], schema.c); - assert.deepEqual({ type: 'Boolean' }, schema.d); - assert.deepEqual([{ a: String, b: [{ type: { type: Buffer }, x: Number }] }], schema.e); + assert.deepEqual({type: 'Boolean'}, schema.d); + assert.deepEqual([{a: String, b: [{type: {type: Buffer}, x: Number}]}], schema.e); done(); }); it('properly gets value of plain objects when dealing with refs (gh-1606)', function(done) { var db = start(); - var el = new Schema({ title : String }); + var el = new Schema({title: String}); var so = new Schema({ - title : String, - obj : { type : Schema.Types.ObjectId, ref : 'Element' } + title: String, + obj: {type: Schema.Types.ObjectId, ref: 'Element'} }); var Element = db.model('Element', el); var Some = db.model('Some', so); - var ele = new Element({ title : 'thing' }); + var ele = new Element({title: 'thing'}); ele.save(function(err) { assert.ifError(err); - var s = new Some({ obj : ele.toObject() }); + var s = new Some({obj: ele.toObject()}); s.save(function(err) { assert.ifError(err); - Some.findOne({ _id : s.id }, function(err, ss) { + Some.findOne({_id: s.id}, function(err, ss) { assert.ifError(err); assert.equal(ss.obj, ele.id); db.close(done); @@ -1279,12 +1313,12 @@ describe('schema', function() { describe('property names', function() { it('that conflict throw', function(done) { - var child = new Schema({ name: String }); + var child = new Schema({name: String}); assert.throws(function() { new Schema({ - on: String - , child: [child] + on: String, + child: [child] }); }, /`on` may not be used as a schema pathname/); @@ -1341,23 +1375,23 @@ describe('schema', function() { }); assert.doesNotThrow(function() { - Schema({ child: [{parent: String}] }); + Schema({child: [{parent: String}]}); }); assert.doesNotThrow(function() { - Schema({ child: [{parentArray: String}] }); + Schema({child: [{parentArray: String}]}); }); assert.doesNotThrow(function() { - var s = Schema({ docs: [{ path: String }] }); + var s = new Schema({docs: [{path: String}]}); var M = mongoose.model('gh-1245', s); - new M({ docs: [{ path: 'works' }] }); + new M({docs: [{path: 'works'}]}); }); assert.doesNotThrow(function() { - var s = Schema({ setMaxListeners: String }); + var s = new Schema({setMaxListeners: String}); var M = mongoose.model('setMaxListeners-as-property-name', s); - new M({ setMaxListeners: 'works' }); + new M({setMaxListeners: 'works'}); }); done(); @@ -1365,7 +1399,7 @@ describe('schema', function() { it('permit _scope to be used (gh-1184)', function(done) { var db = start(); - var child = new Schema({ _scope: Schema.ObjectId }); + var child = new Schema({_scope: Schema.ObjectId}); var C = db.model('scope', child); var c = new C; c.save(function(err) { @@ -1386,77 +1420,164 @@ describe('schema', function() { var schema; before(function() { - schema = Schema({ - n: String - , nest: { thing: { nests: Boolean }} - , docs:[{ x: [{ y:String }] }] - , mixed: {} + schema = new Schema({ + n: String, + nest: {thing: {nests: Boolean}}, + docs: [{x: [{y: String}]}], + mixed: {} }); + schema.virtual('myVirtual').get(function() { return 42; }); }); describe('when called on an explicit real path', function() { it('returns "real"', function(done) { - assert.equal('real', schema.pathType('n')); - assert.equal('real', schema.pathType('nest.thing.nests')); - assert.equal('real', schema.pathType('docs')); - assert.equal('real', schema.pathType('docs.0.x')); - assert.equal('real', schema.pathType('docs.0.x.3.y')); - assert.equal('real', schema.pathType('mixed')); + assert.equal(schema.pathType('n'), 'real'); + assert.equal(schema.pathType('nest.thing.nests'), 'real'); + assert.equal(schema.pathType('docs'), 'real'); + assert.equal(schema.pathType('docs.0.x'), 'real'); + assert.equal(schema.pathType('docs.0.x.3.y'), 'real'); + assert.equal(schema.pathType('mixed'), 'real'); done(); }); }); describe('when called on a virtual', function() { it('returns virtual', function(done) { - assert.equal('virtual', schema.pathType('id')); + assert.equal(schema.pathType('myVirtual'), 'virtual'); done(); }); }); describe('when called on nested structure', function() { it('returns nested', function(done) { - assert.equal('nested', schema.pathType('nest')); - assert.equal('nested', schema.pathType('nest.thing')); + assert.equal(schema.pathType('nest'), 'nested'); + assert.equal(schema.pathType('nest.thing'), 'nested'); done(); }); }); describe('when called on undefined path', function() { it('returns adHocOrUndefined', function(done) { - assert.equal('adhocOrUndefined', schema.pathType('mixed.what')); - assert.equal('adhocOrUndefined', schema.pathType('mixed.4')); - assert.equal('adhocOrUndefined', schema.pathType('mixed.4.thing')); - assert.equal('adhocOrUndefined', schema.pathType('mixed.4a.thing')); - assert.equal('adhocOrUndefined', schema.pathType('mixed.4.9.thing')); - assert.equal('adhocOrUndefined', schema.pathType('n.3')); - assert.equal('adhocOrUndefined', schema.pathType('n.3a')); - assert.equal('adhocOrUndefined', schema.pathType('n.3.four')); - assert.equal('adhocOrUndefined', schema.pathType('n.3.4')); - assert.equal('adhocOrUndefined', schema.pathType('n.3.4a')); - assert.equal('adhocOrUndefined', schema.pathType('nest.x')); - assert.equal('adhocOrUndefined', schema.pathType('nest.thing.x')); - assert.equal('adhocOrUndefined', schema.pathType('nest.thing.nests.9')); - assert.equal('adhocOrUndefined', schema.pathType('nest.thing.nests.9a')); - assert.equal('adhocOrUndefined', schema.pathType('nest.thing.nests.a')); + assert.equal(schema.pathType('mixed.what'), 'adhocOrUndefined'); + assert.equal(schema.pathType('mixed.4'), 'adhocOrUndefined'); + assert.equal(schema.pathType('mixed.4.thing'), 'adhocOrUndefined'); + assert.equal(schema.pathType('mixed.4a.thing'), 'adhocOrUndefined'); + assert.equal(schema.pathType('mixed.4.9.thing'), 'adhocOrUndefined'); + assert.equal(schema.pathType('n.3'), 'adhocOrUndefined'); + assert.equal(schema.pathType('n.3a'), 'adhocOrUndefined'); + assert.equal(schema.pathType('n.3.four'), 'adhocOrUndefined'); + assert.equal(schema.pathType('n.3.4'), 'adhocOrUndefined'); + assert.equal(schema.pathType('n.3.4a'), 'adhocOrUndefined'); + assert.equal(schema.pathType('nest.x'), 'adhocOrUndefined'); + assert.equal(schema.pathType('nest.thing.x'), 'adhocOrUndefined'); + assert.equal(schema.pathType('nest.thing.nests.9'), 'adhocOrUndefined'); + assert.equal(schema.pathType('nest.thing.nests.9a'), 'adhocOrUndefined'); + assert.equal(schema.pathType('nest.thing.nests.a'), 'adhocOrUndefined'); done(); }); }); }); it('required() with doc arrays (gh-3199)', function(done) { - var schema = Schema({ - test: [{ x: String }] + var schema = new Schema({ + test: [{x: String}] }); schema.path('test').schema.path('x').required(true); var M = mongoose.model('gh3199', schema); - var m = new M({ test: [{}] }); + var m = new M({test: [{}]}); assert.equal(m.validateSync().errors['test.0.x'].kind, 'required'); done(); }); - describe('remove()', function() { + it('custom typeKey in doc arrays (gh-3560)', function(done) { + var schema = new Schema({ + test: [{ + name: {$type: String} + }] + }, {typeKey: '$type'}); + + schema.path('test').schema.path('name').required(true); + var M = mongoose.model('gh3560', schema); + var m = new M({test: [{name: 'Val'}]}); + + assert.ifError(m.validateSync()); + assert.equal(m.test[0].name, 'Val'); + done(); + }); + + it('required for single nested schemas (gh-3562)', function(done) { + var personSchema = new Schema({ + name: {type: String, required: true} + }); + + var bandSchema = new Schema({ + name: String, + guitarist: {type: personSchema, required: true} + }); + + var Band = mongoose.model('gh3562', bandSchema); + var band = new Band({name: 'Guns N\' Roses'}); + + assert.ok(band.validateSync()); + assert.ok(band.validateSync().errors.guitarist); + band.guitarist = {name: 'Slash'}; + assert.ifError(band.validateSync()); + + done(); + }); + it('booleans cause cast error for date (gh-3935)', function(done) { + var testSchema = new Schema({ + test: Date + }); + + var Test = mongoose.model('gh3935', testSchema); + var test = new Test({ test: true }); + + assert.ok(test.validateSync()); + assert.equal(test.validateSync().errors.test.name, 'CastError'); + + done(); + }); + + it('trim: false works with strings (gh-4042)', function(done) { + var testSchema = new Schema({ + test: { type: String, trim: false } + }); + + var Test = mongoose.model('gh4042', testSchema); + var test = new Test({ test: ' test ' }); + assert.equal(test.test, ' test '); + done(); + }); + + it('arrays with typeKey (gh-4548)', function(done) { + var testSchema = new Schema({ + test: [{ $type: String }] + }, { typeKey: '$type' }); + + assert.equal(testSchema.paths.test.caster.instance, 'String'); + + var Test = mongoose.model('gh4548', testSchema); + var test = new Test({ test: [123] }); + assert.strictEqual(test.test[0], '123'); + done(); + }); + + it('arrays of mixed arrays (gh-5416)', function(done) { + var testSchema = new Schema({ + test: [Array] + }); + + assert.ok(testSchema.paths.test.casterConstructor !== Array); + assert.equal(testSchema.paths.test.casterConstructor, + mongoose.Schema.Types.Array); + + done(); + }); + + describe('remove()', function() { before(function() { - this.schema = Schema({ + this.schema = new Schema({ a: String, b: { c: { @@ -1490,5 +1611,128 @@ describe('schema', function() { assert.strictEqual(this.schema.path('g'), undefined); done(); }); + + it('works properly with virtuals (gh-2398)', function(done) { + this.schema.remove('a'); + this.schema.virtual('a').get(function() { return 42; }); + var Test = mongoose.model('gh2398', this.schema); + var t = new Test(); + assert.equal(t.a, 42); + done(); + }); + + it('methods named toString (gh-4551)', function(done) { + this.schema.methods.toString = function() { + return 'test'; + }; + // should not throw + mongoose.model('gh4551', this.schema); + done(); + }); + + it('handles default value = 0 (gh-4620)', function(done) { + var schema = new Schema({ + tags: { type: [Number], default: 0 } + }); + assert.deepEqual(schema.path('tags').getDefault().toObject(), [0]); + done(); + }); + + it('type: childSchema (gh-5521)', function(done) { + var childSchema = new mongoose.Schema({ + name: String + }, { _id: false }); + + var schema = new mongoose.Schema({ + children: [{ type: childSchema }] + }); + + var Model = mongoose.model('gh5521', schema); + + var doc = new Model({ children: [{ name: 'test' }] }); + assert.deepEqual(doc.toObject().children, [{ name: 'test' }]); + done(); + }); + + it('Decimal128 type (gh-4759)', function(done) { + var Decimal128 = mongoose.Schema.Types.Decimal128; + var schema = new Schema({ + num: Decimal128, + nums: ['Decimal128'] + }); + assert.ok(schema.path('num') instanceof Decimal128); + assert.ok(schema.path('nums').caster instanceof Decimal128); + + var casted = schema.path('num').cast('6.2e+23'); + assert.ok(casted instanceof mongoose.Types.Decimal128); + assert.equal(casted.toString(), '6.2E+23'); + done(); + }); + + it('clone() copies methods, statics, and query helpers (gh-5752)', function(done) { + var schema = new Schema({}); + + schema.methods.fakeMethod = function() { return 'fakeMethod'; }; + schema.statics.fakeStatic = function() { return 'fakeStatic'; }; + schema.query.fakeQueryHelper = function() { return 'fakeQueryHelper'; }; + + var clone = schema.clone(); + assert.equal(clone.methods.fakeMethod, schema.methods.fakeMethod); + assert.equal(clone.statics.fakeStatic, schema.statics.fakeStatic); + assert.equal(clone.query.fakeQueryHelper, schema.query.fakeQueryHelper); + done(); + }); + + it('clone() copies validators declared with validate() (gh-5607)', function(done) { + var schema = new Schema({ + num: Number + }); + + schema.path('num').validate(function(v) { + return v === 42; + }); + + var clone = schema.clone(); + assert.equal(clone.path('num').validators.length, 1); + assert.ok(clone.path('num').validators[0].validator(42)); + assert.ok(!clone.path('num').validators[0].validator(41)); + done(); + }); + + it('TTL index with timestamps (gh-5656)', function(done) { + var testSchema = new mongoose.Schema({ + foo: String, + updatedAt: { + type: Date, + expires: '2h' + } + }, { timestamps: true }); + + var indexes = testSchema.indexes(); + assert.deepEqual(indexes, [ + [{ updatedAt: 1 }, { background: true, expireAfterSeconds: 7200 }] + ]); + done(); + }); + + it('childSchemas prop (gh-5695)', function(done) { + var schema1 = new Schema({ name: String }); + var schema2 = new Schema({ test: String }); + var schema = new Schema({ + arr: [schema1], + single: schema2 + }); + + assert.equal(schema.childSchemas.length, 2); + assert.equal(schema.childSchemas[0].schema, schema1); + assert.equal(schema.childSchemas[1].schema, schema2); + + schema = schema.clone(); + assert.equal(schema.childSchemas.length, 2); + assert.equal(schema.childSchemas[0].schema, schema1); + assert.equal(schema.childSchemas[1].schema, schema2); + + done(); + }); }); }); diff --git a/test/schema.timestamps.test.js b/test/schema.timestamps.test.js index 3107b24b83e..8d7842af84a 100644 --- a/test/schema.timestamps.test.js +++ b/test/schema.timestamps.test.js @@ -3,11 +3,10 @@ * Test dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , Schema = mongoose.Schema - ; +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema; describe('schema options.timestamps', function() { describe('create schema with options.timestamps', function() { @@ -23,6 +22,18 @@ describe('schema options.timestamps', function() { done(); }); + it('should have createdAt and updatedAt fields', function(done) { + var TestSchema = new Schema({ + name: String + }); + + TestSchema.set('timestamps', true); + + assert.ok(TestSchema.path('createdAt')); + assert.ok(TestSchema.path('updatedAt')); + done(); + }); + it('should have created and updatedAt fields', function(done) { var TestSchema = new Schema({ name: String @@ -37,6 +48,20 @@ describe('schema options.timestamps', function() { done(); }); + it('should have created and updatedAt fields', function(done) { + var TestSchema = new Schema({ + name: String + }); + + TestSchema.set('timestamps', { + createdAt: 'created' + }); + + assert.ok(TestSchema.path('created')); + assert.ok(TestSchema.path('updatedAt')); + done(); + }); + it('should have created and updated fields', function(done) { var TestSchema = new Schema({ name: String @@ -51,18 +76,82 @@ describe('schema options.timestamps', function() { assert.ok(TestSchema.path('updated')); done(); }); + + it('should have created and updated fields', function(done) { + var TestSchema = new Schema({ + name: String + }); + + TestSchema.set('timestamps', { + createdAt: 'created', + updatedAt: 'updated' + }); + + assert.ok(TestSchema.path('created')); + assert.ok(TestSchema.path('updated')); + done(); + }); + + it('should not override createdAt when not selected (gh-4340)', function(done) { + var TestSchema = new Schema({ + name: String + }, { + timestamps: true + }); + + var conn = start(); + var Test = conn.model('Test', TestSchema); + + Test.create({ + name: 'hello' + }, function(err, doc) { + // Let’s save the dates to compare later. + var createdAt = doc.createdAt; + var updatedAt = doc.updatedAt; + + assert.ok(doc.createdAt); + + Test.findById(doc._id, { name: true }, function(err, doc) { + // The dates shouldn’t be selected here. + assert.ok(!doc.createdAt); + assert.ok(!doc.updatedAt); + + doc.name = 'world'; + + doc.save(function(err, doc) { + // Let’s save the new updatedAt date as it should have changed. + var newUpdatedAt = doc.updatedAt; + + assert.ok(!doc.createdAt); + assert.ok(doc.updatedAt); + + Test.findById(doc._id, function(err, doc) { + // Let’s make sure that everything is working again by + // comparing the dates with the ones we saved. + assert.equal(doc.createdAt.valueOf(), createdAt.valueOf()); + assert.notEqual(doc.updatedAt.valueOf(), updatedAt.valueOf()); + assert.equal(doc.updatedAt.valueOf(), newUpdatedAt.valueOf()); + + done(); + }); + }); + }); + }); + }); }); describe('auto update createdAt and updatedAt when create/save/update document', function() { - var CatSchema = new Schema({ - name: String, - hobby: String - }, {timestamps: true}); - - var conn = start(); - var Cat = conn.model('Cat', CatSchema); + var CatSchema; + var conn; + var Cat; before(function(done) { + CatSchema = new Schema({ + name: String, + hobby: String + }, {timestamps: true}); + conn = start(); + Cat = conn.model('Cat', CatSchema); Cat.remove({}, done); }); @@ -71,16 +160,16 @@ describe('schema options.timestamps', function() { cat.save(function(err, doc) { assert.ok(doc.createdAt); assert.ok(doc.updatedAt); - assert.ok(doc.createdAt.getTime() == doc.updatedAt.getTime()); + assert.ok(doc.createdAt.getTime() === doc.updatedAt.getTime()); done(); }); }); it('should have fields when create with findOneAndUpdate', function(done) { - Cat.findOneAndUpdate({name: 'notexistname'}, {$set: {}}, {upsert: true, 'new': true}, function(err, doc) { + Cat.findOneAndUpdate({name: 'notexistname'}, {$set: {}}, {upsert: true, new: true}, function(err, doc) { assert.ok(doc.createdAt); assert.ok(doc.updatedAt); - assert.ok(doc.createdAt.getTime() == doc.updatedAt.getTime()); + assert.ok(doc.createdAt.getTime() === doc.updatedAt.getTime()); done(); }); }); @@ -89,6 +178,8 @@ describe('schema options.timestamps', function() { Cat.findOne({name: 'newcat'}, function(err, doc) { var old = doc.updatedAt; + doc.hobby = 'coding'; + doc.save(function(err, doc) { assert.ok(doc.updatedAt.getTime() > old.getTime()); done(); @@ -96,10 +187,21 @@ describe('schema options.timestamps', function() { }); }); + it('should not change updatedAt when save with no modifications', function(done) { + Cat.findOne({name: 'newcat'}, function(err, doc) { + var old = doc.updatedAt; + + doc.save(function(err, doc) { + assert.ok(doc.updatedAt.getTime() === old.getTime()); + done(); + }); + }); + }); + it('should change updatedAt when findOneAndUpdate', function(done) { Cat.findOne({name: 'newcat'}, function(err, doc) { var old = doc.updatedAt; - Cat.findOneAndUpdate({name: 'newcat'}, {$set: {hobby: 'fish'}}, {'new': true}, function(err, doc) { + Cat.findOneAndUpdate({name: 'newcat'}, {$set: {hobby: 'fish'}}, {new: true}, function(err, doc) { assert.ok(doc.updatedAt.getTime() > old.getTime()); done(); }); @@ -118,9 +220,47 @@ describe('schema options.timestamps', function() { }); }); + it('nested docs (gh-4049)', function(done) { + var GroupSchema = new Schema({ + cats: [CatSchema] + }); + + var Group = conn.model('gh4049', GroupSchema); + var now = Date.now(); + Group.create({ cats: [{ name: 'Garfield' }] }, function(error, group) { + assert.ifError(error); + assert.ok(group.cats[0].createdAt); + assert.ok(group.cats[0].createdAt.getTime() >= now); + done(); + }); + }); + + it('nested docs with push (gh-4049)', function(done) { + var GroupSchema = new Schema({ + cats: [CatSchema] + }); + + var Group = conn.model('gh4049_0', GroupSchema); + var now = Date.now(); + Group.create({ cats: [{ name: 'Garfield' }] }, function(error, group) { + assert.ifError(error); + group.cats.push({ name: 'Keanu' }); + group.save(function(error) { + assert.ifError(error); + Group.findById(group._id, function(error, group) { + assert.ifError(error); + assert.ok(group.cats[1].createdAt); + assert.ok(group.cats[1].createdAt.getTime() > now); + done(); + }); + }); + }); + }); + after(function(done) { - Cat.remove({}, done); + Cat.remove({}, function() { + conn.close(done); + }); }); }); - }); diff --git a/test/schema.type.test.js b/test/schema.type.test.js index 36155686ee9..98fd5b6ea68 100644 --- a/test/schema.type.test.js +++ b/test/schema.type.test.js @@ -3,29 +3,29 @@ * Module dependencies. */ -var mongoose = require('./common').mongoose - , assert = require('assert') - , Schema = mongoose.Schema; +var mongoose = require('./common').mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema; describe('schematype', function() { it('honors the selected option', function(done) { - var s = new Schema({ thought: { type: String, select: false }}); - assert.equal(false, s.path('thought').selected); + var s = new Schema({thought: {type: String, select: false}}); + assert.ok(!s.path('thought').selected); - var a = new Schema({ thought: { type: String, select: true }}); - assert.equal(true, a.path('thought').selected); + var a = new Schema({thought: {type: String, select: true}}); + assert.ok(a.path('thought').selected); done(); }); it('properly handles specifying index in combination with unique or sparse', function(done) { - var s = new Schema({ name: { type: String, index: true, unique: true }}); - assert.deepEqual(s.path('name')._index, { unique: true }); - s = new Schema({ name: { type: String, unique: true, index: true }}); - assert.deepEqual(s.path('name')._index, { unique: true }); - s = new Schema({ name: { type: String, index: true, sparse: true }}); - assert.deepEqual(s.path('name')._index, { sparse: true }); - s = new Schema({ name: { type: String, sparse: true, index: true }}); - assert.deepEqual(s.path('name')._index, { sparse: true }); + var s = new Schema({name: {type: String, index: true, unique: true}}); + assert.deepEqual(s.path('name')._index, {unique: true}); + s = new Schema({name: {type: String, unique: true, index: true}}); + assert.deepEqual(s.path('name')._index, {unique: true}); + s = new Schema({name: {type: String, index: true, sparse: true}}); + assert.deepEqual(s.path('name')._index, {sparse: true}); + s = new Schema({name: {type: String, sparse: true, index: true}}); + assert.deepEqual(s.path('name')._index, {sparse: true}); done(); }); }); diff --git a/test/schema.validation.test.js b/test/schema.validation.test.js index 36e19c49511..a017f47f2e6 100644 --- a/test/schema.validation.test.js +++ b/test/schema.validation.test.js @@ -2,41 +2,42 @@ * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , Schema = mongoose.Schema - , ValidatorError = mongoose.Error.ValidatorError - , SchemaTypes = Schema.Types - , ObjectId = SchemaTypes.ObjectId - , Mixed = SchemaTypes.Mixed - , DocumentObjectId = mongoose.Types.ObjectId - , random = require('../lib/utils').random; +var start = require('./common'); +var mongoose = start.mongoose; +var assert = require('power-assert'); +var Schema = mongoose.Schema; +var ValidatorError = mongoose.Error.ValidatorError; +var SchemaTypes = Schema.Types; +var ObjectId = SchemaTypes.ObjectId; +var Mixed = SchemaTypes.Mixed; +var DocumentObjectId = mongoose.Types.ObjectId; +var random = require('../lib/utils').random; +var Promise = require('bluebird'); describe('schema', function() { describe('validation', function() { it('invalid arguments are rejected (1044)', function(done) { assert.throws(function() { new Schema({ - simple: { type: String, validate: 'nope' } + simple: {type: String, validate: 'nope'} }); }, /Invalid validator/); assert.throws(function() { new Schema({ - simple: { type: String, validate: ['nope'] } + simple: {type: String, validate: ['nope']} }); }, /Invalid validator/); assert.throws(function() { new Schema({ - simple: { type: String, validate: { nope: 1, msg: 'nope' } } + simple: {type: String, validate: {nope: 1, msg: 'nope'}} }); }, /Invalid validator/); assert.throws(function() { new Schema({ - simple: { type: String, validate: [{ nope: 1, msg: 'nope' }, 'nope'] } + simple: {type: String, validate: [{nope: 1, msg: 'nope'}, 'nope']} }); }, /Invalid validator/); @@ -45,12 +46,12 @@ describe('schema', function() { it('string enum', function(done) { var Test = new Schema({ - complex: { type: String, enum: ['a', 'b', undefined, 'c', null] }, - state: { type: String } + complex: {type: String, enum: ['a', 'b', undefined, 'c', null]}, + state: {type: String} }); assert.ok(Test.path('complex') instanceof SchemaTypes.String); - assert.deepEqual(Test.path('complex').enumValues,['a', 'b', 'c', null]); + assert.deepEqual(Test.path('complex').enumValues, ['a', 'b', 'c', null]); assert.equal(Test.path('complex').validators.length, 1); Test.path('complex').enum('d', 'e'); @@ -86,7 +87,7 @@ describe('schema', function() { Test.path('state').doValidate('x', function(err) { assert.ok(err instanceof ValidatorError); assert.equal(err.message, - 'enum validator failed for path `state`: test'); + 'enum validator failed for path `state`: test'); }); Test.path('state').doValidate('opening', function(err) { @@ -102,17 +103,17 @@ describe('schema', function() { it('string regexp', function(done) { var Test = new Schema({ - simple: { type: String, match: /[a-z]/ } + simple: {type: String, match: /[a-z]/} }); - assert.equal(1, Test.path('simple').validators.length); + assert.equal(Test.path('simple').validators.length, 1); Test.path('simple').doValidate('az', function(err) { assert.ifError(err); }); Test.path('simple').match(/[0-9]/); - assert.equal(2, Test.path('simple').validators.length); + assert.equal(Test.path('simple').validators.length, 2); Test.path('simple').doValidate('12', function(err) { assert.ok(err instanceof ValidatorError); @@ -152,14 +153,14 @@ describe('schema', function() { before(function() { db = start(); var PersonSchema = new Schema({ - name: { type: String }, + name: {type: String}, num_cars: {type: Number, min: 20} }); Person = db.model('person-schema-validation-test', PersonSchema); }); - after(function() { - db.close(); + after(function(done) { + db.close(done); }); it('and can be set to "undefined" (gh-1594)', function(done) { @@ -188,7 +189,7 @@ describe('schema', function() { it('number min and max', function(done) { var Tobi = new Schema({ - friends: { type: Number, max: 15, min: 5 } + friends: {type: Number, max: 15, min: 5} }); assert.equal(Tobi.path('friends').validators.length, 2); @@ -199,15 +200,15 @@ describe('schema', function() { Tobi.path('friends').doValidate(100, function(err) { assert.ok(err instanceof ValidatorError); - assert.equal('friends', err.path); - assert.equal('max', err.kind); - assert.equal(100, err.value); + assert.equal(err.path, 'friends'); + assert.equal(err.kind, 'max'); + assert.equal(err.value, 100); }); Tobi.path('friends').doValidate(1, function(err) { assert.ok(err instanceof ValidatorError); - assert.equal('friends', err.path); - assert.equal('min', err.kind); + assert.equal(err.path, 'friends'); + assert.equal(err.kind, 'min'); }); // null is allowed @@ -303,7 +304,7 @@ describe('schema', function() { it('number required', function(done) { var Edwald = new Schema({ - friends: { type: Number, required: true } + friends: {type: Number, required: true} }); Edwald.path('friends').doValidate(null, function(err) { @@ -323,7 +324,7 @@ describe('schema', function() { it('date required', function(done) { var Loki = new Schema({ - birth_date: { type: Date, required: true } + birth_date: {type: Date, required: true} }); Loki.path('birth_date').doValidate(null, function(err) { @@ -343,7 +344,7 @@ describe('schema', function() { it('date not empty string (gh-3132)', function(done) { var HappyBirthday = new Schema({ - date: { type: Date, required: true } + date: {type: Date, required: true} }); HappyBirthday.path('date').doValidate('', function(err) { @@ -354,7 +355,7 @@ describe('schema', function() { it('objectid required', function(done) { var Loki = new Schema({ - owner: { type: ObjectId, required: true } + owner: {type: ObjectId, required: true} }); Loki.path('owner').doValidate(new DocumentObjectId(), function(err) { @@ -373,69 +374,83 @@ describe('schema', function() { it('array required', function(done) { var Loki = new Schema({ - likes: { type: Array, required: true } + likes: {type: Array, required: true} }); + var remaining = 3; + Loki.path('likes').doValidate(null, function(err) { assert.ok(err instanceof ValidatorError); + --remaining || done(); }); Loki.path('likes').doValidate(undefined, function(err) { assert.ok(err instanceof ValidatorError); + --remaining || done(); }); Loki.path('likes').doValidate([], function(err) { assert.ok(err instanceof ValidatorError); + --remaining || done(); }); - done(); }); it('boolean required', function(done) { var Animal = new Schema({ - isFerret: { type: Boolean, required: true } + isFerret: {type: Boolean, required: true} }); + var remaining = 4; + Animal.path('isFerret').doValidate(null, function(err) { assert.ok(err instanceof ValidatorError); + --remaining || done(); }); Animal.path('isFerret').doValidate(undefined, function(err) { assert.ok(err instanceof ValidatorError); + --remaining || done(); }); Animal.path('isFerret').doValidate(true, function(err) { assert.ifError(err); + --remaining || done(); }); Animal.path('isFerret').doValidate(false, function(err) { assert.ifError(err); + --remaining || done(); }); - done(); }); it('mixed required', function(done) { var Animal = new Schema({ - characteristics: { type: Mixed, required: true } + characteristics: {type: Mixed, required: true} }); + var remaining = 4; + Animal.path('characteristics').doValidate(null, function(err) { assert.ok(err instanceof ValidatorError); + --remaining || done(); }); Animal.path('characteristics').doValidate(undefined, function(err) { assert.ok(err instanceof ValidatorError); + --remaining || done(); }); Animal.path('characteristics').doValidate({ aggresive: true }, function(err) { assert.ifError(err); + --remaining || done(); }); Animal.path('characteristics').doValidate('none available', function(err) { assert.ifError(err); + --remaining || done(); }); - done(); }); }); @@ -447,12 +462,14 @@ describe('schema', function() { setTimeout(function() { executed++; fn(value === true); - if (2 === executed) done(); + if (executed === 2) { + done(); + } }, 5); } var Animal = new Schema({ - ferret: { type: Boolean, validate: validator } + ferret: {type: Boolean, validate: validator} }); Animal.path('ferret').doValidate(true, function(err) { @@ -471,23 +488,22 @@ describe('schema', function() { setTimeout(function() { executed++; fn(value === true); - if (2 === executed) done(); + if (executed === 2) { + done(); + } }, 5); } var Animal = new Schema({ ferret: { type: Boolean, - validate: [ - { - 'validator': validator, - 'msg': 'validator1' - }, - { - 'validator': validator, - 'msg': 'validator2' - } - ] + validate: [{ + validator: validator, + msg: 'validator1' + }, { + validator: validator, + msg: 'validator2' + }] } }); @@ -497,8 +513,8 @@ describe('schema', function() { }); it('multiple sequence', function(done) { - var validator1Executed = false - ,validator2Executed = false; + var validator1Executed = false, + validator2Executed = false; function validator1(value, fn) { setTimeout(function() { @@ -520,16 +536,13 @@ describe('schema', function() { var Animal = new Schema({ ferret: { type: Boolean, - validate: [ - { - 'validator': validator1, - 'msg': 'validator1' - }, - { - 'validator': validator2, - 'msg': 'validator2' - } - ] + validate: [{ + validator: validator1, + msg: 'validator1' + }, { + validator: validator2, + msg: 'validator2' + }] } }); @@ -540,8 +553,9 @@ describe('schema', function() { it('scope', function(done) { var called = false; + function validator(value, fn) { - assert.equal('b', this.a); + assert.equal(this.a, 'b'); setTimeout(function() { called = true; @@ -550,14 +564,14 @@ describe('schema', function() { } var Animal = new Schema({ - ferret: { type: Boolean, validate: validator } + ferret: {type: Boolean, validate: validator} }); Animal.path('ferret').doValidate(true, function(err) { assert.ifError(err); - assert.equal(true, called); + assert.equal(called, true); done(); - }, { a: 'b' }); + }, {a: 'b'}); }); }); @@ -565,24 +579,24 @@ describe('schema', function() { describe('are customizable', function() { it('within schema definitions', function(done) { var schema = new Schema({ - name: { type: String, enum: ['one', 'two'] } - , myenum: { type: String, enum: { values: ['x'], message: 'enum validator failed for path: {PATH} with {VALUE}' }} - , requiredString1: { type: String, required: true } - , requiredString2: { type: String, required: 'oops, {PATH} is missing. {TYPE}' } - , matchString0: { type: String, match: /bryancranston/ } - , matchString1: { type: String, match: [/bryancranston/, 'invalid string for {PATH} with value: {VALUE}'] } - , numMin0: { type: Number, min: 10 } - , numMin1: { type: Number, min: [10, 'hey, {PATH} is too small']} - , numMax0: { type: Number, max: 20 } - , numMax1: { type: Number, max: [20, 'hey, {PATH} ({VALUE}) is greater than {MAX}'] } + name: {type: String, enum: ['one', 'two']}, + myenum: {type: String, enum: {values: ['x'], message: 'enum validator failed for path: {PATH} with {VALUE}'}}, + requiredString1: {type: String, required: true}, + requiredString2: {type: String, required: 'oops, {PATH} is missing. {TYPE}'}, + matchString0: {type: String, match: /bryancranston/}, + matchString1: {type: String, match: [/bryancranston/, 'invalid string for {PATH} with value: {VALUE}']}, + numMin0: {type: Number, min: 10}, + numMin1: {type: Number, min: [10, 'hey, {PATH} is too small']}, + numMax0: {type: Number, max: 20}, + numMax1: {type: Number, max: [20, 'hey, {PATH} ({VALUE}) is greater than {MAX}']} }); var A = mongoose.model('schema-validation-messages-' + random(), schema); var a = new A; a.validate(function(err) { - assert.equal('Path `requiredString1` is required.', err.errors.requiredString1); - assert.equal('oops, requiredString2 is missing. required', err.errors.requiredString2); + assert.equal(err.errors.requiredString1, 'Path `requiredString1` is required.'); + assert.equal(err.errors.requiredString2, 'oops, requiredString2 is missing. required'); a.requiredString1 = a.requiredString2 = 'hi'; a.name = 'three'; @@ -592,14 +606,14 @@ describe('schema', function() { a.numMax0 = a.numMax1 = 30; a.validate(function(err) { - assert.equal('`three` is not a valid enum value for path `name`.', err.errors.name); - assert.equal('enum validator failed for path: myenum with y', err.errors.myenum); - assert.equal('Path `matchString0` is invalid (no match).', err.errors.matchString0); - assert.equal('invalid string for matchString1 with value: no match', err.errors.matchString1); - assert.equal('Path `numMin0` (2) is less than minimum allowed value (10).', String(err.errors.numMin0)); - assert.equal('hey, numMin1 is too small', String(err.errors.numMin1)); - assert.equal('Path `numMax0` (30) is more than maximum allowed value (20).', err.errors.numMax0); - assert.equal('hey, numMax1 (30) is greater than 20', String(err.errors.numMax1)); + assert.equal(err.errors.name, '`three` is not a valid enum value for path `name`.'); + assert.equal(err.errors.myenum, 'enum validator failed for path: myenum with y'); + assert.equal(err.errors.matchString0, 'Path `matchString0` is invalid (no match).'); + assert.equal(err.errors.matchString1, 'invalid string for matchString1 with value: no match'); + assert.equal(String(err.errors.numMin0), 'Path `numMin0` (2) is less than minimum allowed value (10).'); + assert.equal(String(err.errors.numMin1), 'hey, numMin1 is too small'); + assert.equal(err.errors.numMax0, 'Path `numMax0` (30) is more than maximum allowed value (20).'); + assert.equal(String(err.errors.numMax1), 'hey, numMax1 (30) is greater than 20'); a.name = 'one'; a.myenum = 'x'; @@ -617,14 +631,92 @@ describe('schema', function() { }; var validator = [validate, '{PATH} failed validation ({VALUE})']; - var schema = new Schema({ x: { type: [], validate: validator }}); + var schema = new Schema({x: {type: [], validate: validator}}); var M = mongoose.model('custom-validator-' + random(), schema); - var m = new M({ x: [3,4,5,6] }); + var m = new M({x: [3, 4, 5, 6]}); + + m.validate(function(err) { + assert.equal(String(err.errors.x), 'x failed validation (3,4,5,6)'); + assert.equal(err.errors.x.kind, 'user defined'); + done(); + }); + }); + + it('custom validators with isAsync = false', function(done) { + var validate = function(v, opts) { + // Make eslint not complain about unused vars + return !!(v && opts && false); + }; + + var schema = new Schema({ + x: { + type: String, + validate: { + isAsync: false, + validator: validate + } + } + }); + var M = mongoose.model('custom-validator-async-' + random(), schema); + + var m = new M({x: 'test'}); m.validate(function(err) { - assert.equal('x failed validation (3,4,5,6)', String(err.errors.x)); - assert.equal('user defined', err.errors.x.kind); + assert.ok(err.errors['x']); + done(); + }); + }); + + it('custom validators with isAsync and .validate() (gh-5125)', function(done) { + var validate = function(v, opts) { + // Make eslint not complain about unused vars + return !!(v && opts && false); + }; + + var schema = new Schema({ + x: { + type: String + } + }); + + schema.path('x').validate({ + isAsync: false, + validator: validate, + message: 'Custom error message!' + }); + var M = mongoose.model('gh5125', schema); + + var m = new M({x: 'test'}); + + m.validate(function(err) { + assert.ok(err.errors['x']); + assert.equal(err.errors['x'].message, 'Custom error message!'); + done(); + }); + }); + + it('custom validators with isAsync and promise (gh-5171)', function(done) { + var validate = function(v) { + return Promise.resolve(v === 'test'); + }; + + var schema = new Schema({ + x: { + type: String + } + }); + + schema.path('x').validate({ + isAsync: true, + validator: validate + }); + var M = mongoose.model('gh5171', schema); + + var m = new M({x: 'not test'}); + + m.validate(function(err) { + assert.ok(err.errors['x']); done(); }); }); @@ -634,7 +726,9 @@ describe('schema', function() { x: { type: String, validate: [{ - validator: function() { return false; }, + validator: function() { + return false; + }, msg: 'Error code {ERRORCODE}', errorCode: 25 }] @@ -642,10 +736,10 @@ describe('schema', function() { }); var M = mongoose.model('gh-2132', schema, 'gh-2132'); - var m = new M({ x: 'a' }); + var m = new M({x: 'a'}); m.validate(function(err) { - assert.equal('Error code 25', err.errors.x.toString()); - assert.equal(25, err.errors.x.properties.errorCode); + assert.equal(err.errors.x.toString(), 'Error code 25'); + assert.equal(err.errors.x.properties.errorCode, 25); done(); }); }); @@ -655,16 +749,18 @@ describe('schema', function() { x: { type: String, validate: [{ - validator: function(value, fn) { fn(false, 'Custom message'); }, + validator: function(value, fn) { + fn(false, 'Custom message'); + }, msg: 'Does not matter' }] } }); var M = mongoose.model('gh-1936', schema, 'gh-1936'); - var m = new M({ x: 'whatever' }); + var m = new M({x: 'whatever'}); m.validate(function(err) { - assert.equal('Custom message', err.errors.x.toString()); + assert.equal(err.errors.x.toString(), 'Custom message'); done(); }); }); @@ -677,16 +773,17 @@ describe('schema', function() { function validate() { return false; } + var validator = [validate, '{PATH} failed validation ({VALUE})', 'customType']; - var schema = new Schema({ x: { type: [], validate: validator }}); + var schema = new Schema({x: {type: [], validate: validator}}); var M = mongoose.model('custom-validator-' + random(), schema); - var m = new M({ x: [3,4,5,6] }); + var m = new M({x: [3, 4, 5, 6]}); m.validate(function(err) { - assert.equal('x failed validation (3,4,5,6)', String(err.errors.x)); - assert.equal('customType', err.errors.x.kind); + assert.equal(String(err.errors.x), 'x failed validation (3,4,5,6)'); + assert.equal(err.errors.x.kind, 'customType'); done(); }); }); @@ -695,17 +792,18 @@ describe('schema', function() { function validate() { return false; } + var validator = [ - { validator: validate, msg: '{PATH} failed validation ({VALUE})', type: 'customType'} + {validator: validate, msg: '{PATH} failed validation ({VALUE})', type: 'customType'} ]; - var schema = new Schema({ x: { type: [], validate: validator }}); + var schema = new Schema({x: {type: [], validate: validator}}); var M = mongoose.model('custom-validator-' + random(), schema); - var m = new M({ x: [3,4,5,6] }); + var m = new M({x: [3, 4, 5, 6]}); m.validate(function(err) { - assert.equal('x failed validation (3,4,5,6)', String(err.errors.x)); - assert.equal('customType', err.errors.x.kind); + assert.equal(String(err.errors.x), 'x failed validation (3,4,5,6)'); + assert.equal(err.errors.x.kind, 'customType'); done(); }); }); @@ -713,14 +811,14 @@ describe('schema', function() { }); it('should clear validator errors (gh-2302)', function(done) { - var userSchema = new Schema({ name: { type: String, required: true } }); + var userSchema = new Schema({name: {type: String, required: true}}); var User = mongoose.model('gh-2302', userSchema, 'gh-2302'); var user = new User(); user.validate(function(err) { assert.ok(err); assert.ok(user.errors); - assert.ok(user.errors['name']); + assert.ok(user.errors.name); user.name = 'bacon'; user.validate(function(err) { assert.ok(!err); @@ -733,23 +831,23 @@ describe('schema', function() { it('should allow an array of enums (gh-661)', function(done) { var validBreakfastFoods = ['bacon', 'eggs', 'steak', 'coffee', 'butter']; var breakfastSchema = new Schema({ - foods: [{ type: String, enum: validBreakfastFoods }] + foods: [{type: String, enum: validBreakfastFoods}] }); var Breakfast = mongoose.model('gh-661', breakfastSchema, 'gh-661'); - var goodBreakfast = new Breakfast({ foods: ['eggs', 'bacon'] }); + var goodBreakfast = new Breakfast({foods: ['eggs', 'bacon']}); goodBreakfast.validate(function(error) { assert.ifError(error); - var badBreakfast = new Breakfast({ foods: ['tofu', 'waffles', 'coffee'] }); + var badBreakfast = new Breakfast({foods: ['tofu', 'waffles', 'coffee']}); badBreakfast.validate(function(error) { assert.ok(error); assert.ok(error.errors['foods.0']); assert.equal(error.errors['foods.0'].message, - '`tofu` is not a valid enum value for path `foods`.'); + '`tofu` is not a valid enum value for path `foods`.'); assert.ok(error.errors['foods.1']); assert.equal(error.errors['foods.1'].message, - '`waffles` is not a valid enum value for path `foods`.'); + '`waffles` is not a valid enum value for path `foods`.'); assert.ok(!error.errors['foods.2']); done(); @@ -757,6 +855,75 @@ describe('schema', function() { }); }); + it('should allow an array of subdocuments with enums (gh-3521)', function(done) { + var coolSchema = new Schema({ + votes: [{ + vote: {type: String, enum: ['cool', 'not-cool']} + }] + }); + var Cool = mongoose.model('gh-3521', coolSchema, 'gh-3521'); + + var cool = new Cool(); + cool.votes.push(cool.votes.create({ + vote: 'cool' + })); + cool.validate(function(error) { + assert.ifError(error); + + var terrible = new Cool(); + terrible.votes.push(terrible.votes.create({ + vote: 'terrible' + })); + + terrible.validate(function(error) { + assert.ok(error); + assert.ok(error.errors['votes.0.vote']); + assert.equal(error.errors['votes.0.vote'].message, + '`terrible` is not a valid enum value for path `vote`.'); + + done(); + }); + }); + }); + + it('should validate subdocuments subproperty enums (gh-4111)', function(done) { + var M = mongoose.model('M', new Schema({ + p: { + val: { type: String, enum: ['test'] } + }, + children: [{ + prop: { + val: { type: String, enum: ['valid'] } + } + }] + })); + + var model = new M(); + model.p = { val: 'test' }; + var child = model.children.create(); + child.prop = { + val: 'valid' + }; + + model.children.push(child); + + model.validate(function(error) { + assert.ifError(error); + + child.prop.val = 'invalid'; + + assert.equal(model.children[0].prop.val, 'invalid'); + + model.validate(function(error) { + assert.ok(error); + assert.equal(error.errors['children.0.prop.val'].message, + '`invalid` is not a valid enum value for path `prop.val`.'); + + done(); + }); + }); + }); + it('doesnt do double validation on document arrays (gh-2618)', function(done) { var A = new Schema({str: String}); var B = new Schema({a: [A]}); @@ -769,10 +936,101 @@ describe('schema', function() { B = mongoose.model('b', B); var p = new B(); - p.a.push({ str: 'asdf' }); + p.a.push({str: 'asdf'}); p.validate(function(err) { assert.ifError(err); - assert.equal(1, validateCalls); + assert.equal(validateCalls, 1); + done(); + }); + }); + + it('doesnt do double validation on document arrays underneath nested (gh-5411)', function(done) { + var callScope = []; + + function myValidator() { + callScope.push(this); + } + + var TestSchema = new Schema({ + nest1: { + nest2: { + nestarr: [new Schema({ + value: { + type: Boolean, + required: false, + validate: {validator: myValidator} + } + })] + } + } + }); + + var Test = mongoose.model('gh5411', TestSchema); + var testInstance = new Test({ + nest1: { + nest2: { + nestarr: [{ + value: true + }] + } + } + }); + + testInstance.nest1 = { + nest2: { + nestarr: [{ + value: false + }] + } + }; + + testInstance.validateSync(); + assert.equal(callScope.length, 1); + assert.strictEqual(callScope[0], testInstance.nest1.nest2.nestarr[0]); + done(); + }); + + it('no double validation on set nested docarray (gh-4145)', function(done) { + var calls = 0; + var myValidator = function() { + ++calls; + return true; + }; + + var InnerSchema = new mongoose.Schema({ + myfield: { + type: String, + validate: { + validator: myValidator, + message: 'Message' + } + }, + sibling: String + }); + + var MySchema = new mongoose.Schema({ + nest: { + myarray: [InnerSchema] + }, + rootSibling: String + }); + + var Model = mongoose.model('gh4145', MySchema); + + var instance = new Model({ + rootSibling: 'This is the root sibling' + }); + // Direct object assignment + instance.nest = { + myarray: [{ + myfield: 'This is my field', + sibling: 'This is the nested sibling' + }] + }; + + instance.validate(function(error) { + assert.ifError(error); + assert.equal(calls, 1); done(); }); }); @@ -783,7 +1041,7 @@ describe('schema', function() { }); var Breakfast = mongoose.model('gh-2611', breakfastSchema, 'gh-2611'); - var bad = new Breakfast({ eggs: 'none' }); + var bad = new Breakfast({eggs: 'none'}); bad.validate(function(error) { assert.ok(error); done(); @@ -791,11 +1049,11 @@ describe('schema', function() { }); it('handles multiple subdocument errors (gh-2589)', function(done) { - var foodSchema = new Schema({ name: { type: String, required: true, enum: ['bacon', 'eggs'] } }); - var breakfast = new Schema({ foods: [foodSchema], id: Number }); + var foodSchema = new Schema({name: {type: String, required: true, enum: ['bacon', 'eggs']}}); + var breakfast = new Schema({foods: [foodSchema], id: Number}); var Breakfast = mongoose.model('gh-2589', breakfast, 'gh-2589'); - var bad = new Breakfast({ foods: [{ name: 'tofu' }, { name: 'waffles' }], id: 'Not a number' }); + var bad = new Breakfast({foods: [{name: 'tofu'}, {name: 'waffles'}], id: 'Not a number'}); bad.validate(function(error) { assert.ok(error); assert.deepEqual(['id', 'foods.0.name', 'foods.1.name'], Object.keys(error.errors)); @@ -804,28 +1062,28 @@ describe('schema', function() { }); it('handles subdocument cast errors (gh-2819)', function(done) { - var foodSchema = new Schema({ eggs: { type: Number, required: true } }); - var breakfast = new Schema({ foods: [foodSchema], id: Number }); + var foodSchema = new Schema({eggs: {type: Number, required: true}}); + var breakfast = new Schema({foods: [foodSchema], id: Number}); var Breakfast = mongoose.model('gh-2819', breakfast, 'gh-2819'); // Initially creating subdocs with cast errors - var bad = new Breakfast({ foods: [{ eggs: 'Not a number' }], id: 'Not a number' }); + var bad = new Breakfast({foods: [{eggs: 'Not a number'}], id: 'Not a number'}); bad.validate(function(error) { assert.ok(error); - assert.deepEqual(['id', 'foods.0.eggs'], Object.keys(error.errors)); + assert.deepEqual(['foods.0.eggs', 'id'], Object.keys(error.errors).sort()); assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError); // Pushing docs with cast errors - bad.foods.push({ eggs: 'Also not a number' }); + bad.foods.push({eggs: 'Also not a number'}); bad.validate(function(error) { - assert.deepEqual(['id', 'foods.0.eggs', 'foods.1.eggs'], Object.keys(error.errors)); + assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'id'], Object.keys(error.errors).sort()); assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError); // Splicing docs with cast errors - bad.foods.splice(1, 1, { eggs: 'fail1' }, { eggs: 'fail2' }); + bad.foods.splice(1, 1, {eggs: 'fail1'}, {eggs: 'fail2'}); bad.validate(function(error) { - assert.deepEqual(['id', 'foods.0.eggs', 'foods.1.eggs', 'foods.2.eggs'], Object.keys(error.errors)); + assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'foods.2.eggs', 'id'], Object.keys(error.errors).sort()); assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError); assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError); assert.ok(error.errors['foods.2.eggs'] instanceof mongoose.Error.CastError); @@ -833,14 +1091,14 @@ describe('schema', function() { // Remove the cast error by setting field bad.foods[2].eggs = 3; bad.validate(function(error) { - assert.deepEqual(['id', 'foods.0.eggs', 'foods.1.eggs'], Object.keys(error.errors)); + assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'id'], Object.keys(error.errors).sort()); assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError); assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError); // Remove the cast error using array.set() - bad.foods.set(1, { eggs: 1 }); + bad.foods.set(1, {eggs: 1}); bad.validate(function(error) { - assert.deepEqual(['id', 'foods.0.eggs'], Object.keys(error.errors)); + assert.deepEqual(['foods.0.eggs', 'id'], Object.keys(error.errors).sort()); assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError); done(); @@ -852,50 +1110,50 @@ describe('schema', function() { }); it('fails when you try to set a nested path to a primitive (gh-2592)', function(done) { - var breakfast = new Schema({ foods: { bacon: Number, eggs: Number } }); + var breakfast = new Schema({foods: {bacon: Number, eggs: Number}}); var Breakfast = mongoose.model('gh-2592', breakfast, 'gh-2592'); var bad = new Breakfast(); bad.foods = 'waffles'; bad.validate(function(error) { assert.ok(error); - var errorMessage = 'CastError: Cast to Object failed for value ' + - '"waffles" at path "foods"'; + var errorMessage = 'foods: Cast to Object failed for value ' + + '"waffles" at path "foods"'; assert.ok(error.toString().indexOf(errorMessage) !== -1, error.toString()); done(); }); }); it('doesnt execute other validators if required fails (gh-2725)', function(done) { - var breakfast = new Schema({ description: { type: String, required: true, maxlength: 50 } }); + var breakfast = new Schema({description: {type: String, required: true, maxlength: 50}}); var Breakfast = mongoose.model('gh2725', breakfast, 'gh2725'); var bad = new Breakfast({}); bad.validate(function(error) { assert.ok(error); - var errorMessage = 'ValidationError: Path `description` is required.'; + var errorMessage = 'ValidationError: description: Path `description` is required.'; assert.equal(errorMessage, error.toString()); done(); }); }); it('doesnt execute other validators if required fails (gh-3025)', function(done) { - var breakfast = new Schema({ description: { type: String, required: true, maxlength: 50 } }); + var breakfast = new Schema({description: {type: String, required: true, maxlength: 50}}); var Breakfast = mongoose.model('gh3025', breakfast, 'gh3025'); var bad = new Breakfast({}); var error = bad.validateSync(); assert.ok(error); - var errorMessage = 'ValidationError: Path `description` is required.'; + var errorMessage = 'ValidationError: description: Path `description` is required.'; assert.equal(errorMessage, error.toString()); done(); }); it('validateSync allows you to filter paths (gh-3153)', function(done) { var breakfast = new Schema({ - description: { type: String, required: true, maxlength: 50 }, - other: { type: String, required: true } + description: {type: String, required: true, maxlength: 50}, + other: {type: String, required: true} }); var Breakfast = mongoose.model('gh3153', breakfast, 'gh3153'); @@ -904,19 +1162,19 @@ describe('schema', function() { assert.ok(error); assert.equal(Object.keys(error.errors).length, 1); - assert.ok(error.errors['other']); - assert.ok(!error.errors['description']); + assert.ok(error.errors.other); + assert.ok(!error.errors.description); done(); }); it('adds required validators to the front of the list (gh-2843)', function(done) { - var breakfast = new Schema({ description: { type: String, maxlength: 50, required: true } }); + var breakfast = new Schema({description: {type: String, maxlength: 50, required: true}}); var Breakfast = mongoose.model('gh2843', breakfast, 'gh2843'); var bad = new Breakfast({}); bad.validate(function(error) { assert.ok(error); - var errorMessage = 'ValidationError: Path `description` is required.'; + var errorMessage = 'ValidationError: description: Path `description` is required.'; assert.equal(errorMessage, error.toString()); done(); }); @@ -924,29 +1182,33 @@ describe('schema', function() { it('sets path correctly when setter throws exception (gh-2832)', function(done) { var breakfast = new Schema({ - description: { type: String, set: function() { throw new Error('oops'); } } + description: { + type: String, set: function() { + throw new Error('oops'); + } + } }); var Breakfast = mongoose.model('gh2832', breakfast, 'gh2832'); - Breakfast.create({ description: undefined }, function(error) { + Breakfast.create({description: undefined}, function(error) { assert.ok(error); - var errorMessage = 'ValidationError: CastError: Cast to String failed for value "undefined" at path "description"'; + var errorMessage = 'ValidationError: description: Cast to String failed for value "undefined" at path "description"'; assert.equal(errorMessage, error.toString()); - assert.ok(error.errors['description']); - assert.equal(error.errors['description'].reason.toString(), 'Error: oops'); + assert.ok(error.errors.description); + assert.equal(error.errors.description.reason.toString(), 'Error: oops'); done(); }); }); it('allows you to validate embedded doc that was .create()-ed (gh-2902) (gh-2929)', function(done) { var parentSchema = mongoose.Schema({ - children: [{ name: { type: String, required: true } }] + children: [{name: {type: String, required: true}}] }); var Parent = mongoose.model('gh2902', parentSchema); var p = new Parent(); - var n = p.children.create({ name: '2' }); + var n = p.children.create({name: '2'}); n.validate(function(error) { assert.ifError(error); var bad = p.children.create({}); @@ -960,25 +1222,50 @@ describe('schema', function() { }); it('returns correct kind for user defined custom validators (gh-2885)', function(done) { - var s = mongoose.Schema({ n: { type: String, validate: { validator: function() { return false; } }, msg: 'fail' } }); + var s = mongoose.Schema({ + n: { + type: String, validate: { + validator: function() { + return false; + } + }, msg: 'fail' + } + }); var M = mongoose.model('gh2885', s); - var m = new M({ n: 'test' }); + var m = new M({n: 'test'}); m.validate(function(error) { assert.ok(error); - assert.equal(error.errors['n'].kind, 'user defined'); + assert.equal(error.errors.n.kind, 'user defined'); done(); }); }); it('enums report kind (gh-3009)', function(done) { - var s = mongoose.Schema({ n: { type: String, enum: ['a', 'b'] } }); + var s = mongoose.Schema({n: {type: String, enum: ['a', 'b']}}); var M = mongoose.model('gh3009', s); - var m = new M({ n: 'test' }); + var m = new M({n: 'test'}); m.validate(function(error) { assert.ok(error); - assert.equal(error.errors['n'].kind, 'enum'); + assert.equal(error.errors.n.kind, 'enum'); + done(); + }); + }); + + it('skips conditional required (gh-3539)', function(done) { + var s = mongoose.Schema({ + n: { + type: Number, required: function() { + return false; + }, min: 0 + } + }); + var M = mongoose.model('gh3539', s); + + var m = new M(); + m.validate(function(error) { + assert.ifError(error); done(); }); }); diff --git a/test/services.query.test.js b/test/services.query.test.js new file mode 100644 index 00000000000..89bc5e2a1d7 --- /dev/null +++ b/test/services.query.test.js @@ -0,0 +1,58 @@ +'use strict'; + +var Query = require('../lib/query'); +var Schema = require('../lib/schema'); +var assert = require('assert'); +var selectPopulatedFields = require('../lib/services/query/selectPopulatedFields'); + +describe('Query helpers', function() { + describe('selectPopulatedFields', function() { + it('handles nested populate if parent key is projected in (gh-5669)', function(done) { + var schema = new Schema({ + nested: { + key1: String, + key2: String + } + }); + + var q = new Query({}); + q.schema = schema; + + assert.strictEqual(q._fields, void 0); + + q.select('nested'); + q.populate('nested.key1'); + assert.deepEqual(q._fields, { nested: 1 }); + + selectPopulatedFields(q); + + assert.deepEqual(q._fields, { nested: 1 }); + + done(); + }); + + it('handles nested populate if parent key is projected out (gh-5669)', function(done) { + var schema = new Schema({ + nested: { + key1: String, + key2: String + } + }); + + var q = new Query({}); + q.schema = schema; + + assert.strictEqual(q._fields, void 0); + + q.select('-nested'); + q.populate('nested.key1'); + assert.deepEqual(q._fields, { nested: 0 }); + + selectPopulatedFields(q); + + assert.deepEqual(q._fields, { nested: 0 }); + + done(); + }); + }); +}); diff --git a/test/shard.test.js b/test/shard.test.js index 7e138a623e6..9d11710668c 100644 --- a/test/shard.test.js +++ b/test/shard.test.js @@ -1,33 +1,32 @@ - -var start = require('./common') - , assert = require('assert') - , random = require('../lib/utils').random - , mongoose = start.mongoose - , Schema = mongoose.Schema; +var start = require('./common'), + assert = require('power-assert'), + random = require('../lib/utils').random, + mongoose = start.mongoose, + Schema = mongoose.Schema; var uri = process.env.MONGOOSE_SHARD_TEST_URI; if (!uri) { - console.log('\033[31m', '\n', 'You\'re not testing shards!' - , '\n', 'Please set the MONGOOSE_SHARD_TEST_URI env variable.', '\n' - , 'e.g: `mongodb://localhost:27017/database', '\n' - , 'Sharding must already be enabled on your database' - , '\033[39m'); - - // let expresso shut down this test - exports.r = function expressoHack() {}; + console.log( + '\033[31m', '\n', 'You\'re not testing shards!', + '\n', 'Please set the MONGOOSE_SHARD_TEST_URI env variable.', '\n', + 'e.g: `mongodb://localhost:27017/database', '\n', + 'Sharding must already be enabled on your database', + '\033[39m' + ); + return; } var schema = new Schema({ - name: String - , age: Number - , likes: [String] -}, { shardkey: { name: 1, age: 1 }}); + name: String, + age: Number, + likes: [String] +}, {shardkey: {name: 1, age: 1}}); // to keep mongodb happy when sharding the collection // we add a matching index -schema.index({ name: 1, age: 1 }); +schema.index({name: 1, age: 1}); var collection = 'shardperson_' + random(); mongoose.model('ShardPerson', schema, collection); @@ -37,15 +36,15 @@ var greaterThan20x; var db; describe('shard', function() { before(function(done) { - db = start({ uri: uri }); + db = start({uri: uri}); db.on('error', function(err) { if (/failed to connect/.test(err)) { err.message = 'Shard test error: ' - + err.message - + '\n' - + ' Are you sure there is a db running at ' - + uri + ' ?' - + '\n'; + + err.message + + '\n' + + ' Are you sure there is a db running at ' + + uri + ' ?' + + '\n'; } return done(err); }); @@ -55,33 +54,22 @@ describe('shard', function() { // an existing index on shard key is required before sharding P.on('index', function() { - // enable sharding on our collection var cmd = {}; cmd.shardcollection = db.name + '.' + collection; cmd.key = P.schema.options.shardkey; - P.db.db.executeDbAdminCommand(cmd, function(err, res) { + P.db.db.executeDbAdminCommand(cmd, function(err) { assert.ifError(err); - if (!(res && res.documents && res.documents[0] && res.documents[0].ok)) { - err = new Error('could not shard test collection ' - + collection + '\n' - + res.documents[0].errmsg + '\n' - + 'Make sure to use a different database than what ' - + 'is used for the MULTI_MONGOS_TEST' ); - return done(err); - } - - db.db.admin(function(err, admin) { + db.db.admin().serverStatus(function(err, info) { + db.close(); assert.ifError(err); - admin.serverStatus(function(err, info) { - db.close(); - assert.ifError(err); - version = info.version.split('.').map(function(n) { return parseInt(n, 10); }); - greaterThan20x = 2 < version[0] || 2 == version[0] && 0 < version[0]; - done(); + version = info.version.split('.').map(function(n) { + return parseInt(n, 10); }); + greaterThan20x = version[0] > 2 || version[0] === 2 && version[0] > 0; + done(); }); }); }); @@ -89,25 +77,25 @@ describe('shard', function() { }); it('can read and write to a shard', function(done) { - var db = start({ uri: uri }); + var db = start({uri: uri}); var P = db.model('ShardPerson', collection); - P.create({ name: 'ryu', age: 25, likes: ['street fighting']}, function(err, ryu) { + P.create({name: 'ryu', age: 25, likes: ['street fighting']}, function(err, ryu) { assert.ifError(err); P.findById(ryu._id, function(err, doc) { db.close(); assert.ifError(err); - assert.equal(doc.id,ryu.id); + assert.equal(doc.id, ryu.id); done(); }); }); }); it('save() and remove() works with shard keys transparently', function(done) { - var db = start({ uri: uri }); + var db = start({uri: uri}); var P = db.model('ShardPerson', collection); - var zangief = new P({ name: 'Zangief', age: 33 }); + var zangief = new P({name: 'Zangief', age: 33}); zangief.save(function(err) { assert.ifError(err); @@ -144,12 +132,12 @@ describe('shard', function() { }); it('inserting to a sharded collection without the full shard key fails', function(done) { - var db = start({ uri: uri }); + var db = start({uri: uri}); var P = db.model('ShardPerson', collection); var pending = 6; - P.create({ name: 'ryu', likes: ['street fighting']}, function(err) { + P.create({name: 'ryu', likes: ['street fighting']}, function(err) { assert.ok(err); assert.ok(err.message); if (!--pending) { @@ -158,7 +146,7 @@ describe('shard', function() { } }); - P.create({ likes: ['street fighting']}, function(err) { + P.create({likes: ['street fighting']}, function(err) { assert.ok(err); assert.ok(err.message); if (!--pending) { @@ -167,7 +155,7 @@ describe('shard', function() { } }); - P.create({ name: 'ryu' }, function(err) { + P.create({name: 'ryu'}, function(err) { assert.ok(err); assert.ok(err.message); if (!--pending) { @@ -176,7 +164,7 @@ describe('shard', function() { } }); - P.create({ age: 49 }, function(err) { + P.create({age: 49}, function(err) { assert.ok(err); assert.ok(err.message); if (!--pending) { @@ -185,7 +173,7 @@ describe('shard', function() { } }); - P.create({ likes: ['street fighting'], age: 8 }, function(err) { + P.create({likes: ['street fighting'], age: 8}, function(err) { assert.ok(err); assert.ok(err.message); if (!--pending) { @@ -206,16 +194,16 @@ describe('shard', function() { }); it('updating a sharded collection without the full shard key fails', function(done) { - var db = start({ uri: uri }); + var db = start({uri: uri}); var P = db.model('ShardPerson', collection); - P.create({ name: 'ken', age: 27 }, function(err, ken) { + P.create({name: 'ken', age: 27}, function(err, ken) { assert.ifError(err); - P.update({ name: 'ken' }, { likes: ['kicking', 'punching'] }, function(err) { + P.update({name: 'ken'}, {likes: ['kicking', 'punching']}, function(err) { assert.ok(/shard key/.test(err.message)); - P.update({ _id: ken._id, name: 'ken' }, { likes: ['kicking', 'punching'] }, function(err) { + P.update({_id: ken._id, name: 'ken'}, {likes: ['kicking', 'punching']}, function(err) { // mongo 2.0.x returns: can't do non-multi update with query that doesn't have a valid shard key if (greaterThan20x) { assert.ok(!err, err); @@ -223,7 +211,7 @@ describe('shard', function() { assert.ok(/shard key/.test(err.message)); } - P.update({ _id: ken._id, age: 27 }, { likes: ['kicking', 'punching'] }, function(err) { + P.update({_id: ken._id, age: 27}, {likes: ['kicking', 'punching']}, function(err) { // mongo 2.0.x returns: can't do non-multi update with query that doesn't have a valid shard key if (greaterThan20x) { assert.ok(!err, err); @@ -231,7 +219,7 @@ describe('shard', function() { assert.ok(/shard key/.test(err.message)); } - P.update({ age: 27 }, { likes: ['kicking', 'punching'] }, function(err) { + P.update({age: 27}, {likes: ['kicking', 'punching']}, function(err) { db.close(); assert.ok(err); done(); @@ -243,9 +231,9 @@ describe('shard', function() { }); it('updating shard key values fails', function(done) { - var db = start({ uri: uri }); + var db = start({uri: uri}); var P = db.model('ShardPerson', collection); - P.create({ name: 'chun li', age: 19, likes: ['street fighting']}, function(err, chunli) { + P.create({name: 'chun li', age: 19, likes: ['street fighting']}, function(err, chunli) { assert.ifError(err); assert.equal(chunli.$__.shardval.name, 'chun li'); @@ -276,10 +264,10 @@ describe('shard', function() { }); it('allows null shard key values', function(done) { - var db = start({ uri: uri }); + var db = start({uri: uri}); var P = db.model('ShardPerson', collection); - P.create({ name: null, age: 27 }, function(err, ken) { + P.create({name: null, age: 27}, function(err, ken) { assert.ifError(err); P.findById(ken, function(err) { assert.ifError(err); @@ -289,12 +277,11 @@ describe('shard', function() { }); after(function(done) { - var db = start({ uri: uri }); + var db = start({uri: uri}); var P = db.model('ShardPerson', collection); P.collection.drop(function() { db.close(); done(); }); }); - }); diff --git a/test/timestamps.test.js b/test/timestamps.test.js new file mode 100644 index 00000000000..536f9595dd5 --- /dev/null +++ b/test/timestamps.test.js @@ -0,0 +1,165 @@ +'use strict'; + +var assert = require('assert'); +var start = require('./common'); + +var mongoose = start.mongoose; + +describe('timestamps', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + it('does not override timestamp params defined in schema (gh-4868)', function(done) { + var startTime = Date.now(); + var schema = new mongoose.Schema({ + createdAt: { + type: Date, + select: false + }, + updatedAt: { + type: Date, + select: true + }, + name: String + }, { timestamps: true }); + var M = db.model('gh4868', schema); + + M.create({ name: 'Test' }, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.createdAt); + assert.ok(doc.updatedAt); + assert.ok(doc.updatedAt.valueOf() >= startTime); + done(); + }); + }); + }); + + it('updatedAt without createdAt (gh-5598)', function(done) { + var startTime = Date.now(); + var schema = new mongoose.Schema({ + name: String + }, { timestamps: { createdAt: null, updatedAt: true } }); + var M = db.model('gh5598', schema); + + M.create({ name: 'Test' }, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.createdAt); + assert.ok(doc.updatedAt); + assert.ok(doc.updatedAt.valueOf() >= startTime); + done(); + }); + }); + }); + + it('updatedAt without createdAt for nested (gh-5598)', function(done) { + var startTime = Date.now(); + var schema = new mongoose.Schema({ + name: String + }, { timestamps: { createdAt: null, updatedAt: true } }); + var parentSchema = new mongoose.Schema({ + child: schema + }); + var M = db.model('gh5598_0', parentSchema); + + M.create({ child: { name: 'test' } }, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.child.createdAt); + assert.ok(doc.child.updatedAt); + assert.ok(doc.child.updatedAt.valueOf() >= startTime); + done(); + }); + }); + }); + + it('nested paths (gh-4503)', function(done) { + var startTime = Date.now(); + var schema = new mongoose.Schema({ + name: String + }, { timestamps: { createdAt: 'ts.c', updatedAt: 'ts.a' } }); + var M = db.model('gh4503', schema); + + M.create({ name: 'Test' }, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(doc.ts.c); + assert.ok(doc.ts.c.valueOf() >= startTime); + assert.ok(doc.ts.a); + assert.ok(doc.ts.a.valueOf() >= startTime); + done(); + }); + }); + }); + + it('does not override nested timestamp params defined in schema (gh-4868)', function(done) { + var startTime = Date.now(); + var schema = new mongoose.Schema({ + ts: { + createdAt: { + type: Date, + select: false + }, + updatedAt: { + type: Date, + select: true + } + }, + name: String + }, { timestamps: { createdAt: 'ts.createdAt', updatedAt: 'ts.updatedAt' } }); + var M = db.model('gh4868_0', schema); + + M.create({ name: 'Test' }, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.ts.createdAt); + assert.ok(doc.ts.updatedAt); + assert.ok(doc.ts.updatedAt.valueOf() >= startTime); + done(); + }); + }); + }); + + it('does not override timestamps in nested schema (gh-4868)', function(done) { + var startTime = Date.now(); + var tsSchema = new mongoose.Schema({ + createdAt: { + type: Date, + select: false + }, + updatedAt: { + type: Date, + select: true + } + }); + var schema = new mongoose.Schema({ + ts: tsSchema, + name: String + }, { timestamps: { createdAt: 'ts.createdAt', updatedAt: 'ts.updatedAt' } }); + var M = db.model('gh4868_1', schema); + + M.create({ name: 'Test' }, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.ts.createdAt); + assert.ok(doc.ts.updatedAt); + assert.ok(doc.ts.updatedAt.valueOf() >= startTime); + done(); + }); + }); + }); +}); diff --git a/test/triage/.gitignore b/test/triage/.gitignore deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/triage/_goosetest.js b/test/triage/_goosetest.js deleted file mode 100644 index 6369f364287..00000000000 --- a/test/triage/_goosetest.js +++ /dev/null @@ -1,42 +0,0 @@ - -var mongoose = require('../../'); -var Schema = mongoose.Schema; -var assert = require('assert') - -console.log('\n==========='); -console.log(' mongoose version: %s', mongoose.version); -console.log('========\n\n'); - -var dbname = 'goosetest-{NAME}'; -mongoose.connect('localhost', dbname); -mongoose.connection.on('error', function () { - console.error('connection error', arguments); -}); - - -var schema = new Schema({ - name: String -}); - -var A = mongoose.model('A', schema); - - -mongoose.connection.on('open', function () { - var a = new A({ name: '{NAME}' }); - - a.save(function (err) { - if (err) return done(err); - - A.findById(a, function (err, doc) { - console.log(arguments); - done(err); - }) - }) -}); - -function done (err) { - if (err) console.error(err.stack); - mongoose.connection.db.dropDatabase(function () { - mongoose.connection.close(); - }); -} diff --git a/test/triage/createtest b/test/triage/createtest deleted file mode 100644 index 58cfef17770..00000000000 --- a/test/triage/createtest +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# copy our _goosetest.js to another filename passed on the cmd line -# and replace all occurances of {NAME} with the filename too. -test $# -ne 1 && echo "usage: createtest scriptname" && exit 0; - -# EDITOR must be set -test -e $EDITOR && echo "Please set your EDITOR environment variable" && exit 0; - -# does the file they are about to create exist? -if test -e triage/$1.js; then - # do not overwrite. open for editing. - $EDITOR triage/$1.js -else - # create - cat _goosetest.js | sed "s/{NAME}/$1/g" >> triage/$1.js - $EDITOR triage/$1.js -fi diff --git a/test/types.array.test.js b/test/types.array.test.js index 3eed1259c15..c968aa1af72 100644 --- a/test/types.array.test.js +++ b/test/types.array.test.js @@ -1,48 +1,53 @@ - /** * Module dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = require('./common').mongoose - , Schema = mongoose.Schema - , random = require('../lib/utils').random - , MongooseArray = mongoose.Types.Array - , collection = 'avengers_' + random(); - -var User = new Schema({ - name: String - , pets: [Schema.ObjectId] -}); - -mongoose.model('User', User); - -var Pet = new Schema({ - name: String -}); - -mongoose.model('Pet', Pet); +var start = require('./common'); +var assert = require('power-assert'); +var mongoose = require('./common').mongoose; +var Schema = mongoose.Schema; +var random = require('../lib/utils').random; +var mongodb = require('mongodb'); +var MongooseArray = mongoose.Types.Array; +var collection = 'avengers_' + random(); /** * Test. */ describe('types array', function() { + var User; + var Pet; + + before(function() { + User = new Schema({ + name: String, + pets: [Schema.ObjectId] + }); + + mongoose.model('User', User); + + Pet = new Schema({ + name: String + }); + + mongoose.model('Pet', Pet); + }); + it('behaves and quacks like an Array', function(done) { var a = new MongooseArray; assert.ok(a instanceof Array); assert.ok(a.isMongooseArray); - assert.equal(true, Array.isArray(a)); - assert.deepEqual(Object.keys(a), Object.keys(a.toObject())); + assert.equal(Array.isArray(a), true); + assert.deepEqual(a._atomics.constructor, Object); done(); }); describe('hasAtomics', function() { it('does not throw', function(done) { - var b = new MongooseArray([12,3,4,5]).filter(Boolean); + var b = new MongooseArray([12, 3, 4, 5]).filter(Boolean); var threw = false; try { @@ -53,9 +58,9 @@ describe('types array', function() { assert.ok(!threw); - var a = new MongooseArray([67,8]).filter(Boolean); + var a = new MongooseArray([67, 8]).filter(Boolean); try { - a.push(3,4); + a.push(3, 4); } catch (_) { console.error(_); threw = true; @@ -64,19 +69,18 @@ describe('types array', function() { assert.ok(!threw); done(); }); - }); describe('indexOf()', function() { it('works', function(done) { - var db = start() - , User = db.model('User', 'users_' + random()) - , Pet = db.model('Pet', 'pets' + random()); + var db = start(), + User = db.model('User', 'users_' + random()), + Pet = db.model('Pet', 'pets' + random()); - var tj = new User({ name: 'tj' }) - , tobi = new Pet({ name: 'tobi' }) - , loki = new Pet({ name: 'loki' }) - , jane = new Pet({ name: 'jane' }); + var tj = new User({name: 'tj'}), + tobi = new Pet({name: 'tobi'}), + loki = new Pet({name: 'loki'}), + jane = new Pet({name: 'jane'}); tj.pets.push(tobi); tj.pets.push(loki); @@ -84,33 +88,31 @@ describe('types array', function() { var pending = 3; - [tobi, loki, jane].forEach(function(pet) { - pet.save(function() { - --pending || cb(); - }); - }); - function cb() { Pet.find({}, function(err) { assert.ifError(err); tj.save(function(err) { assert.ifError(err); - User.findOne({ name: 'tj' }, function(err, user) { - db.close(); + User.findOne({name: 'tj'}, function(err, user) { assert.ifError(err); assert.equal(user.pets.length, 3); - assert.equal(user.pets.indexOf(tobi.id),0); - assert.equal(user.pets.indexOf(loki.id),1); - assert.equal(user.pets.indexOf(jane.id),2); - assert.equal(user.pets.indexOf(tobi._id),0); - assert.equal(user.pets.indexOf(loki._id),1); - assert.equal(user.pets.indexOf(jane._id),2); - done(); + assert.equal(user.pets.indexOf(tobi.id), 0); + assert.equal(user.pets.indexOf(loki.id), 1); + assert.equal(user.pets.indexOf(jane.id), 2); + assert.equal(user.pets.indexOf(tobi._id), 0); + assert.equal(user.pets.indexOf(loki._id), 1); + assert.equal(user.pets.indexOf(jane._id), 2); + db.close(done); }); }); }); } + [tobi, loki, jane].forEach(function(pet) { + pet.save(function() { + --pending || cb(); + }); + }); }); }); @@ -119,22 +121,27 @@ describe('types array', function() { function save(doc, cb) { doc.save(function(err) { - if (err) return cb(err); + if (err) { + cb(err); + return; + } doc.constructor.findById(doc._id, cb); }); } before(function(done) { db = start(); - N = db.model('arraySet', Schema({ arr: [Number] })); - S = db.model('arraySetString', Schema({ arr: [String] })); - B = db.model('arraySetBuffer', Schema({ arr: [Buffer] })); - M = db.model('arraySetMixed', Schema({ arr: [] })); - D = db.model('arraySetSubDocs', Schema({ arr: [{ name: String}] })); - ST = db.model('arrayWithSetters', Schema({ arr: [{ - type: String, - lowercase: true - }] })); + N = db.model('arraySet', Schema({arr: [Number]})); + S = db.model('arraySetString', Schema({arr: [String]})); + B = db.model('arraySetBuffer', Schema({arr: [Buffer]})); + M = db.model('arraySetMixed', Schema({arr: []})); + D = db.model('arraySetSubDocs', Schema({arr: [{name: String}]})); + ST = db.model('arrayWithSetters', Schema({ + arr: [{ + type: String, + lowercase: true + }] + })); done(); }); @@ -143,17 +150,17 @@ describe('types array', function() { }); it('works with numbers', function(done) { - var m = new N({ arr: [3,4,5,6] }); + var m = new N({arr: [3, 4, 5, 6]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(4, doc.arr.length); + assert.equal(doc.arr.length, 4); doc.arr.push(8); assert.strictEqual(8, doc.arr[doc.arr.length - 1]); assert.strictEqual(8, doc.arr[4]); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(5, doc.arr.length); + assert.equal(doc.arr.length, 5); assert.strictEqual(3, doc.arr[0]); assert.strictEqual(4, doc.arr[1]); assert.strictEqual(5, doc.arr[2]); @@ -166,17 +173,17 @@ describe('types array', function() { }); it('works with strings', function(done) { - var m = new S({ arr: [3,4,5,6] }); + var m = new S({arr: [3, 4, 5, 6]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(4, doc.arr.length); + assert.equal(doc.arr.length, 4); doc.arr.push(8); assert.strictEqual('8', doc.arr[doc.arr.length - 1]); assert.strictEqual('8', doc.arr[4]); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(5, doc.arr.length); + assert.equal(doc.arr.length, 5); assert.strictEqual('3', doc.arr[0]); assert.strictEqual('4', doc.arr[1]); assert.strictEqual('5', doc.arr[2]); @@ -189,43 +196,43 @@ describe('types array', function() { }); it('works with buffers', function(done) { - var m = new B({ arr: [[0], new Buffer(1)] }); + var m = new B({arr: [[0], new Buffer(1)]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(2, doc.arr.length); + assert.equal(doc.arr.length, 2); assert.ok(doc.arr[0].isMongooseBuffer); assert.ok(doc.arr[1].isMongooseBuffer); - doc.arr.push("nice"); - assert.equal(3, doc.arr.length); + doc.arr.push('nice'); + assert.equal(doc.arr.length, 3); assert.ok(doc.arr[2].isMongooseBuffer); - assert.strictEqual("nice", doc.arr[2].toString('utf8')); + assert.strictEqual('nice', doc.arr[2].toString('utf8')); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(3, doc.arr.length); + assert.equal(doc.arr.length, 3); assert.ok(doc.arr[0].isMongooseBuffer); assert.ok(doc.arr[1].isMongooseBuffer); assert.ok(doc.arr[2].isMongooseBuffer); assert.strictEqual('\u0000', doc.arr[0].toString()); - assert.strictEqual("nice", doc.arr[2].toString()); + assert.strictEqual('nice', doc.arr[2].toString()); done(); }); }); }); it('works with mixed', function(done) { - var m = new M({ arr: [3,{x:1},'yes', [5]] }); + var m = new M({arr: [3, {x: 1}, 'yes', [5]]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(4, doc.arr.length); + assert.equal(doc.arr.length, 4); doc.arr.push(null); - assert.equal(5, doc.arr.length); + assert.equal(doc.arr.length, 5); assert.strictEqual(null, doc.arr[4]); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(5, doc.arr.length); + assert.equal(doc.arr.length, 5); assert.strictEqual(3, doc.arr[0]); assert.strictEqual(1, doc.arr[1].x); assert.strictEqual('yes', doc.arr[2]); @@ -234,17 +241,17 @@ describe('types array', function() { assert.strictEqual(null, doc.arr[4]); doc.arr.push(Infinity); - assert.equal(6, doc.arr.length); + assert.equal(doc.arr.length, 6); assert.strictEqual(Infinity, doc.arr[5]); doc.arr.push(new Buffer(0)); - assert.equal(7, doc.arr.length); + assert.equal(doc.arr.length, 7); assert.strictEqual('', doc.arr[6].toString()); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(7, doc.arr.length); + assert.equal(doc.arr.length, 7); assert.strictEqual(3, doc.arr[0]); assert.strictEqual(1, doc.arr[1].x); assert.strictEqual('yes', doc.arr[2]); @@ -261,22 +268,22 @@ describe('types array', function() { }); it('works with sub-docs', function(done) { - var m = new D({ arr: [{name:'aaron'}, {name:'moombahton '}] }); + var m = new D({arr: [{name: 'aaron'}, {name: 'moombahton '}]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(2, doc.arr.length); - doc.arr.push({name:"Restrepo"}); - assert.equal(3, doc.arr.length); - assert.equal("Restrepo", doc.arr[2].name); + assert.equal(doc.arr.length, 2); + doc.arr.push({name: 'Restrepo'}); + assert.equal(doc.arr.length, 3); + assert.equal(doc.arr[2].name, 'Restrepo'); save(doc, function(err, doc) { assert.ifError(err); // validate - assert.equal(3, doc.arr.length); - assert.equal('aaron', doc.arr[0].name); - assert.equal("moombahton ", doc.arr[1].name); - assert.equal("Restrepo", doc.arr[2].name); + assert.equal(doc.arr.length, 3); + assert.equal(doc.arr[0].name, 'aaron'); + assert.equal(doc.arr[1].name, 'moombahton '); + assert.equal(doc.arr[2].name, 'Restrepo'); done(); }); @@ -284,18 +291,18 @@ describe('types array', function() { }); it('applies setters (gh-3032)', function(done) { - var m = new ST({ arr: ["ONE", "TWO"] }); + var m = new ST({arr: ['ONE', 'TWO']}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(2, doc.arr.length); - doc.arr.push("THREE"); + assert.equal(doc.arr.length, 2); + doc.arr.push('THREE'); assert.strictEqual('one', doc.arr[0]); assert.strictEqual('two', doc.arr[1]); assert.strictEqual('three', doc.arr[2]); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(3, doc.arr.length); + assert.equal(doc.arr.length, 3); assert.strictEqual('one', doc.arr[0]); assert.strictEqual('two', doc.arr[1]); assert.strictEqual('three', doc.arr[2]); @@ -307,29 +314,37 @@ describe('types array', function() { }); describe('splice()', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + it('works', function(done) { var collection = 'splicetest-number' + random(); - var db = start() - , schema = new Schema({ numbers: [Number] }) - , A = db.model('splicetestNumber', schema, collection); + var schema = new Schema({numbers: [Number]}), + A = db.model('splicetestNumber', schema, collection); - var a = new A({ numbers: [4,5,6,7] }); + var a = new A({numbers: [4, 5, 6, 7]}); a.save(function(err) { assert.ifError(err); A.findById(a._id, function(err, doc) { assert.ifError(err); - var removed = doc.numbers.splice(1, 1, "10"); + var removed = doc.numbers.splice(1, 1, '10'); assert.deepEqual(removed, [5]); - assert.equal('number', typeof doc.numbers[1]); - assert.deepEqual(doc.numbers.toObject(),[4,10,6,7]); + assert.equal(typeof doc.numbers[1], 'number'); + assert.deepEqual(doc.numbers.toObject(), [4, 10, 6, 7]); doc.save(function(err) { assert.ifError(err); A.findById(a._id, function(err, doc) { assert.ifError(err); - assert.deepEqual(doc.numbers.toObject(), [4,10,6,7]); + assert.deepEqual(doc.numbers.toObject(), [4, 10, 6, 7]); A.collection.drop(function(err) { - db.close(); assert.ifError(err); done(); }); @@ -341,11 +356,10 @@ describe('types array', function() { it('on embedded docs', function(done) { var collection = 'splicetest-embeddeddocs' + random(); - var db = start() - , schema = new Schema({ types: [new Schema({ type: String }) ]}) - , A = db.model('splicetestEmbeddedDoc', schema, collection); + var schema = new Schema({types: [new Schema({type: String})]}), + A = db.model('splicetestEmbeddedDoc', schema, collection); - var a = new A({ types: [{type:'bird'},{type:'boy'},{type:'frog'},{type:'cloud'}] }); + var a = new A({types: [{type: 'bird'}, {type: 'boy'}, {type: 'frog'}, {type: 'cloud'}]}); a.save(function(err) { assert.ifError(err); A.findById(a._id, function(err, doc) { @@ -354,22 +368,21 @@ describe('types array', function() { doc.types.$pop(); var removed = doc.types.splice(1, 1); - assert.equal(removed.length,1); - assert.equal(removed[0].type,'boy'); + assert.equal(removed.length, 1); + assert.equal(removed[0].type, 'boy'); var obj = doc.types.toObject(); - assert.equal(obj[0].type,'bird'); - assert.equal(obj[1].type,'frog'); + assert.equal(obj[0].type, 'bird'); + assert.equal(obj[1].type, 'frog'); doc.save(function(err) { assert.ifError(err); A.findById(a._id, function(err, doc) { - db.close(); assert.ifError(err); var obj = doc.types.toObject(); - assert.equal(obj[0].type,'bird'); - assert.equal(obj[1].type,'frog'); + assert.equal(obj[0].type, 'bird'); + assert.equal(obj[1].type, 'frog'); done(); }); }); @@ -379,19 +392,28 @@ describe('types array', function() { }); describe('unshift()', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + it('works', function(done) { - var db = start() - , schema = new Schema({ - types: [new Schema({ type: String })] - , nums: [Number] - , strs: [String] - }) - , A = db.model('unshift', schema, 'unshift' + random()); + var schema = new Schema({ + types: [new Schema({type: String})], + nums: [Number], + strs: [String] + }), + A = db.model('unshift', schema, 'unshift' + random()); var a = new A({ - types: [{type:'bird'},{type:'boy'},{type:'frog'},{type:'cloud'}] - , nums: [1,2,3] - , strs: 'one two three'.split(' ') + types: [{type: 'bird'}, {type: 'boy'}, {type: 'frog'}, {type: 'cloud'}], + nums: [1, 2, 3], + strs: 'one two three'.split(' ') }); a.save(function(err) { @@ -399,60 +421,59 @@ describe('types array', function() { A.findById(a._id, function(err, doc) { assert.ifError(err); - var tlen = doc.types.unshift({type:'tree'}); + var tlen = doc.types.unshift({type: 'tree'}); var nlen = doc.nums.unshift(0); var slen = doc.strs.unshift('zero'); - assert.equal(tlen,5); - assert.equal(nlen,4); - assert.equal(slen,4); + assert.equal(tlen, 5); + assert.equal(nlen, 4); + assert.equal(slen, 4); - doc.types.push({type:'worm'}); + doc.types.push({type: 'worm'}); var obj = doc.types.toObject(); - assert.equal(obj[0].type,'tree'); - assert.equal(obj[1].type,'bird'); - assert.equal(obj[2].type,'boy'); - assert.equal(obj[3].type,'frog'); - assert.equal(obj[4].type,'cloud'); - assert.equal(obj[5].type,'worm'); + assert.equal(obj[0].type, 'tree'); + assert.equal(obj[1].type, 'bird'); + assert.equal(obj[2].type, 'boy'); + assert.equal(obj[3].type, 'frog'); + assert.equal(obj[4].type, 'cloud'); + assert.equal(obj[5].type, 'worm'); obj = doc.nums.toObject(); - assert.equal(obj[0].valueOf(),0); - assert.equal(obj[1].valueOf(),1); - assert.equal(obj[2].valueOf(),2); - assert.equal(obj[3].valueOf(),3); + assert.equal(obj[0].valueOf(), 0); + assert.equal(obj[1].valueOf(), 1); + assert.equal(obj[2].valueOf(), 2); + assert.equal(obj[3].valueOf(), 3); obj = doc.strs.toObject(); - assert.equal(obj[0],'zero'); - assert.equal(obj[1],'one'); - assert.equal(obj[2],'two'); - assert.equal(obj[3],'three'); + assert.equal(obj[0], 'zero'); + assert.equal(obj[1], 'one'); + assert.equal(obj[2], 'two'); + assert.equal(obj[3], 'three'); doc.save(function(err) { assert.ifError(err); A.findById(a._id, function(err, doc) { - db.close(); assert.ifError(err); var obj = doc.types.toObject(); - assert.equal(obj[0].type,'tree'); - assert.equal(obj[1].type,'bird'); - assert.equal(obj[2].type,'boy'); - assert.equal(obj[3].type,'frog'); - assert.equal(obj[4].type,'cloud'); - assert.equal(obj[5].type,'worm'); + assert.equal(obj[0].type, 'tree'); + assert.equal(obj[1].type, 'bird'); + assert.equal(obj[2].type, 'boy'); + assert.equal(obj[3].type, 'frog'); + assert.equal(obj[4].type, 'cloud'); + assert.equal(obj[5].type, 'worm'); obj = doc.nums.toObject(); - assert.equal(obj[0].valueOf(),0); - assert.equal(obj[1].valueOf(),1); - assert.equal(obj[2].valueOf(),2); - assert.equal(obj[3].valueOf(),3); + assert.equal(obj[0].valueOf(), 0); + assert.equal(obj[1].valueOf(), 1); + assert.equal(obj[2].valueOf(), 2); + assert.equal(obj[3].valueOf(), 3); obj = doc.strs.toObject(); - assert.equal(obj[0],'zero'); - assert.equal(obj[1],'one'); - assert.equal(obj[2],'two'); - assert.equal(obj[3],'three'); + assert.equal(obj[0], 'zero'); + assert.equal(obj[1], 'one'); + assert.equal(obj[2], 'two'); + assert.equal(obj[3], 'three'); done(); }); }); @@ -461,48 +482,57 @@ describe('types array', function() { }); it('applies setters (gh-3032)', function(done) { - var db = start(); - var ST = db.model('setterArray', Schema({ arr: [{ - type: String, - lowercase: true - }] })); - var m = new ST({ arr: ["ONE", "TWO"] }); + var ST = db.model('setterArray', Schema({ + arr: [{ + type: String, + lowercase: true + }] + })); + var m = new ST({arr: ['ONE', 'TWO']}); m.save(function(err, doc) { assert.ifError(err); - assert.equal(2, doc.arr.length); - doc.arr.unshift("THREE"); + assert.equal(doc.arr.length, 2); + doc.arr.unshift('THREE'); assert.strictEqual('three', doc.arr[0]); assert.strictEqual('one', doc.arr[1]); assert.strictEqual('two', doc.arr[2]); doc.save(function(err, doc) { assert.ifError(err); - assert.equal(3, doc.arr.length); + assert.equal(doc.arr.length, 3); assert.strictEqual('three', doc.arr[0]); assert.strictEqual('one', doc.arr[1]); assert.strictEqual('two', doc.arr[2]); - db.close(done); + done(); }); }); }); }); describe('shift()', function() { + var db; + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + it('works', function(done) { - var db = start() - , schema = new Schema({ - types: [new Schema({ type: String })] - , nums: [Number] - , strs: [String] - }); + var schema = new Schema({ + types: [new Schema({type: String})], + nums: [Number], + strs: [String] + }); var A = db.model('shift', schema, 'unshift' + random()); var a = new A({ - types: [{type:'bird'},{type:'boy'},{type:'frog'},{type:'cloud'}] - , nums: [1,2,3] - , strs: 'one two three'.split(' ') + types: [{type: 'bird'}, {type: 'boy'}, {type: 'frog'}, {type: 'cloud'}], + nums: [1, 2, 3], + strs: 'one two three'.split(' ') }); a.save(function(err) { @@ -514,24 +544,24 @@ describe('types array', function() { var n = doc.nums.shift(); var s = doc.strs.shift(); - assert.equal(t.type,'bird'); - assert.equal(n,1); - assert.equal(s,'one'); + assert.equal(t.type, 'bird'); + assert.equal(n, 1); + assert.equal(s, 'one'); var obj = doc.types.toObject(); - assert.equal(obj[0].type,'boy'); - assert.equal(obj[1].type,'frog'); - assert.equal(obj[2].type,'cloud'); + assert.equal(obj[0].type, 'boy'); + assert.equal(obj[1].type, 'frog'); + assert.equal(obj[2].type, 'cloud'); doc.nums.push(4); obj = doc.nums.toObject(); - assert.equal(2, obj[0].valueOf()); - assert.equal(obj[1].valueOf(),3); - assert.equal(obj[2].valueOf(),4); + assert.equal(obj[0].valueOf(), 2); + assert.equal(obj[1].valueOf(), 3); + assert.equal(obj[2].valueOf(), 4); obj = doc.strs.toObject(); - assert.equal(obj[0],'two'); - assert.equal(obj[1],'three'); + assert.equal(obj[0], 'two'); + assert.equal(obj[1], 'three'); doc.save(function(err) { assert.ifError(err); @@ -540,18 +570,18 @@ describe('types array', function() { assert.ifError(err); var obj = doc.types.toObject(); - assert.equal(obj[0].type,'boy'); - assert.equal(obj[1].type,'frog'); - assert.equal(obj[2].type,'cloud'); + assert.equal(obj[0].type, 'boy'); + assert.equal(obj[1].type, 'frog'); + assert.equal(obj[2].type, 'cloud'); obj = doc.nums.toObject(); - assert.equal(obj[0].valueOf(),2); - assert.equal(obj[1].valueOf(),3); - assert.equal(obj[2].valueOf(),4); + assert.equal(obj[0].valueOf(), 2); + assert.equal(obj[1].valueOf(), 3); + assert.equal(obj[2].valueOf(), 4); obj = doc.strs.toObject(); - assert.equal(obj[0],'two'); - assert.equal(obj[1],'three'); + assert.equal(obj[0], 'two'); + assert.equal(obj[1], 'three'); done(); }); }); @@ -564,34 +594,34 @@ describe('types array', function() { it('works', function(done) { // atomic shift uses $pop -1 var db = start(); - var painting = new Schema({ colors: [] }); + var painting = new Schema({colors: []}); var Painting = db.model('Painting', painting); - var p = new Painting({ colors : ['blue', 'green', 'yellow'] }); + var p = new Painting({colors: ['blue', 'green', 'yellow']}); p.save(function(err) { assert.ifError(err); Painting.findById(p, function(err, doc) { assert.ifError(err); - assert.equal(3, doc.colors.length); + assert.equal(doc.colors.length, 3); var color = doc.colors.$shift(); - assert.equal(2, doc.colors.length); + assert.equal(doc.colors.length, 2); assert.equal(color, 'blue'); // MongoDB pop command can only be called once per save, each // time only removing one element. color = doc.colors.$shift(); assert.equal(color, undefined); - assert.equal(2, doc.colors.length); + assert.equal(doc.colors.length, 2); doc.save(function(err) { - assert.equal(null, err); + assert.equal(err, null); var color = doc.colors.$shift(); - assert.equal(1, doc.colors.length); + assert.equal(doc.colors.length, 1); assert.equal(color, 'green'); doc.save(function(err) { - assert.equal(null, err); + assert.equal(err, null); Painting.findById(doc, function(err, doc) { db.close(); assert.ifError(err); - assert.equal(1, doc.colors.length); + assert.equal(doc.colors.length, 1); assert.equal(doc.colors[0], 'yellow'); done(); }); @@ -604,19 +634,19 @@ describe('types array', function() { describe('pop()', function() { it('works', function(done) { - var db = start() - , schema = new Schema({ - types: [new Schema({ type: String })] - , nums: [Number] - , strs: [String] - }); + var db = start(), + schema = new Schema({ + types: [new Schema({type: String})], + nums: [Number], + strs: [String] + }); var A = db.model('pop', schema, 'pop' + random()); var a = new A({ - types: [{type:'bird'},{type:'boy'},{type:'frog'},{type:'cloud'}] - , nums: [1,2,3] - , strs: 'one two three'.split(' ') + types: [{type: 'bird'}, {type: 'boy'}, {type: 'frog'}, {type: 'cloud'}], + nums: [1, 2, 3], + strs: 'one two three'.split(' ') }); a.save(function(err) { @@ -628,24 +658,24 @@ describe('types array', function() { var n = doc.nums.pop(); var s = doc.strs.pop(); - assert.equal(t.type,'cloud'); - assert.equal(n,3); - assert.equal(s,'three'); + assert.equal(t.type, 'cloud'); + assert.equal(n, 3); + assert.equal(s, 'three'); var obj = doc.types.toObject(); - assert.equal(obj[0].type,'bird'); - assert.equal(obj[1].type,'boy'); - assert.equal(obj[2].type,'frog'); + assert.equal(obj[0].type, 'bird'); + assert.equal(obj[1].type, 'boy'); + assert.equal(obj[2].type, 'frog'); doc.nums.push(4); obj = doc.nums.toObject(); - assert.equal(obj[0].valueOf(),1); - assert.equal(obj[1].valueOf(),2); - assert.equal(obj[2].valueOf(),4); + assert.equal(obj[0].valueOf(), 1); + assert.equal(obj[1].valueOf(), 2); + assert.equal(obj[2].valueOf(), 4); obj = doc.strs.toObject(); - assert.equal(obj[0],'one'); - assert.equal(obj[1],'two'); + assert.equal(obj[0], 'one'); + assert.equal(obj[1], 'two'); doc.save(function(err) { assert.ifError(err); @@ -654,18 +684,18 @@ describe('types array', function() { assert.ifError(err); var obj = doc.types.toObject(); - assert.equal(obj[0].type,'bird'); - assert.equal(obj[1].type,'boy'); - assert.equal(obj[2].type,'frog'); + assert.equal(obj[0].type, 'bird'); + assert.equal(obj[1].type, 'boy'); + assert.equal(obj[2].type, 'frog'); obj = doc.nums.toObject(); - assert.equal(obj[0].valueOf(),1); - assert.equal(obj[1].valueOf(),2); - assert.equal(obj[2].valueOf(),4); + assert.equal(obj[0].valueOf(), 1); + assert.equal(obj[1].valueOf(), 2); + assert.equal(obj[2].valueOf(), 4); obj = doc.strs.toObject(); - assert.equal(obj[0],'one'); - assert.equal(obj[1],'two'); + assert.equal(obj[0], 'one'); + assert.equal(obj[1], 'two'); done(); }); }); @@ -677,26 +707,26 @@ describe('types array', function() { describe('pull()', function() { it('works', function(done) { var db = start(); - var catschema = new Schema({ name: String }); + var catschema = new Schema({name: String}); var Cat = db.model('Cat', catschema); var schema = new Schema({ - a: [{ type: Schema.ObjectId, ref: 'Cat' }] + a: [{type: Schema.ObjectId, ref: 'Cat'}] }); var A = db.model('TestPull', schema); - var cat = new Cat({ name: 'peanut' }); + var cat = new Cat({name: 'peanut'}); cat.save(function(err) { assert.ifError(err); - var a = new A({ a: [cat._id] }); + var a = new A({a: [cat._id]}); a.save(function(err) { assert.ifError(err); A.findById(a, function(err, doc) { db.close(); assert.ifError(err); - assert.equal(1, doc.a.length); + assert.equal(doc.a.length, 1); doc.a.pull(cat.id); - assert.equal(doc.a.length,0); + assert.equal(doc.a.length, 0); done(); }); }); @@ -708,7 +738,7 @@ describe('types array', function() { var personSchema = new Schema({ name: String, role: String - }, { _id: false }); + }, {_id: false}); var bandSchema = new Schema({ name: String, members: [personSchema] @@ -717,19 +747,19 @@ describe('types array', function() { var Band = db.model('gh3341', bandSchema, 'gh3341'); var gnr = new Band({ - name: "Guns N' Roses", + name: 'Guns N\' Roses', members: [ - { name: 'Axl', role: 'Lead Singer' }, - { name: 'Slash', role: 'Guitar' }, - { name: 'Izzy', role: 'Guitar' }, - { name: 'Duff', role: 'Bass' }, - { name: 'Adler', role: 'Drums' } + {name: 'Axl', role: 'Lead Singer'}, + {name: 'Slash', role: 'Guitar'}, + {name: 'Izzy', role: 'Guitar'}, + {name: 'Duff', role: 'Bass'}, + {name: 'Adler', role: 'Drums'} ] }); gnr.save(function(error) { assert.ifError(error); - gnr.members.pull({ name: 'Slash', role: 'Guitar' }); + gnr.members.pull({name: 'Slash', role: 'Guitar'}); gnr.save(function(error) { assert.ifError(error); assert.equal(gnr.members.length, 4); @@ -744,7 +774,38 @@ describe('types array', function() { assert.equal(gnr.members[1].name, 'Izzy'); assert.equal(gnr.members[2].name, 'Duff'); assert.equal(gnr.members[3].name, 'Adler'); - done(); + db.close(done); + }); + }); + }); + }); + + it('properly works with undefined', function(done) { + var db = start(); + var catschema = new Schema({ name: String, colors: [{hex: String}] }); + var Cat = db.model('Cat', catschema); + + var cat = new Cat({name: 'peanut', colors: [ + {hex: '#FFF'}, {hex: '#000'}, null + ]}); + + cat.save(function(err) { + assert.ifError(err); + + cat.colors.pull(undefined); // converted to null (as mongodb does) + assert.equal(cat.colors.length, 2); + assert.equal(cat.colors[0].hex, '#FFF'); + assert.equal(cat.colors[1].hex, '#000'); + + cat.save(function(err) { + assert.ifError(err); + + Cat.findById(cat._id, function(err, doc) { + assert.ifError(err); + assert.equal(doc.colors.length, 2); + assert.equal(doc.colors[0].hex, '#FFF'); + assert.equal(doc.colors[1].hex, '#000'); + db.close(done); }); }); }); @@ -754,35 +815,35 @@ describe('types array', function() { describe('$pop()', function() { it('works', function(done) { var db = start(); - var painting = new Schema({ colors: [] }); + var painting = new Schema({colors: []}); var Painting = db.model('Painting', painting); - var p = new Painting({ colors : ['blue', 'green', 'yellow'] }); + var p = new Painting({colors: ['blue', 'green', 'yellow']}); p.save(function(err) { assert.ifError(err); Painting.findById(p, function(err, doc) { assert.ifError(err); - assert.equal(3, doc.colors.length); + assert.equal(doc.colors.length, 3); var color = doc.colors.$pop(); - assert.equal(2, doc.colors.length); + assert.equal(doc.colors.length, 2); assert.equal(color, 'yellow'); // MongoDB pop command can only be called once per save, each // time only removing one element. color = doc.colors.$pop(); assert.equal(color, undefined); - assert.equal(2, doc.colors.length); - assert.equal(false, '$set' in doc.colors._atomics, 'invalid $atomic op used'); + assert.equal(doc.colors.length, 2); + assert.ok(!('$set' in doc.colors._atomics), 'invalid $atomic op used'); doc.save(function(err) { - assert.equal(null, err); + assert.equal(err, null); var color = doc.colors.$pop(); - assert.equal(1, doc.colors.length); + assert.equal(doc.colors.length, 1); assert.equal(color, 'green'); doc.save(function(err) { - assert.equal(null, err); + assert.equal(err, null); Painting.findById(doc, function(err, doc) { db.close(); assert.strictEqual(null, err); - assert.equal(1, doc.colors.length); + assert.equal(doc.colors.length, 1); assert.equal(doc.colors[0], 'blue'); done(); }); @@ -795,29 +856,29 @@ describe('types array', function() { describe('addToSet()', function() { it('works', function(done) { - var db = start() - , e = new Schema({ name: String, arr: [] }) - , schema = new Schema({ - num: [Number] - , str: [String] - , doc: [e] - , date: [Date] - , id: [Schema.ObjectId] - }); + var db = start(), + e = new Schema({name: String, arr: []}), + schema = new Schema({ + num: [Number], + str: [String], + doc: [e], + date: [Date], + id: [Schema.ObjectId] + }); var M = db.model('testAddToSet', schema); var m = new M; - m.num.push(1,2,3); - m.str.push('one','two','tres'); - m.doc.push({ name: 'Dubstep', arr: [1] }, { name: 'Polka', arr: [{ x: 3 }]}); + m.num.push(1, 2, 3); + m.str.push('one', 'two', 'tres'); + m.doc.push({name: 'Dubstep', arr: [1]}, {name: 'Polka', arr: [{x: 3}]}); var d1 = new Date; - var d2 = new Date( +d1 + 60000); - var d3 = new Date( +d1 + 30000); - var d4 = new Date( +d1 + 20000); - var d5 = new Date( +d1 + 90000); - var d6 = new Date( +d1 + 10000); + var d2 = new Date(+d1 + 60000); + var d3 = new Date(+d1 + 30000); + var d4 = new Date(+d1 + 20000); + var d5 = new Date(+d1 + 90000); + var d6 = new Date(+d1 + 10000); m.date.push(d1, d2); var id1 = new mongoose.Types.ObjectId; @@ -829,69 +890,75 @@ describe('types array', function() { m.id.push(id1, id2); - m.num.addToSet(3,4,5); - assert.equal(5, m.num.length); + m.num.addToSet(3, 4, 5); + assert.equal(m.num.length, 5); m.str.addToSet('four', 'five', 'two'); - assert.equal(m.str.length,5); + assert.equal(m.str.length, 5); m.id.addToSet(id2, id3); - assert.equal(m.id.length,3); + assert.equal(m.id.length, 3); m.doc.addToSet(m.doc[0]); - assert.equal(m.doc.length,2); - m.doc.addToSet({ name: 'Waltz', arr: [1] }, m.doc[0]); - assert.equal(m.doc.length,3); - assert.equal(m.date.length,2); + assert.equal(m.doc.length, 2); + m.doc.addToSet({name: 'Waltz', arr: [1]}, m.doc[0]); + assert.equal(m.doc.length, 3); + assert.equal(m.date.length, 2); m.date.addToSet(d1); - assert.equal(m.date.length,2); + assert.equal(m.date.length, 2); m.date.addToSet(d3); - assert.equal(m.date.length,3); + assert.equal(m.date.length, 3); m.save(function(err) { assert.ifError(err); M.findById(m, function(err, m) { assert.ifError(err); - assert.equal(m.num.length,5); + assert.equal(m.num.length, 5); assert.ok(~m.num.indexOf(1)); assert.ok(~m.num.indexOf(2)); assert.ok(~m.num.indexOf(3)); assert.ok(~m.num.indexOf(4)); assert.ok(~m.num.indexOf(5)); - assert.equal(m.str.length,5); + assert.equal(m.str.length, 5); assert.ok(~m.str.indexOf('one')); assert.ok(~m.str.indexOf('two')); assert.ok(~m.str.indexOf('tres')); assert.ok(~m.str.indexOf('four')); assert.ok(~m.str.indexOf('five')); - assert.equal(m.id.length,3); + assert.equal(m.id.length, 3); assert.ok(~m.id.indexOf(id1)); assert.ok(~m.id.indexOf(id2)); assert.ok(~m.id.indexOf(id3)); - assert.equal(m.date.length,3); + assert.equal(m.date.length, 3); assert.ok(~m.date.indexOf(d1.toString())); assert.ok(~m.date.indexOf(d2.toString())); assert.ok(~m.date.indexOf(d3.toString())); - assert.equal(m.doc.length,3); - assert.ok(m.doc.some(function(v) { return v.name === 'Waltz';})); - assert.ok(m.doc.some(function(v) { return v.name === 'Dubstep';})); - assert.ok(m.doc.some(function(v) { return v.name === 'Polka';})); + assert.equal(m.doc.length, 3); + assert.ok(m.doc.some(function(v) { + return v.name === 'Waltz'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === 'Dubstep'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === 'Polka'; + })); // test single $addToSet - m.num.addToSet(3,4,5,6); - assert.equal(m.num.length,6); + m.num.addToSet(3, 4, 5, 6); + assert.equal(m.num.length, 6); m.str.addToSet('four', 'five', 'two', 'six'); - assert.equal(m.str.length,6); + assert.equal(m.str.length, 6); m.id.addToSet(id2, id3, id4); - assert.equal(m.id.length,4); + assert.equal(m.id.length, 4); m.date.addToSet(d1, d3, d4); - assert.equal(m.date.length,4); + assert.equal(m.date.length, 4); - m.doc.addToSet(m.doc[0], { name: '8bit' }); - assert.equal(m.doc.length,4); + m.doc.addToSet(m.doc[0], {name: '8bit'}); + assert.equal(m.doc.length, 4); m.save(function(err) { assert.ifError(err); @@ -899,7 +966,7 @@ describe('types array', function() { M.findById(m, function(err, m) { assert.ifError(err); - assert.equal(m.num.length,6); + assert.equal(m.num.length, 6); assert.ok(~m.num.indexOf(1)); assert.ok(~m.num.indexOf(2)); assert.ok(~m.num.indexOf(3)); @@ -907,7 +974,7 @@ describe('types array', function() { assert.ok(~m.num.indexOf(5)); assert.ok(~m.num.indexOf(6)); - assert.equal(m.str.length,6); + assert.equal(m.str.length, 6); assert.ok(~m.str.indexOf('one')); assert.ok(~m.str.indexOf('two')); assert.ok(~m.str.indexOf('tres')); @@ -915,37 +982,45 @@ describe('types array', function() { assert.ok(~m.str.indexOf('five')); assert.ok(~m.str.indexOf('six')); - assert.equal(m.id.length,4); + assert.equal(m.id.length, 4); assert.ok(~m.id.indexOf(id1)); assert.ok(~m.id.indexOf(id2)); assert.ok(~m.id.indexOf(id3)); assert.ok(~m.id.indexOf(id4)); - assert.equal(m.date.length,4); + assert.equal(m.date.length, 4); assert.ok(~m.date.indexOf(d1.toString())); assert.ok(~m.date.indexOf(d2.toString())); assert.ok(~m.date.indexOf(d3.toString())); assert.ok(~m.date.indexOf(d4.toString())); - assert.equal(m.doc.length,4); - assert.ok(m.doc.some(function(v) { return v.name === 'Waltz';})); - assert.ok(m.doc.some(function(v) { return v.name === 'Dubstep';})); - assert.ok(m.doc.some(function(v) { return v.name === 'Polka';})); - assert.ok(m.doc.some(function(v) { return v.name === '8bit';})); + assert.equal(m.doc.length, 4); + assert.ok(m.doc.some(function(v) { + return v.name === 'Waltz'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === 'Dubstep'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === 'Polka'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === '8bit'; + })); // test multiple $addToSet - m.num.addToSet(7,8); - assert.equal(m.num.length,8); + m.num.addToSet(7, 8); + assert.equal(m.num.length, 8); m.str.addToSet('seven', 'eight'); - assert.equal(m.str.length,8); + assert.equal(m.str.length, 8); m.id.addToSet(id5, id6); - assert.equal(m.id.length,6); + assert.equal(m.id.length, 6); m.date.addToSet(d5, d6); - assert.equal(m.date.length,6); + assert.equal(m.date.length, 6); - m.doc.addToSet(m.doc[1], { name: 'BigBeat' }, { name: 'Funk' }); - assert.equal(m.doc.length,6); + m.doc.addToSet(m.doc[1], {name: 'BigBeat'}, {name: 'Funk'}); + assert.equal(m.doc.length, 6); m.save(function(err) { assert.ifError(err); @@ -954,7 +1029,7 @@ describe('types array', function() { db.close(); assert.ifError(err); - assert.equal(m.num.length,8); + assert.equal(m.num.length, 8); assert.ok(~m.num.indexOf(1)); assert.ok(~m.num.indexOf(2)); assert.ok(~m.num.indexOf(3)); @@ -964,7 +1039,7 @@ describe('types array', function() { assert.ok(~m.num.indexOf(7)); assert.ok(~m.num.indexOf(8)); - assert.equal(m.str.length,8); + assert.equal(m.str.length, 8); assert.ok(~m.str.indexOf('one')); assert.ok(~m.str.indexOf('two')); assert.ok(~m.str.indexOf('tres')); @@ -974,7 +1049,7 @@ describe('types array', function() { assert.ok(~m.str.indexOf('seven')); assert.ok(~m.str.indexOf('eight')); - assert.equal(m.id.length,6); + assert.equal(m.id.length, 6); assert.ok(~m.id.indexOf(id1)); assert.ok(~m.id.indexOf(id2)); assert.ok(~m.id.indexOf(id3)); @@ -982,7 +1057,7 @@ describe('types array', function() { assert.ok(~m.id.indexOf(id5)); assert.ok(~m.id.indexOf(id6)); - assert.equal(m.date.length,6); + assert.equal(m.date.length, 6); assert.ok(~m.date.indexOf(d1.toString())); assert.ok(~m.date.indexOf(d2.toString())); assert.ok(~m.date.indexOf(d3.toString())); @@ -990,13 +1065,25 @@ describe('types array', function() { assert.ok(~m.date.indexOf(d5.toString())); assert.ok(~m.date.indexOf(d6.toString())); - assert.equal(m.doc.length,6); - assert.ok(m.doc.some(function(v) { return v.name === 'Waltz';})); - assert.ok(m.doc.some(function(v) { return v.name === 'Dubstep';})); - assert.ok(m.doc.some(function(v) { return v.name === 'Polka';})); - assert.ok(m.doc.some(function(v) { return v.name === '8bit';})); - assert.ok(m.doc.some(function(v) { return v.name === 'BigBeat';})); - assert.ok(m.doc.some(function(v) { return v.name === 'Funk';})); + assert.equal(m.doc.length, 6); + assert.ok(m.doc.some(function(v) { + return v.name === 'Waltz'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === 'Dubstep'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === 'Polka'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === '8bit'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === 'BigBeat'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === 'Funk'; + })); done(); }); }); @@ -1007,27 +1094,31 @@ describe('types array', function() { }); it('handles sub-documents that do not have an _id gh-1973', function(done) { - var db = start() - , e = new Schema({ name: String, arr: [] }, { _id: false }) - , schema = new Schema({ - doc: [e] - }); + var db = start(), + e = new Schema({name: String, arr: []}, {_id: false}), + schema = new Schema({ + doc: [e] + }); var M = db.model('gh1973', schema); var m = new M; - m.doc.addToSet({ name: 'Rap' }); + m.doc.addToSet({name: 'Rap'}); m.save(function(error, m) { assert.ifError(error); - assert.equal(1, m.doc.length); - assert.equal('Rap', m.doc[0].name); - m.doc.addToSet({ name: 'House' }); - assert.equal(2, m.doc.length); + assert.equal(m.doc.length, 1); + assert.equal(m.doc[0].name, 'Rap'); + m.doc.addToSet({name: 'House'}); + assert.equal(m.doc.length, 2); m.save(function(error, m) { assert.ifError(error); - assert.equal(2, m.doc.length); - assert.ok(m.doc.some(function(v) { return v.name === 'Rap'; })); - assert.ok(m.doc.some(function(v) { return v.name === 'House'; })); + assert.equal(m.doc.length, 2); + assert.ok(m.doc.some(function(v) { + return v.name === 'Rap'; + })); + assert.ok(m.doc.some(function(v) { + return v.name === 'House'; + })); db.close(done); }); }); @@ -1035,22 +1126,24 @@ describe('types array', function() { it('applies setters (gh-3032)', function(done) { var db = start(); - var ST = db.model('setterArray', Schema({ arr: [{ - type: String, - lowercase: true - }] })); - var m = new ST({ arr: ["ONE", "TWO"] }); + var ST = db.model('setterArray', Schema({ + arr: [{ + type: String, + lowercase: true + }] + })); + var m = new ST({arr: ['ONE', 'TWO']}); m.save(function(err, doc) { assert.ifError(err); - assert.equal(2, doc.arr.length); - doc.arr.addToSet("THREE"); + assert.equal(doc.arr.length, 2); + doc.arr.addToSet('THREE'); assert.strictEqual('one', doc.arr[0]); assert.strictEqual('two', doc.arr[1]); assert.strictEqual('three', doc.arr[2]); doc.save(function(err, doc) { assert.ifError(err); - assert.equal(3, doc.arr.length); + assert.equal(doc.arr.length, 3); assert.strictEqual('one', doc.arr[0]); assert.strictEqual('two', doc.arr[1]); assert.strictEqual('three', doc.arr[2]); @@ -1067,31 +1160,31 @@ describe('types array', function() { var U = db.model('User'); var ID = mongoose.Types.ObjectId; - var u = new U({ name: 'banana', pets: [new ID] }); - assert.equal(u.pets.length,1); + var u = new U({name: 'banana', pets: [new ID]}); + assert.equal(u.pets.length, 1); u.pets.nonAtomicPush(new ID); - assert.equal(u.pets.length,2); + assert.equal(u.pets.length, 2); u.save(function(err) { assert.ifError(err); U.findById(u._id, function(err) { assert.ifError(err); - assert.equal(u.pets.length,2); + assert.equal(u.pets.length, 2); var id0 = u.pets[0]; var id1 = u.pets[1]; var id2 = new ID; u.pets.pull(id0); u.pets.nonAtomicPush(id2); - assert.equal(u.pets.length,2); - assert.equal(u.pets[0].toString(),id1.toString()); - assert.equal(u.pets[1].toString(),id2.toString()); + assert.equal(u.pets.length, 2); + assert.equal(u.pets[0].toString(), id1.toString()); + assert.equal(u.pets[1].toString(), id2.toString()); u.save(function(err) { assert.ifError(err); U.findById(u._id, function(err) { db.close(); assert.ifError(err); - assert.equal(u.pets.length,2); - assert.equal(u.pets[0].toString(),id1.toString()); - assert.equal(u.pets[1].toString(),id2.toString()); + assert.equal(u.pets.length, 2); + assert.equal(u.pets[0].toString(), id1.toString()); + assert.equal(u.pets[1].toString(), id2.toString()); done(); }); }); @@ -1103,17 +1196,17 @@ describe('types array', function() { describe('sort()', function() { it('order should be saved', function(done) { var db = start(); - var M = db.model('ArraySortOrder', new Schema({ x: [Number] })); - var m = new M({ x: [1,4,3,2] }); + var M = db.model('ArraySortOrder', new Schema({x: [Number]})); + var m = new M({x: [1, 4, 3, 2]}); m.save(function(err) { assert.ifError(err); M.findById(m, function(err, m) { assert.ifError(err); - assert.equal(1, m.x[0]); - assert.equal(4, m.x[1]); - assert.equal(3, m.x[2]); - assert.equal(2, m.x[3]); + assert.equal(m.x[0], 1); + assert.equal(m.x[1], 4); + assert.equal(m.x[2], 3); + assert.equal(m.x[3], 2); m.x.sort(); @@ -1122,13 +1215,13 @@ describe('types array', function() { M.findById(m, function(err, m) { assert.ifError(err); - assert.equal(1, m.x[0]); - assert.equal(2, m.x[1]); - assert.equal(3, m.x[2]); - assert.equal(4, m.x[3]); + assert.equal(m.x[0], 1); + assert.equal(m.x[1], 2); + assert.equal(m.x[2], 3); + assert.equal(m.x[3], 4); - m.x.sort(function(a,b) { - return b > a; + m.x.sort(function(a, b) { + return b - a; }); m.save(function(err) { @@ -1136,10 +1229,10 @@ describe('types array', function() { M.findById(m, function(err, m) { assert.ifError(err); - assert.equal(4, m.x[0]); - assert.equal(3, m.x[1]); - assert.equal(2, m.x[2]); - assert.equal(1, m.x[3]); + assert.equal(m.x[0], 4); + assert.equal(m.x[1], 3); + assert.equal(m.x[2], 2); + assert.equal(m.x[3], 1); db.close(done); }); }); @@ -1155,22 +1248,27 @@ describe('types array', function() { function save(doc, cb) { doc.save(function(err) { - if (err) return cb(err); + if (err) { + cb(err); + return; + } doc.constructor.findById(doc._id, cb); }); } before(function(done) { db = start(); - N = db.model('arraySet', Schema({ arr: [Number] })); - S = db.model('arraySetString', Schema({ arr: [String] })); - B = db.model('arraySetBuffer', Schema({ arr: [Buffer] })); - M = db.model('arraySetMixed', Schema({ arr: [] })); - D = db.model('arraySetSubDocs', Schema({ arr: [{ name: String}] })); - ST = db.model('arrayWithSetters', Schema({ arr: [{ - type: String, - lowercase: true - }] })); + N = db.model('arraySet', Schema({arr: [Number]})); + S = db.model('arraySetString', Schema({arr: [String]})); + B = db.model('arraySetBuffer', Schema({arr: [Buffer]})); + M = db.model('arraySetMixed', Schema({arr: []})); + D = db.model('arraySetSubDocs', Schema({arr: [{name: String}]})); + ST = db.model('arrayWithSetters', Schema({ + arr: [{ + type: String, + lowercase: true + }] + })); done(); }); @@ -1179,45 +1277,45 @@ describe('types array', function() { }); it('works combined with other ops', function(done) { - var m = new N({ arr: [3,4,5,6] }); + var m = new N({arr: [3, 4, 5, 6]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(4, doc.arr.length); + assert.equal(doc.arr.length, 4); doc.arr.push(20); doc.arr.set(2, 10); - assert.equal(5, doc.arr.length); - assert.equal(10, doc.arr[2]); - assert.equal(20, doc.arr[4]); + assert.equal(doc.arr.length, 5); + assert.equal(doc.arr[2], 10); + assert.equal(doc.arr[4], 20); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(5, doc.arr.length); - assert.equal(3, doc.arr[0]); - assert.equal(4, doc.arr[1]); - assert.equal(10, doc.arr[2]); - assert.equal(6, doc.arr[3]); - assert.equal(20, doc.arr[4]); + assert.equal(doc.arr.length, 5); + assert.equal(doc.arr[0], 3); + assert.equal(doc.arr[1], 4); + assert.equal(doc.arr[2], 10); + assert.equal(doc.arr[3], 6); + assert.equal(doc.arr[4], 20); doc.arr.$pop(); - assert.equal(4, doc.arr.length); + assert.equal(doc.arr.length, 4); doc.arr.set(4, 99); - assert.equal(5, doc.arr.length); - assert.equal(99, doc.arr[4]); + assert.equal(doc.arr.length, 5); + assert.equal(doc.arr[4], 99); doc.arr.remove(10); - assert.equal(4, doc.arr.length); - assert.equal(3, doc.arr[0]); - assert.equal(4, doc.arr[1]); - assert.equal(6, doc.arr[2]); - assert.equal(99, doc.arr[3]); + assert.equal(doc.arr.length, 4); + assert.equal(doc.arr[0], 3); + assert.equal(doc.arr[1], 4); + assert.equal(doc.arr[2], 6); + assert.equal(doc.arr[3], 99); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(4, doc.arr.length); - assert.equal(3, doc.arr[0]); - assert.equal(4, doc.arr[1]); - assert.equal(6, doc.arr[2]); - assert.equal(99, doc.arr[3]); + assert.equal(doc.arr.length, 4); + assert.equal(doc.arr[0], 3); + assert.equal(doc.arr[1], 4); + assert.equal(doc.arr[2], 6); + assert.equal(doc.arr[3], 99); done(); }); }); @@ -1227,44 +1325,44 @@ describe('types array', function() { }); it('works with numbers', function(done) { - var m = new N({ arr: [3,4,5,6] }); + var m = new N({arr: [3, 4, 5, 6]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(4, doc.arr.length); + assert.equal(doc.arr.length, 4); doc.arr.set(2, 10); - assert.equal(4, doc.arr.length); - assert.equal(10, doc.arr[2]); + assert.equal(doc.arr.length, 4); + assert.equal(doc.arr[2], 10); doc.arr.set(doc.arr.length, 11); - assert.equal(5, doc.arr.length); - assert.equal(11, doc.arr[4]); + assert.equal(doc.arr.length, 5); + assert.equal(doc.arr[4], 11); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(5, doc.arr.length); - assert.equal(3, doc.arr[0]); - assert.equal(4, doc.arr[1]); - assert.equal(10, doc.arr[2]); - assert.equal(6, doc.arr[3]); - assert.equal(11, doc.arr[4]); + assert.equal(doc.arr.length, 5); + assert.equal(doc.arr[0], 3); + assert.equal(doc.arr[1], 4); + assert.equal(doc.arr[2], 10); + assert.equal(doc.arr[3], 6); + assert.equal(doc.arr[4], 11); // casting + setting beyond current array length - doc.arr.set(8, "1"); - assert.equal(9, doc.arr.length); + doc.arr.set(8, '1'); + assert.equal(doc.arr.length, 9); assert.strictEqual(1, doc.arr[8]); - assert.equal(undefined, doc.arr[7]); + assert.equal(doc.arr[7], undefined); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(9, doc.arr.length); - assert.equal(3, doc.arr[0]); - assert.equal(4, doc.arr[1]); - assert.equal(10, doc.arr[2]); - assert.equal(6, doc.arr[3]); - assert.equal(11, doc.arr[4]); - assert.equal(null, doc.arr[5]); - assert.equal(null, doc.arr[6]); - assert.equal(null, doc.arr[7]); + assert.equal(doc.arr.length, 9); + assert.equal(doc.arr[0], 3); + assert.equal(doc.arr[1], 4); + assert.equal(doc.arr[2], 10); + assert.equal(doc.arr[3], 6); + assert.equal(doc.arr[4], 11); + assert.equal(doc.arr[5], null); + assert.equal(doc.arr[6], null); + assert.equal(doc.arr[7], null); assert.strictEqual(1, doc.arr[8]); done(); }); @@ -1273,44 +1371,44 @@ describe('types array', function() { }); it('works with strings', function(done) { - var m = new S({ arr: [3,4,5,6] }); + var m = new S({arr: [3, 4, 5, 6]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal('4', doc.arr.length); + assert.equal(doc.arr.length, '4'); doc.arr.set(2, 10); - assert.equal(4, doc.arr.length); - assert.equal('10', doc.arr[2]); + assert.equal(doc.arr.length, 4); + assert.equal(doc.arr[2], '10'); doc.arr.set(doc.arr.length, '11'); - assert.equal(5, doc.arr.length); - assert.equal('11', doc.arr[4]); + assert.equal(doc.arr.length, 5); + assert.equal(doc.arr[4], '11'); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(5, doc.arr.length); - assert.equal('3', doc.arr[0]); - assert.equal('4', doc.arr[1]); - assert.equal('10', doc.arr[2]); - assert.equal('6', doc.arr[3]); - assert.equal('11', doc.arr[4]); + assert.equal(doc.arr.length, 5); + assert.equal(doc.arr[0], '3'); + assert.equal(doc.arr[1], '4'); + assert.equal(doc.arr[2], '10'); + assert.equal(doc.arr[3], '6'); + assert.equal(doc.arr[4], '11'); // casting + setting beyond current array length - doc.arr.set(8, "yo"); - assert.equal(9, doc.arr.length); - assert.strictEqual("yo", doc.arr[8]); - assert.equal(undefined, doc.arr[7]); + doc.arr.set(8, 'yo'); + assert.equal(doc.arr.length, 9); + assert.strictEqual('yo', doc.arr[8]); + assert.equal(doc.arr[7], undefined); save(doc, function(err, doc) { assert.ifError(err); - assert.equal('9', doc.arr.length); - assert.equal('3', doc.arr[0]); - assert.equal('4', doc.arr[1]); - assert.equal('10', doc.arr[2]); - assert.equal('6', doc.arr[3]); - assert.equal('11', doc.arr[4]); - assert.equal(null, doc.arr[5]); - assert.equal(null, doc.arr[6]); - assert.equal(null, doc.arr[7]); + assert.equal(doc.arr.length, '9'); + assert.equal(doc.arr[0], '3'); + assert.equal(doc.arr[1], '4'); + assert.equal(doc.arr[2], '10'); + assert.equal(doc.arr[3], '6'); + assert.equal(doc.arr[4], '11'); + assert.equal(doc.arr[5], null); + assert.equal(doc.arr[6], null); + assert.equal(doc.arr[7], null); assert.strictEqual('yo', doc.arr[8]); done(); }); @@ -1319,82 +1417,82 @@ describe('types array', function() { }); it('works with buffers', function(done) { - var m = new B({ arr: [[0], new Buffer(1)] }); + var m = new B({arr: [[0], new Buffer(1)]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(2, doc.arr.length); + assert.equal(doc.arr.length, 2); assert.ok(doc.arr[0].isMongooseBuffer); assert.ok(doc.arr[1].isMongooseBuffer); - doc.arr.set(1, "nice"); - assert.equal(2, doc.arr.length); + doc.arr.set(1, 'nice'); + assert.equal(doc.arr.length, 2); assert.ok(doc.arr[1].isMongooseBuffer); - assert.equal("nice", doc.arr[1].toString('utf8')); + assert.equal(doc.arr[1].toString('utf8'), 'nice'); doc.arr.set(doc.arr.length, [11]); - assert.equal(3, doc.arr.length); - assert.equal(11, doc.arr[2][0]); + assert.equal(doc.arr.length, 3); + assert.equal(doc.arr[2][0], 11); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(3, doc.arr.length); + assert.equal(doc.arr.length, 3); assert.ok(doc.arr[0].isMongooseBuffer); assert.ok(doc.arr[1].isMongooseBuffer); assert.ok(doc.arr[2].isMongooseBuffer); - assert.equal('\u0000', doc.arr[0].toString()); - assert.equal("nice", doc.arr[1].toString()); - assert.equal(11, doc.arr[2][0]); + assert.equal(doc.arr[0].toString(), '\u0000'); + assert.equal(doc.arr[1].toString(), 'nice'); + assert.equal(doc.arr[2][0], 11); done(); }); }); }); it('works with mixed', function(done) { - var m = new M({ arr: [3,{x:1},'yes', [5]] }); + var m = new M({arr: [3, {x: 1}, 'yes', [5]]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(4, doc.arr.length); + assert.equal(doc.arr.length, 4); doc.arr.set(2, null); - assert.equal(4, doc.arr.length); - assert.equal(null, doc.arr[2]); - doc.arr.set(doc.arr.length, "last"); - assert.equal(5, doc.arr.length); - assert.equal("last", doc.arr[4]); + assert.equal(doc.arr.length, 4); + assert.equal(doc.arr[2], null); + doc.arr.set(doc.arr.length, 'last'); + assert.equal(doc.arr.length, 5); + assert.equal(doc.arr[4], 'last'); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(5, doc.arr.length); - assert.equal(3, doc.arr[0]); + assert.equal(doc.arr.length, 5); + assert.equal(doc.arr[0], 3); assert.strictEqual(1, doc.arr[1].x); - assert.equal(null, doc.arr[2]); + assert.equal(doc.arr[2], null); assert.ok(Array.isArray(doc.arr[3])); - assert.equal(5, doc.arr[3][0]); - assert.equal("last", doc.arr[4]); + assert.equal(doc.arr[3][0], 5); + assert.equal(doc.arr[4], 'last'); doc.arr.set(8, Infinity); - assert.equal(9, doc.arr.length); + assert.equal(doc.arr.length, 9); assert.strictEqual(Infinity, doc.arr[8]); - assert.equal(undefined, doc.arr[7]); + assert.equal(doc.arr[7], undefined); doc.arr.push(new Buffer(0)); - assert.equal('', doc.arr[9].toString()); - assert.equal(10, doc.arr.length); + assert.equal(doc.arr[9].toString(), ''); + assert.equal(doc.arr.length, 10); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(10, doc.arr.length); - assert.equal(3, doc.arr[0]); + assert.equal(doc.arr.length, 10); + assert.equal(doc.arr[0], 3); assert.strictEqual(1, doc.arr[1].x); - assert.equal(null, doc.arr[2]); + assert.equal(doc.arr[2], null); assert.ok(Array.isArray(doc.arr[3])); - assert.equal(5, doc.arr[3][0]); - assert.equal("last", doc.arr[4]); + assert.equal(doc.arr[3][0], 5); + assert.equal(doc.arr[4], 'last'); assert.strictEqual(null, doc.arr[5]); assert.strictEqual(null, doc.arr[6]); assert.strictEqual(null, doc.arr[7]); assert.strictEqual(Infinity, doc.arr[8]); // arr[9] is actually a mongodb Binary since mixed won't cast to buffer - assert.equal('', doc.arr[9].toString()); + assert.equal(doc.arr[9].toString(), ''); done(); }); @@ -1403,85 +1501,83 @@ describe('types array', function() { }); it('works with sub-docs', function(done) { - var m = new D({ arr: [{name:'aaron'}, {name:'moombahton '}] }); + var m = new D({arr: [{name: 'aaron'}, {name: 'moombahton '}]}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(2, doc.arr.length); - doc.arr.set(0, {name:'vdrums'}); - assert.equal(2, doc.arr.length); - assert.equal('vdrums', doc.arr[0].name); - doc.arr.set(doc.arr.length, {name:"Restrepo"}); - assert.equal(3, doc.arr.length); - assert.equal("Restrepo", doc.arr[2].name); + assert.equal(doc.arr.length, 2); + doc.arr.set(0, {name: 'vdrums'}); + assert.equal(doc.arr.length, 2); + assert.equal(doc.arr[0].name, 'vdrums'); + doc.arr.set(doc.arr.length, {name: 'Restrepo'}); + assert.equal(doc.arr.length, 3); + assert.equal(doc.arr[2].name, 'Restrepo'); save(doc, function(err, doc) { assert.ifError(err); // validate - assert.equal(3, doc.arr.length); - assert.equal('vdrums', doc.arr[0].name); - assert.equal("moombahton ", doc.arr[1].name); - assert.equal("Restrepo", doc.arr[2].name); + assert.equal(doc.arr.length, 3); + assert.equal(doc.arr[0].name, 'vdrums'); + assert.equal(doc.arr[1].name, 'moombahton '); + assert.equal(doc.arr[2].name, 'Restrepo'); - doc.arr.set(10, { name: 'temple of doom' }); - assert.equal(11, doc.arr.length); - assert.equal('temple of doom', doc.arr[10].name); - assert.equal(null, doc.arr[9]); + doc.arr.set(10, {name: 'temple of doom'}); + assert.equal(doc.arr.length, 11); + assert.equal(doc.arr[10].name, 'temple of doom'); + assert.equal(doc.arr[9], null); save(doc, function(err, doc) { assert.ifError(err); // validate - assert.equal(11, doc.arr.length); - assert.equal('vdrums', doc.arr[0].name); - assert.equal("moombahton ", doc.arr[1].name); - assert.equal("Restrepo", doc.arr[2].name); - assert.equal(null, doc.arr[3]); - assert.equal(null, doc.arr[9]); - assert.equal('temple of doom', doc.arr[10].name); + assert.equal(doc.arr.length, 11); + assert.equal(doc.arr[0].name, 'vdrums'); + assert.equal(doc.arr[1].name, 'moombahton '); + assert.equal(doc.arr[2].name, 'Restrepo'); + assert.equal(doc.arr[3], null); + assert.equal(doc.arr[9], null); + assert.equal(doc.arr[10].name, 'temple of doom'); doc.arr.remove(doc.arr[0]); - doc.arr.set(7, { name: 7 }); - assert.strictEqual("7", doc.arr[7].name); - assert.equal(10, doc.arr.length); + doc.arr.set(7, {name: 7}); + assert.strictEqual('7', doc.arr[7].name); + assert.equal(doc.arr.length, 10); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(10, doc.arr.length); - assert.equal("moombahton ", doc.arr[0].name); - assert.equal("Restrepo", doc.arr[1].name); - assert.equal(null, doc.arr[2]); + assert.equal(doc.arr.length, 10); + assert.equal(doc.arr[0].name, 'moombahton '); + assert.equal(doc.arr[1].name, 'Restrepo'); + assert.equal(doc.arr[2], null); assert.ok(doc.arr[7]); - assert.strictEqual("7", doc.arr[7].name); - assert.equal(null, doc.arr[8]); - assert.equal('temple of doom', doc.arr[9].name); + assert.strictEqual('7', doc.arr[7].name); + assert.equal(doc.arr[8], null); + assert.equal(doc.arr[9].name, 'temple of doom'); done(); - }); }); - }); }); }); it('applies setters (gh-3032)', function(done) { - var m = new ST({ arr: ["ONE", "TWO"] }); + var m = new ST({arr: ['ONE', 'TWO']}); save(m, function(err, doc) { assert.ifError(err); - assert.equal(2, doc.arr.length); - doc.arr.set(0, "THREE"); + assert.equal(doc.arr.length, 2); + doc.arr.set(0, 'THREE'); assert.strictEqual('three', doc.arr[0]); assert.strictEqual('two', doc.arr[1]); - doc.arr.set(doc.arr.length, "FOUR"); + doc.arr.set(doc.arr.length, 'FOUR'); assert.strictEqual('three', doc.arr[0]); assert.strictEqual('two', doc.arr[1]); assert.strictEqual('four', doc.arr[2]); save(doc, function(err, doc) { assert.ifError(err); - assert.equal(3, doc.arr.length); + assert.equal(doc.arr.length, 3); assert.strictEqual('three', doc.arr[0]); assert.strictEqual('two', doc.arr[1]); assert.strictEqual('four', doc.arr[2]); @@ -1497,14 +1593,14 @@ describe('types array', function() { var db = start(); var D = db.model('subDocPositions', new Schema({ - em1: [new Schema({ name: String })] + em1: [new Schema({name: String})] })); var d = new D({ em1: [ - { name: 'pos0' } - , { name: 'pos1' } - , { name: 'pos2' } + {name: 'pos0'}, + {name: 'pos1'}, + {name: 'pos2'} ] }); @@ -1526,8 +1622,8 @@ describe('types array', function() { D.findById(d, function(err, d) { db.close(); assert.ifError(err); - assert.equal(d.em1[0].name,'position two'); - assert.equal(d.em1[1].name,'pos1'); + assert.equal(d.em1[0].name, 'position two'); + assert.equal(d.em1[1].name, 'pos1'); done(); }); }); @@ -1542,15 +1638,15 @@ describe('types array', function() { var D = db.model('similarPathNames', new Schema({ account: { - role: String - , roles: [String] - } - , em: [new Schema({ name: String })] + role: String, + roles: [String] + }, + em: [new Schema({name: String})] })); var d = new D({ - account: { role: 'teacher', roles: ['teacher', 'admin'] } - , em: [{ name: 'bob' }] + account: {role: 'teacher', roles: ['teacher', 'admin']}, + em: [{name: 'bob'}] }); d.save(function(err) { @@ -1561,14 +1657,14 @@ describe('types array', function() { d.account.role = 'president'; d.account.roles = ['president', 'janitor']; d.em[0].name = 'memorable'; - d.em = [{ name: 'frida' }]; + d.em = [{name: 'frida'}]; d.save(function(err) { assert.ifError(err); D.findById(d, function(err, d) { db.close(); assert.ifError(err); - assert.equal(d.account.role,'president'); + assert.equal(d.account.role, 'president'); assert.equal(d.account.roles.length, 2); assert.equal(d.account.roles[0], 'president'); assert.equal(d.account.roles[1], 'janitor'); @@ -1585,16 +1681,16 @@ describe('types array', function() { describe('of number', function() { it('allows nulls', function(done) { var db = start(); - var schema = new Schema({ x: [Number] }, { collection: 'nullsareallowed' + random() }); + var schema = new Schema({x: [Number]}, {collection: 'nullsareallowed' + random()}); var M = db.model('nullsareallowed', schema); var m; - m = new M({ x: [1, null, 3] }); + m = new M({x: [1, null, 3]}); m.save(function(err) { assert.ifError(err); // undefined is not allowed - m = new M({ x: [1, undefined, 3] }); + m = new M({x: [1, undefined, 3]}); m.save(function(err) { db.close(); assert.ok(err); @@ -1604,85 +1700,113 @@ describe('types array', function() { }); }); - it('modifying subdoc props and manipulating the array works (gh-842)', function(done) { - var db = start(); - var schema = new Schema({ em: [new Schema({ username: String })]}); - var M = db.model('modifyingSubDocAndPushing', schema); - var m = new M({ em: [ { username: 'Arrietty' }]}); + describe('bug fixes', function() { + var db; - m.save(function(err) { - assert.ifError(err); - M.findById(m, function(err, m) { - assert.ifError(err); - assert.equal(m.em[0].username, 'Arrietty'); + before(function() { + db = start(); + }); - m.em[0].username = 'Shawn'; - m.em.push({ username: 'Homily' }); - m.save(function(err) { + after(function(done) { + db.close(done); + }); + + it('modifying subdoc props and manipulating the array works (gh-842)', function(done) { + var schema = new Schema({em: [new Schema({username: String})]}); + var M = db.model('modifyingSubDocAndPushing', schema); + var m = new M({em: [{username: 'Arrietty'}]}); + + m.save(function(err) { + assert.ifError(err); + M.findById(m, function(err, m) { assert.ifError(err); + assert.equal(m.em[0].username, 'Arrietty'); - M.findById(m, function(err, m) { + m.em[0].username = 'Shawn'; + m.em.push({username: 'Homily'}); + m.save(function(err) { assert.ifError(err); - assert.equal(m.em.length, 2); - assert.equal(m.em[0].username, 'Shawn'); - assert.equal(m.em[1].username, 'Homily'); - m.em[0].username = 'Arrietty'; - m.em[1].remove(); - m.save(function(err) { + M.findById(m, function(err, m) { assert.ifError(err); + assert.equal(m.em.length, 2); + assert.equal(m.em[0].username, 'Shawn'); + assert.equal(m.em[1].username, 'Homily'); - M.findById(m, function(err, m) { - db.close(); + m.em[0].username = 'Arrietty'; + m.em[1].remove(); + m.save(function(err) { assert.ifError(err); - assert.equal(m.em.length, 1); - assert.equal(m.em[0].username, 'Arrietty'); - done(); + + M.findById(m, function(err, m) { + assert.ifError(err); + assert.equal(m.em.length, 1); + assert.equal(m.em[0].username, 'Arrietty'); + done(); + }); }); }); }); }); }); }); - }); - it('pushing top level arrays and subarrays works (gh-1073)', function(done) { - var db = start(); - var schema = new Schema({ em: [new Schema({ sub: [String] })]}); - var M = db.model('gh1073', schema); - var m = new M({ em: [ { sub: [] }]}); - m.save(function() { - M.findById(m, function(err, m) { - assert.ifError(err); - - m.em[m.em.length - 1].sub.push("a"); - m.em.push({ sub: [] }); + it('pushing top level arrays and subarrays works (gh-1073)', function(done) { + var schema = new Schema({em: [new Schema({sub: [String]})]}); + var M = db.model('gh1073', schema); + var m = new M({em: [{sub: []}]}); + m.save(function() { + M.findById(m, function(err, m) { + assert.ifError(err); - assert.equal(2, m.em.length); - assert.equal(1, m.em[0].sub.length); + m.em[m.em.length - 1].sub.push('a'); + m.em.push({sub: []}); - m.save(function(err) { - assert.ifError(err); + assert.equal(m.em.length, 2); + assert.equal(m.em[0].sub.length, 1); - M.findById(m, function(err, m) { + m.save(function(err) { assert.ifError(err); - assert.equal(2, m.em.length); - assert.equal(1, m.em[0].sub.length); - assert.equal('a', m.em[0].sub[0]); - db.close(done); + + M.findById(m, function(err, m) { + assert.ifError(err); + assert.equal(m.em.length, 2); + assert.equal(m.em[0].sub.length, 1); + assert.equal(m.em[0].sub[0], 'a'); + done(); + }); }); }); }); }); + + it('finding ids by string (gh-4011)', function(done) { + var sub = new Schema({ + _id: String, + other: String + }); + + var main = new Schema({ + subs: [sub] + }); + + var Model = db.model('gh4011', main); + + var doc = new Model({ subs: [{ _id: '57067021ee0870440c76f489' }] }); + + assert.ok(doc.subs.id('57067021ee0870440c76f489')); + assert.ok(doc.subs.id(new mongodb.ObjectId('57067021ee0870440c76f489'))); + done(); + }); }); describe('default type', function() { it('casts to Mixed', function(done) { - var db = start() - , DefaultArraySchema = new Schema({ - num1: Array - , num2: [] - }); + var db = start(), + DefaultArraySchema = new Schema({ + num1: Array, + num2: [] + }); mongoose.model('DefaultArraySchema', DefaultArraySchema); var DefaultArray = db.model('DefaultArraySchema', collection); @@ -1692,13 +1816,13 @@ describe('types array', function() { assert.equal(arr.get('num1').length, 0); assert.equal(arr.get('num2').length, 0); - var threw1 = false - , threw2 = false; + var threw1 = false, + threw2 = false; try { - arr.num1.push({ x: 1 }); + arr.num1.push({x: 1}); arr.num1.push(9); - arr.num1.push("woah"); + arr.num1.push('woah'); } catch (err) { threw1 = true; } @@ -1706,9 +1830,9 @@ describe('types array', function() { assert.equal(threw1, false); try { - arr.num2.push({ x: 1 }); + arr.num2.push({x: 1}); arr.num2.push(9); - arr.num2.push("woah"); + arr.num2.push('woah'); } catch (err) { threw2 = true; } @@ -1723,12 +1847,12 @@ describe('types array', function() { var B; before(function(done) { - var schema = Schema({ - numbers: ['number'] - , numberIds: [{ _id: 'number', name: 'string' }] - , stringIds: [{ _id: 'string', name: 'string' }] - , bufferIds: [{ _id: 'buffer', name: 'string' }] - , oidIds: [{ name: 'string' }] + var schema = new Schema({ + numbers: ['number'], + numberIds: [{_id: 'number', name: 'string'}], + stringIds: [{_id: 'string', name: 'string'}], + bufferIds: [{_id: 'buffer', name: 'string'}], + oidIds: [{name: 'string'}] }); db = start(); @@ -1765,7 +1889,7 @@ describe('types array', function() { B.findById(post._id, function(err, doc) { assert.ifError(err); - assert.equal(0, doc.numbers.length); + assert.equal(doc.numbers.length, 0); done(); }); }); @@ -1778,12 +1902,12 @@ describe('types array', function() { describe('with subdocs', function() { function docs(arr) { return arr.map(function(val) { - return { _id: val }; + return {_id: val}; }); } it('supports passing strings', function(done) { - var post = new B({ stringIds: docs('a b c d'.split(' ')) }); + var post = new B({stringIds: docs('a b c d'.split(' '))}); post.save(function(err) { assert.ifError(err); B.findById(post, function(err, post) { @@ -1793,7 +1917,7 @@ describe('types array', function() { assert.ifError(err); B.findById(post, function(err, post) { assert.ifError(err); - assert.equal(3, post.stringIds.length); + assert.equal(post.stringIds.length, 3); assert.ok(!post.stringIds.id('b')); done(); }); @@ -1802,17 +1926,17 @@ describe('types array', function() { }); }); it('supports passing numbers', function(done) { - var post = new B({ numberIds: docs([1,2,3,4]) }); + var post = new B({numberIds: docs([1, 2, 3, 4])}); post.save(function(err) { assert.ifError(err); B.findById(post, function(err, post) { assert.ifError(err); - post.numberIds.remove(2,4); + post.numberIds.remove(2, 4); post.save(function(err) { assert.ifError(err); B.findById(post, function(err, post) { assert.ifError(err); - assert.equal(2, post.numberIds.length); + assert.equal(post.numberIds.length, 2); assert.ok(!post.numberIds.id(2)); assert.ok(!post.numberIds.id(4)); done(); @@ -1826,17 +1950,17 @@ describe('types array', function() { var a = new OID; var b = new OID; var c = new OID; - var post = new B({ oidIds: docs([a,b,c]) }); + var post = new B({oidIds: docs([a, b, c])}); post.save(function(err) { assert.ifError(err); B.findById(post, function(err, post) { assert.ifError(err); - post.oidIds.remove(a,c); + post.oidIds.remove(a, c); post.save(function(err) { assert.ifError(err); B.findById(post, function(err, post) { assert.ifError(err); - assert.equal(1, post.oidIds.length); + assert.equal(post.oidIds.length, 1); assert.ok(!post.oidIds.id(a)); assert.ok(!post.oidIds.id(c)); done(); @@ -1846,7 +1970,7 @@ describe('types array', function() { }); }); it('supports passing buffers', function(done) { - var post = new B({ bufferIds: docs(['a','b','c','d']) }); + var post = new B({bufferIds: docs(['a', 'b', 'c', 'd'])}); post.save(function(err) { assert.ifError(err); B.findById(post, function(err, post) { @@ -1856,7 +1980,7 @@ describe('types array', function() { assert.ifError(err); B.findById(post, function(err, post) { assert.ifError(err); - assert.equal(3, post.bufferIds.length); + assert.equal(post.bufferIds.length, 3); assert.ok(!post.bufferIds.id(new Buffer('a'))); done(); }); diff --git a/test/types.buffer.test.js b/test/types.buffer.test.js index 8fbded796c3..20944461077 100644 --- a/test/types.buffer.test.js +++ b/test/types.buffer.test.js @@ -1,32 +1,18 @@ - /** * Module dependencies. */ -var start = require('./common') - , assert = require('assert') - , mongoose = require('./common').mongoose - , Schema = mongoose.Schema - , random = require('../lib/utils').random - , MongooseBuffer = mongoose.Types.Buffer; +var start = require('./common'), + assert = require('power-assert'), + mongoose = require('./common').mongoose, + Schema = mongoose.Schema, + random = require('../lib/utils').random, + MongooseBuffer = mongoose.Types.Buffer; function valid(v) { return !v || v.length > 10; } -var subBuf = new Schema({ - name: String - , buf: { type: Buffer, validate: [valid, 'valid failed'], required: true } -}); - -var UserBuffer = new Schema({ - name: String - , serial: Buffer - , array: [Buffer] - , required: { type: Buffer, required: true, index: true } - , sub: [subBuf] -}); - // Dont put indexed models on the default connection, it // breaks index.test.js tests on a "pure" default conn. // mongoose.model('UserBuffer', UserBuffer); @@ -36,6 +22,23 @@ var UserBuffer = new Schema({ */ describe('types.buffer', function() { + var subBuf; + var UserBuffer; + + before(function() { + subBuf = new Schema({ + name: String, + buf: {type: Buffer, validate: [valid, 'valid failed'], required: true} + }); + + UserBuffer = new Schema({ + name: String, + serial: Buffer, + array: [Buffer], + required: {type: Buffer, required: true, index: true}, + sub: [subBuf] + }); + }); it('test that a mongoose buffer behaves and quacks like a buffer', function(done) { var a = new MongooseBuffer; @@ -45,7 +48,7 @@ describe('types.buffer', function() { assert.equal(true, Buffer.isBuffer(a)); a = new MongooseBuffer([195, 188, 98, 101, 114]); - var b = new MongooseBuffer("buffer shtuffs are neat"); + var b = new MongooseBuffer('buffer shtuffs are neat'); var c = new MongooseBuffer('aGVsbG8gd29ybGQ=', 'base64'); var d = new MongooseBuffer(0); @@ -57,8 +60,8 @@ describe('types.buffer', function() { }); it('buffer validation', function(done) { - var db = start() - , User = db.model('UserBuffer', UserBuffer, 'usersbuffer_' + random()); + var db = start(), + User = db.model('UserBuffer', UserBuffer, 'usersbuffer_' + random()); User.on('index', function() { var t = new User({ @@ -66,29 +69,29 @@ describe('types.buffer', function() { }); t.validate(function(err) { - assert.equal(err.message,'UserBuffer validation failed'); - assert.equal(err.errors.required.kind,'required'); - t.required = {x:[20]}; + assert.ok(err.message.indexOf('UserBuffer validation failed') === 0, err.message); + assert.equal(err.errors.required.kind, 'required'); + t.required = {x: [20]}; t.save(function(err) { assert.ok(err); assert.equal(err.name, 'ValidationError'); assert.equal(err.errors.required.name, 'CastError'); assert.equal(err.errors.required.kind, 'Buffer'); - assert.equal(err.errors.required.message, 'Cast to Buffer failed for value "[object Object]" at path "required"'); - assert.deepEqual(err.errors.required.value, {x:[20]}); - t.required = new Buffer("hello"); + assert.equal(err.errors.required.message, 'Cast to Buffer failed for value "{ x: [ 20 ] }" at path "required"'); + assert.deepEqual(err.errors.required.value, {x: [20]}); + t.required = new Buffer('hello'); - t.sub.push({ name: 'Friday Friday' }); + t.sub.push({name: 'Friday Friday'}); t.save(function(err) { - assert.equal(err.message,'UserBuffer validation failed'); - assert.equal(err.errors['sub.0.buf'].kind,'required'); - t.sub[0].buf = new Buffer("well well"); + assert.ok(err.message.indexOf('UserBuffer validation failed') === 0, err.message); + assert.equal(err.errors['sub.0.buf'].kind, 'required'); + t.sub[0].buf = new Buffer('well well'); t.save(function(err) { - assert.equal(err.message,'UserBuffer validation failed'); - assert.equal(err.errors['sub.0.buf'].kind,'user defined'); - assert.equal(err.errors['sub.0.buf'].message,'valid failed'); + assert.ok(err.message.indexOf('UserBuffer validation failed') === 0, err.message); + assert.equal(err.errors['sub.0.buf'].kind, 'user defined'); + assert.equal(err.errors['sub.0.buf'].message, 'valid failed'); - t.sub[0].buf = new Buffer("well well well"); + t.sub[0].buf = new Buffer('well well well'); t.validate(function(err) { db.close(); assert.ifError(err); @@ -102,16 +105,16 @@ describe('types.buffer', function() { }); it('buffer storage', function(done) { - var db = start() - , User = db.model('UserBuffer', UserBuffer, 'usersbuffer_' + random()); + var db = start(), + User = db.model('UserBuffer', UserBuffer, 'usersbuffer_' + random()); User.on('index', function() { var sampleBuffer = new Buffer([123, 223, 23, 42, 11]); var tj = new User({ - name: 'tj' - , serial: sampleBuffer - , required: new Buffer(sampleBuffer) + name: 'tj', + serial: sampleBuffer, + required: new Buffer(sampleBuffer) }); tj.save(function(err) { @@ -123,9 +126,9 @@ describe('types.buffer', function() { var user = users[0]; var base64 = sampleBuffer.toString('base64'); assert.equal(base64, - user.serial.toString('base64'), 'buffer mismatch'); + user.serial.toString('base64'), 'buffer mismatch'); assert.equal(base64, - user.required.toString('base64'), 'buffer mismatch'); + user.required.toString('base64'), 'buffer mismatch'); done(); }); }); @@ -133,16 +136,16 @@ describe('types.buffer', function() { }); it('test write markModified', function(done) { - var db = start() - , User = db.model('UserBuffer', UserBuffer, 'usersbuffer_' + random()); + var db = start(), + User = db.model('UserBuffer', UserBuffer, 'usersbuffer_' + random()); User.on('index', function() { var sampleBuffer = new Buffer([123, 223, 23, 42, 11]); var tj = new User({ - name: 'tj' - , serial: sampleBuffer - , required: sampleBuffer + name: 'tj', + serial: sampleBuffer, + required: sampleBuffer }); tj.save(function(err) { @@ -161,7 +164,7 @@ describe('types.buffer', function() { var expectedBuffer = new Buffer([123, 97, 97, 42, 11]); assert.equal(expectedBuffer.toString('base64'), - user.serial.toString('base64'), 'buffer mismatch'); + user.serial.toString('base64'), 'buffer mismatch'); assert.equal(false, tj.isModified('required')); tj.serial.copy(tj.required, 1); @@ -178,169 +181,170 @@ describe('types.buffer', function() { // buffer method tests var fns = { - 'writeUInt8': function() { + writeUInt8: function() { reset(tj); not(tj); tj.required.writeUInt8(0x3, 0, 'big'); is(tj); - } - , 'writeUInt16': function() { - reset(tj); - not(tj); - tj.required.writeUInt16(0xbeef, 0, 'little'); - is(tj); - } - , 'writeUInt16LE': function() { - reset(tj); - not(tj); - tj.required.writeUInt16LE(0xbeef, 0); - is(tj); - } - , 'writeUInt16BE': function() { - reset(tj); - not(tj); - tj.required.writeUInt16BE(0xbeef, 0); - is(tj); - } - , 'writeUInt32': function() { - reset(tj); - not(tj); - tj.required.writeUInt32(0xfeedface, 0, 'little'); - is(tj); - } - , 'writeUInt32LE': function() { - reset(tj); - not(tj); - tj.required.writeUInt32LE(0xfeedface, 0); - is(tj); - } - , 'writeUInt32BE': function() { - reset(tj); - not(tj); - tj.required.writeUInt32BE(0xfeedface, 0); - is(tj); - } - , 'writeInt8': function() { - reset(tj); - not(tj); - tj.required.writeInt8(-5, 0, 'big'); - is(tj); - } - , 'writeInt16': function() { - reset(tj); - not(tj); - tj.required.writeInt16(0x0023, 2, 'little'); - is(tj); - assert.equal(tj.required[2], 0x23); - assert.equal(tj.required[3], 0x00); - } - , 'writeInt16LE': function() { - reset(tj); - not(tj); - tj.required.writeInt16LE(0x0023, 2); - is(tj); - assert.equal(tj.required[2], 0x23); - assert.equal(tj.required[3], 0x00); - } - , 'writeInt16BE': function() { - reset(tj); - not(tj); - tj.required.writeInt16BE(0x0023, 2); - is(tj); - } - , 'writeInt32': function() { - reset(tj); - not(tj); - tj.required.writeInt32(0x23, 0, 'big'); - is(tj); - assert.equal(tj.required[0], 0x00); - assert.equal(tj.required[1], 0x00); - assert.equal(tj.required[2], 0x00); - assert.equal(tj.required[3], 0x23); - tj.required = new Buffer(8); - } - , 'writeInt32LE': function() { - tj.required = new Buffer(8); - reset(tj); - not(tj); - tj.required.writeInt32LE(0x23, 0); - is(tj); - } - , 'writeInt32BE': function() { - tj.required = new Buffer(8); - reset(tj); - not(tj); - tj.required.writeInt32BE(0x23, 0); - is(tj); - assert.equal(tj.required[0], 0x00); - assert.equal(tj.required[1], 0x00); - assert.equal(tj.required[2], 0x00); - assert.equal(tj.required[3], 0x23); - } - , 'writeFloat': function() { - tj.required = new Buffer(16); - reset(tj); - not(tj); - tj.required.writeFloat(2.225073858507201e-308, 0, 'big'); - is(tj); - assert.equal(tj.required[0], 0x00); - assert.equal(tj.required[1], 0x0f); - assert.equal(tj.required[2], 0xff); - assert.equal(tj.required[3], 0xff); - assert.equal(tj.required[4], 0xff); - assert.equal(tj.required[5], 0xff); - assert.equal(tj.required[6], 0xff); - assert.equal(tj.required[7], 0xff); - } - , 'writeFloatLE': function() { - tj.required = new Buffer(16); - reset(tj); - not(tj); - tj.required.writeFloatLE(2.225073858507201e-308, 0); - is(tj); - } - , 'writeFloatBE': function() { - tj.required = new Buffer(16); - reset(tj); - not(tj); - tj.required.writeFloatBE(2.225073858507201e-308, 0); - is(tj); - } - , 'writeDoubleLE': function() { - tj.required = new Buffer(8); - reset(tj); - not(tj); - tj.required.writeDoubleLE(0xdeadbeefcafebabe, 0); - is(tj); - } - , 'writeDoubleBE': function() { - tj.required = new Buffer(8); - reset(tj); - not(tj); - tj.required.writeDoubleBE(0xdeadbeefcafebabe, 0); - is(tj); - } - , 'fill': function() { - tj.required = new Buffer(8); - reset(tj); - not(tj); - tj.required.fill(0); - is(tj); - for (var i = 0; i < tj.required.length; i++) { - assert.strictEqual(tj.required[i], 0); - } - } - , 'set': function() { - reset(tj); - not(tj); - tj.required.set(0, 1); - is(tj); + }, + writeUInt16: function() { + reset(tj); + not(tj); + tj.required.writeUInt16(0xbeef, 0, 'little'); + is(tj); + }, + writeUInt16LE: function() { + reset(tj); + not(tj); + tj.required.writeUInt16LE(0xbeef, 0); + is(tj); + }, + writeUInt16BE: function() { + reset(tj); + not(tj); + tj.required.writeUInt16BE(0xbeef, 0); + is(tj); + }, + writeUInt32: function() { + reset(tj); + not(tj); + tj.required.writeUInt32(0xfeedface, 0, 'little'); + is(tj); + }, + writeUInt32LE: function() { + reset(tj); + not(tj); + tj.required.writeUInt32LE(0xfeedface, 0); + is(tj); + }, + writeUInt32BE: function() { + reset(tj); + not(tj); + tj.required.writeUInt32BE(0xfeedface, 0); + is(tj); + }, + writeInt8: function() { + reset(tj); + not(tj); + tj.required.writeInt8(-5, 0, 'big'); + is(tj); + }, + writeInt16: function() { + reset(tj); + not(tj); + tj.required.writeInt16(0x0023, 2, 'little'); + is(tj); + assert.equal(tj.required[2], 0x23); + assert.equal(tj.required[3], 0x00); + }, + writeInt16LE: function() { + reset(tj); + not(tj); + tj.required.writeInt16LE(0x0023, 2); + is(tj); + assert.equal(tj.required[2], 0x23); + assert.equal(tj.required[3], 0x00); + }, + writeInt16BE: function() { + reset(tj); + not(tj); + tj.required.writeInt16BE(0x0023, 2); + is(tj); + }, + writeInt32: function() { + reset(tj); + not(tj); + tj.required.writeInt32(0x23, 0, 'big'); + is(tj); + assert.equal(tj.required[0], 0x00); + assert.equal(tj.required[1], 0x00); + assert.equal(tj.required[2], 0x00); + assert.equal(tj.required[3], 0x23); + tj.required = new Buffer(8); + }, + writeInt32LE: function() { + tj.required = new Buffer(8); + reset(tj); + not(tj); + tj.required.writeInt32LE(0x23, 0); + is(tj); + }, + writeInt32BE: function() { + tj.required = new Buffer(8); + reset(tj); + not(tj); + tj.required.writeInt32BE(0x23, 0); + is(tj); + assert.equal(tj.required[0], 0x00); + assert.equal(tj.required[1], 0x00); + assert.equal(tj.required[2], 0x00); + assert.equal(tj.required[3], 0x23); + }, + writeFloat: function() { + tj.required = new Buffer(16); + reset(tj); + not(tj); + tj.required.writeFloat(2.225073858507201e-308, 0, 'big'); + is(tj); + assert.equal(tj.required[0], 0x00); + assert.equal(tj.required[1], 0x0f); + assert.equal(tj.required[2], 0xff); + assert.equal(tj.required[3], 0xff); + assert.equal(tj.required[4], 0xff); + assert.equal(tj.required[5], 0xff); + assert.equal(tj.required[6], 0xff); + assert.equal(tj.required[7], 0xff); + }, + writeFloatLE: function() { + tj.required = new Buffer(16); + reset(tj); + not(tj); + tj.required.writeFloatLE(2.225073858507201e-308, 0); + is(tj); + }, + writeFloatBE: function() { + tj.required = new Buffer(16); + reset(tj); + not(tj); + tj.required.writeFloatBE(2.225073858507201e-308, 0); + is(tj); + }, + writeDoubleLE: function() { + tj.required = new Buffer(8); + reset(tj); + not(tj); + tj.required.writeDoubleLE(0xdeadbeefcafebabe, 0); + is(tj); + }, + writeDoubleBE: function() { + tj.required = new Buffer(8); + reset(tj); + not(tj); + tj.required.writeDoubleBE(0xdeadbeefcafebabe, 0); + is(tj); + }, + fill: function() { + tj.required = new Buffer(8); + reset(tj); + not(tj); + tj.required.fill(0); + is(tj); + for (var i = 0; i < tj.required.length; i++) { + assert.strictEqual(tj.required[i], 0); } + }, + set: function() { + reset(tj); + not(tj); + tj.required[0] = 1; + tj.markModified('required'); + is(tj); + } }; - var keys = Object.keys(fns) - , i = keys.length - , key; + var keys = Object.keys(fns), + i = keys.length, + key; while (i--) { key = keys[i]; @@ -364,20 +368,34 @@ describe('types.buffer', function() { }); it('can be set to null', function(done) { - var db = start() - , User = db.model('UserBuffer', UserBuffer, 'usersbuffer_' + random()); - var user = new User({ array: [null], required: new Buffer(1) }); + var db = start(), + User = db.model('UserBuffer', UserBuffer, 'usersbuffer_' + random()); + var user = new User({array: [null], required: new Buffer(1)}); user.save(function(err, doc) { assert.ifError(err); User.findById(doc, function(err, doc) { db.close(); assert.ifError(err); - assert.equal(1, doc.array.length); - assert.equal(null, doc.array[0]); + assert.equal(doc.array.length, 1); + assert.equal(doc.array[0], null); done(); }); }); + }); + it('can be updated to null', function(done) { + var db = start(), + User = db.model('UserBuffer', UserBuffer, 'usersbuffer_' + random()); + var user = new User({array: [null], required: new Buffer(1), serial: new Buffer(1)}); + user.save(function(err, doc) { + assert.ifError(err); + User.findOneAndUpdate({_id: doc.id}, {serial: null}, {new: true}, function(err, doc) { + db.close(); + assert.ifError(err); + assert.equal(doc.serial, null); + done(); + }); + }); }); describe('#toObject', function() { @@ -395,7 +413,7 @@ describe('types.buffer', function() { before(function(done) { db = start(); - bufferSchema = new Schema({ buf: Buffer }); + bufferSchema = new Schema({buf: Buffer}); B = db.model('1571', bufferSchema); done(); }); @@ -405,44 +423,62 @@ describe('types.buffer', function() { }); it('default value', function(done) { - var b = new B({ buf: new Buffer('hi') }); + var b = new B({buf: new Buffer('hi')}); assert.strictEqual(0, b.buf._subtype); done(); }); it('method works', function(done) { - var b = new B({ buf: new Buffer('hi') }); + var b = new B({buf: new Buffer('hi')}); b.buf.subtype(128); assert.strictEqual(128, b.buf._subtype); done(); }); it('is stored', function(done) { - var b = new B({ buf: new Buffer('hi') }); + var b = new B({buf: new Buffer('hi')}); b.buf.subtype(128); b.save(function(err) { - if (err) return done(err); + if (err) { + done(err); + return; + } B.findById(b, function(err, doc) { - if (err) return done(err); - assert.equal(128, doc.buf._subtype); + if (err) { + done(err); + return; + } + assert.equal(doc.buf._subtype, 128); done(); }); }); }); it('changes are retained', function(done) { - var b = new B({ buf: new Buffer('hi') }); + var b = new B({buf: new Buffer('hi')}); b.buf.subtype(128); b.save(function(err) { - if (err) return done(err); + if (err) { + done(err); + return; + } B.findById(b, function(err, doc) { - if (err) return done(err); - assert.equal(128, doc.buf._subtype); + if (err) { + done(err); + return; + } + assert.equal(doc.buf._subtype, 128); doc.buf.subtype(0); doc.save(function(err) { - if (err) return done(err); + if (err) { + done(err); + return; + } B.findById(b, function(err, doc) { - if (err) return done(err); + if (err) { + done(err); + return; + } assert.strictEqual(0, doc.buf._subtype); done(); }); @@ -450,5 +486,14 @@ describe('types.buffer', function() { }); }); }); + + it('cast from number (gh-3764)', function(done) { + var schema = new Schema({buf: Buffer}); + var MyModel = mongoose.model('gh3764', schema); + + var doc = new MyModel({buf: 9001}); + assert.equal(doc.buf.length, 1); + done(); + }); }); }); diff --git a/test/types.document.test.js b/test/types.document.test.js index 51d6156f09a..29329778fbf 100644 --- a/test/types.document.test.js +++ b/test/types.document.test.js @@ -3,68 +3,65 @@ * Module dependencies. */ -var assert = require('assert') - , start = require('./common') - , mongoose = start.mongoose - , EmbeddedDocument = require('../lib/types/embedded') - , DocumentArray = require('../lib/types/documentarray') - , Schema = mongoose.Schema - , ValidationError = mongoose.Document.ValidationError; - -/** - * Setup. - */ - -function Dummy() { - mongoose.Document.call(this, {}); -} -Dummy.prototype.__proto__ = mongoose.Document.prototype; -Dummy.prototype.$__setSchema(new Schema); - -function Subdocument() { - var arr = new DocumentArray; - arr._path = 'jsconf.ar'; - arr._parent = new Dummy; - arr[0] = this; - EmbeddedDocument.call(this, {}, arr); -} - -/** - * Inherits from EmbeddedDocument. - */ - -Subdocument.prototype.__proto__ = EmbeddedDocument.prototype; - -/** - * Set schema. - */ - -Subdocument.prototype.$__setSchema(new Schema({ - test: { type: String, required: true } - , work: { type: String, validate: /^good/ } -})); - -/** - * Schema. - */ - -var RatingSchema = new Schema({ - stars: Number - , description: { source: { url: String, time: Date }} -}); - -var MovieSchema = new Schema({ - title: String - , ratings: [RatingSchema] -}); - -mongoose.model('Movie', MovieSchema); +var assert = require('power-assert'); +var start = require('./common'); +var mongoose = start.mongoose; +var EmbeddedDocument = require('../lib/types/embedded'); +var EventEmitter = require('events').EventEmitter; +var DocumentArray = require('../lib/types/documentarray'); +var Schema = mongoose.Schema; +var ValidationError = mongoose.Document.ValidationError; /** * Test. */ describe('types.document', function() { + var Dummy; + var Subdocument; + var RatingSchema; + var MovieSchema; + + before(function() { + function _Dummy() { + mongoose.Document.call(this, {}); + } + Dummy = _Dummy; + Dummy.prototype.__proto__ = mongoose.Document.prototype; + Dummy.prototype.$__setSchema(new Schema); + + function _Subdocument() { + var arr = new DocumentArray; + arr._path = 'jsconf.ar'; + arr._parent = new Dummy; + arr[0] = this; + EmbeddedDocument.call(this, {}, arr); + } + Subdocument = _Subdocument; + + Subdocument.prototype.__proto__ = EmbeddedDocument.prototype; + + for (var i in EventEmitter.prototype) { + Subdocument[i] = EventEmitter.prototype[i]; + } + + Subdocument.prototype.$__setSchema(new Schema({ + test: {type: String, required: true}, + work: {type: String, validate: /^good/} + })); + + RatingSchema = new Schema({ + stars: Number, + description: {source: {url: String, time: Date}} + }); + + MovieSchema = new Schema({ + title: String, + ratings: [RatingSchema] + }); + + mongoose.model('Movie', MovieSchema); + }); it('test that validate sets errors', function(done) { var a = new Subdocument(); @@ -75,14 +72,13 @@ describe('types.document', function() { a.validate(function() { assert.ok(a.__parent.$__.validationError instanceof ValidationError); assert.equal(a.__parent.errors['jsconf.ar.0.work'].name, 'ValidatorError'); - assert.equal(a.__parent.$__.validationError.toString(), 'ValidationError: Path `test` is required., Validator failed for path `work` with value `nope`'); done(); }); }); it('objects can be passed to #set', function(done) { var a = new Subdocument(); - a.set({ test: 'paradiddle', work: 'good flam'}); + a.set({test: 'paradiddle', work: 'good flam'}); assert.equal(a.test, 'paradiddle'); assert.equal(a.work, 'good flam'); done(); @@ -90,7 +86,7 @@ describe('types.document', function() { it('Subdocuments can be passed to #set', function(done) { var a = new Subdocument(); - a.set({ test: 'paradiddle', work: 'good flam'}); + a.set({test: 'paradiddle', work: 'good flam'}); assert.equal(a.test, 'paradiddle'); assert.equal(a.work, 'good flam'); var b = new Subdocument(); @@ -113,7 +109,7 @@ describe('types.document', function() { var m2 = new Movie; delete m2._doc._id; - m2.init({ _id: new mongoose.Types.ObjectId }); + m2.init({_id: new mongoose.Types.ObjectId}); assert.equal(m2.id, m2.$__._id); assert.strictEqual(true, m.$__._id !== m2.$__._id); assert.strictEqual(true, m.id !== m2.id); @@ -125,17 +121,17 @@ describe('types.document', function() { var db = start(); var Movie = db.model('Movie'); - var super8 = new Movie({ title: 'Super 8' }); + var super8 = new Movie({title: 'Super 8'}); var id1 = '4e3d5fc7da5d7eb635063c96'; var id2 = '4e3d5fc7da5d7eb635063c97'; var id3 = '4e3d5fc7da5d7eb635063c98'; var id4 = '4e3d5fc7da5d7eb635063c99'; - super8.ratings.push({ stars: 9, _id: id1 }); - super8.ratings.push({ stars: 8, _id: id2 }); - super8.ratings.push({ stars: 7, _id: id3 }); - super8.ratings.push({ stars: 6, _id: id4 }); + super8.ratings.push({stars: 9, _id: id1}); + super8.ratings.push({stars: 8, _id: id2}); + super8.ratings.push({stars: 7, _id: id3}); + super8.ratings.push({stars: 6, _id: id4}); super8.save(function(err) { assert.ifError(err); @@ -158,7 +154,7 @@ describe('types.document', function() { assert.ifError(err); assert.equal(movie.title, 'Super 8'); - assert.equal(movie.ratings.length,3); + assert.equal(movie.ratings.length, 3); assert.equal(movie.ratings.id(id1).stars.valueOf(), 5); assert.equal(movie.ratings.id(id3).stars.valueOf(), 4); assert.equal(movie.ratings.id(id4).stars.valueOf(), 3); @@ -172,7 +168,7 @@ describe('types.document', function() { Movie.findById(super8._id, function(err, movie) { assert.ifError(err); - assert.equal(movie.ratings.length,2); + assert.equal(movie.ratings.length, 2); assert.equal(movie.ratings.id(id1).stars.valueOf(), 2); assert.equal(movie.ratings.id(id4).stars.valueOf(), 1); @@ -182,7 +178,7 @@ describe('types.document', function() { movie.save(function() { Movie.findById(super8._id, function(err, movie) { assert.ifError(err); - assert.equal(0, movie.ratings.length); + assert.equal(movie.ratings.length, 0); db.close(done); }); }); @@ -199,12 +195,12 @@ describe('types.document', function() { var Movie = db.model('Movie'); Movie.create({ - title: 'Life of Pi' - , ratings: [{ + title: 'Life of Pi', + ratings: [{ description: { source: { - url: 'http://www.imdb.com/title/tt0454876/' - , time: new Date + url: 'http://www.imdb.com/title/tt0454876/', + time: new Date } } }] @@ -215,7 +211,7 @@ describe('types.document', function() { assert.ifError(err); assert.ok(movie.ratings[0].description.source.time instanceof Date); - movie.ratings[0].description.source = { url: 'http://www.lifeofpimovie.com/' }; + movie.ratings[0].description.source = {url: 'http://www.lifeofpimovie.com/'}; movie.save(function(err) { assert.ifError(err); @@ -229,7 +225,7 @@ describe('types.document', function() { assert.equal(undefined, movie.ratings[0].description.source.time); var newDate = new Date; - movie.ratings[0].set('description.source.time', newDate, { merge: true }); + movie.ratings[0].set('description.source.time', newDate, {merge: true}); movie.save(function(err) { assert.ifError(err); @@ -247,5 +243,4 @@ describe('types.document', function() { }); }); }); - }); diff --git a/test/types.documentarray.test.js b/test/types.documentarray.test.js index faa588303bb..0a9cc16052d 100644 --- a/test/types.documentarray.test.js +++ b/test/types.documentarray.test.js @@ -1,19 +1,17 @@ - /** * Module dependencies. */ -var start = require('./common') - , mongoose = require('./common').mongoose - , random = require('../lib/utils').random - , setValue = require('../lib/utils').setValue - , MongooseArray = mongoose.Types.Array - , MongooseDocumentArray = mongoose.Types.DocumentArray - , EmbeddedDocument = require('../lib/types/embedded') - , DocumentArray = require('../lib/types/documentarray') - , Schema = mongoose.Schema - , assert = require('assert') - , collection = 'types.documentarray_' + random(); +var start = require('./common'); +var mongoose = require('./common').mongoose; +var random = require('../lib/utils').random; +var setValue = require('../lib/utils').setValue; +var MongooseDocumentArray = mongoose.Types.DocumentArray; +var EmbeddedDocument = require('../lib/types/embedded'); +var DocumentArray = require('../lib/types/documentarray'); +var Schema = mongoose.Schema; +var assert = require('power-assert'); +var collection = 'types.documentarray_' + random(); /** * Setup. @@ -35,7 +33,7 @@ function TestDoc(schema) { */ var SubSchema = new Schema({ - title: { type: String } + title: {type: String} }); Subdocument.prototype.$__setSchema(schema || SubSchema); @@ -55,12 +53,9 @@ describe('types.documentarray', function() { assert.ok(a.isMongooseArray); assert.ok(a.isMongooseDocumentArray); assert.ok(Array.isArray(a)); - assert.equal('Object', a._atomics.constructor.name); - assert.equal('object', typeof a); - var b = new MongooseArray([1,2,3,4]); - assert.equal('object', typeof b); - assert.equal(Object.keys(b.toObject()).length,4); + assert.deepEqual(a._atomics.constructor, Object); + done(); }); @@ -77,8 +72,8 @@ describe('types.documentarray', function() { // test with custom string _id var Custom = new Schema({ - title: { type: String } - , _id: { type: String, required: true } + title: {type: String}, + _id: {type: String, required: true} }); Subdocument = TestDoc(Custom); @@ -94,8 +89,8 @@ describe('types.documentarray', function() { // test with custom number _id var CustNumber = new Schema({ - title: { type: String } - , _id: { type: Number, required: true } + title: {type: String}, + _id: {type: Number, required: true} }); Subdocument = TestDoc(CustNumber); @@ -111,8 +106,8 @@ describe('types.documentarray', function() { // test with object as _id Custom = new Schema({ - title: { type: String } - , _id: { one: { type: String }, two: { type: String } } + title: {type: String}, + _id: {one: {type: String}, two: {type: String}} }); Subdocument = TestDoc(Custom); @@ -125,14 +120,14 @@ describe('types.documentarray', function() { sub2._id = {one: 'rock', two: 'roll'}; sub2.title = 'rock-n-roll'; - a = new MongooseDocumentArray([sub1,sub2]); + a = new MongooseDocumentArray([sub1, sub2]); assert.notEqual(a.id({one: 'rolling', two: 'rock'}).title, 'rock-n-roll'); assert.equal(a.id({one: 'rock', two: 'roll'}).title, 'rock-n-roll'); // test with no _id var NoId = new Schema({ - title: { type: String } - }, { noId: true }); + title: {type: String} + }, {noId: true}); Subdocument = TestDoc(NoId); @@ -146,36 +141,36 @@ describe('types.documentarray', function() { } catch (err) { threw = err; } - assert.equal(false, threw); + assert.equal(threw, false); // test the _id option, noId is deprecated NoId = new Schema({ - title: { type: String } - }, { _id: false }); + title: {type: String} + }, {_id: false}); Subdocument = TestDoc(NoId); sub4 = new Subdocument(); sub4.title = 'rock-n-roll'; - a = new MongooseDocumentArray([sub4]) - , threw = false; + a = new MongooseDocumentArray([sub4]); + threw = false; try { a.id('i better not throw'); } catch (err) { threw = err; } - assert.equal(false, threw); + assert.equal(threw, false); // undefined and null should not match a nonexistent _id assert.strictEqual(null, a.id(undefined)); assert.strictEqual(null, a.id(null)); // test when _id is a populated document Custom = new Schema({ - title: { type: String } + title: {type: String} }); - var Custom1 = new Schema({}, { id: false }); + var Custom1 = new Schema({}, {id: false}); Subdocument = TestDoc(Custom); var Subdocument1 = TestDoc(Custom1); @@ -184,7 +179,7 @@ describe('types.documentarray', function() { sub1 = new Subdocument1(); sub.title = 'Hello again to all my friends'; id = sub1._id.toString(); - setValue('_id', sub1 , sub); + setValue('_id', sub1, sub); a = new MongooseDocumentArray([sub]); assert.equal(a.id(id).title, 'Hello again to all my friends'); @@ -220,9 +215,9 @@ describe('types.documentarray', function() { assert.ok(!threw); done(); }); - it('passes options to its documents (gh-1415)', function(done) { + it('passes options to its documents (gh-1415) (gh-4455)', function(done) { var subSchema = new Schema({ - title: { type: String } + title: {type: String} }); subSchema.set('toObject', { @@ -235,11 +230,20 @@ describe('types.documentarray', function() { }); var db = mongoose.createConnection(); - var M = db.model('gh-1415', { docs: [subSchema] }); + var M = db.model('gh-1415', {docs: [subSchema]}); var m = new M; - m.docs.push({ docs: [{ title: 'hello' }] }); + m.docs.push({docs: [{title: 'hello'}]}); var delta = m.$__delta()[1]; - assert.equal(undefined, delta.$pushAll.docs[0].changed); + assert.equal(delta.$pushAll.docs[0].changed, undefined); + + M = db.model('gh-1415-1', new Schema({docs: [subSchema]}, { + usePushEach: true + })); + m = new M; + m.docs.push({docs: [{title: 'hello'}]}); + delta = m.$__delta()[1]; + assert.equal(delta.$push.docs.$each[0].changed, undefined); + done(); }); it('uses the correct transform (gh-1412)', function(done) { @@ -267,8 +271,7 @@ describe('types.documentarray', function() { var First = db.model('first', FirstSchema); var Second = db.model('second', SecondSchema); - var first = new First({ - }); + var first = new First({}); first.second.push(new Second()); first.second.push(new Second()); @@ -286,13 +289,13 @@ describe('types.documentarray', function() { describe('create()', function() { it('works', function(done) { var a = new MongooseDocumentArray([]); - assert.equal('function', typeof a.create); + assert.equal(typeof a.create, 'function'); - var schema = new Schema({ docs: [new Schema({ name: 'string' })] }); + var schema = new Schema({docs: [new Schema({name: 'string'})]}); var T = mongoose.model('embeddedDocument#create_test', schema, 'asdfasdfa' + random()); var t = new T; - assert.equal('function', typeof t.docs.create); - var subdoc = t.docs.create({ name: 100 }); + assert.equal(typeof t.docs.create, 'function'); + var subdoc = t.docs.create({name: 100}); assert.ok(subdoc._id); assert.equal(subdoc.name, '100'); assert.ok(subdoc instanceof EmbeddedDocument); @@ -304,22 +307,22 @@ describe('types.documentarray', function() { it('does not re-cast instances of its embedded doc', function(done) { var db = start(); - var child = new Schema({ name: String, date: Date }); + var child = new Schema({name: String, date: Date}); child.pre('save', function(next) { this.date = new Date; next(); }); - var schema = Schema({ children: [child] }); + var schema = new Schema({children: [child]}); var M = db.model('embeddedDocArray-push-re-cast', schema, 'edarecast-' + random()); var m = new M; m.save(function(err) { assert.ifError(err); M.findById(m._id, function(err, doc) { assert.ifError(err); - var c = doc.children.create({ name: 'first' }); - assert.equal(undefined, c.date); + var c = doc.children.create({name: 'first'}); + assert.equal(c.date, undefined); doc.children.push(c); - assert.equal(undefined, c.date); + assert.equal(c.date, undefined); doc.save(function(err) { assert.ifError(err); assert.ok(doc.children[doc.children.length - 1].date); @@ -332,7 +335,7 @@ describe('types.documentarray', function() { assert.ifError(err); M.findById(m._id, function(err, doc) { assert.ifError(err); - assert.equal(3, doc.children.length); + assert.equal(doc.children.length, 3); doc.children.forEach(function(child) { assert.equal(doc.children[0].id, child.id); }); @@ -345,9 +348,9 @@ describe('types.documentarray', function() { }); it('corrects #ownerDocument() if value was created with array.create() (gh-1385)', function(done) { var mg = new mongoose.Mongoose; - var M = mg.model('1385', { docs: [{ name: String }] }); + var M = mg.model('1385', {docs: [{name: String}]}); var m = new M; - var doc = m.docs.create({ name: 'test 1385' }); + var doc = m.docs.create({name: 'test 1385'}); assert.notEqual(String(doc.ownerDocument()._id), String(m._id)); m.docs.push(doc); assert.equal(doc.ownerDocument()._id, String(m._id)); @@ -358,21 +361,21 @@ describe('types.documentarray', function() { it('#push should work on EmbeddedDocuments more than 2 levels deep', function(done) { var Comments = new Schema; Comments.add({ - title : String - , comments : [Comments] + title: String, + comments: [Comments] }); var BlogPost = new Schema({ - title : String - , comments : [Comments] + title: String, + comments: [Comments] }); - var db = start() - , Post = db.model('docarray-BlogPost', BlogPost, collection); + var db = start(), + Post = db.model('docarray-BlogPost', BlogPost, collection); - var p = new Post({ title: "comment nesting" }); - var c1 = p.comments.create({ title: "c1" }); - var c2 = p.comments.create({ title: "c2" }); - var c3 = p.comments.create({ title: "c3" }); + var p = new Post({title: 'comment nesting'}); + var c1 = p.comments.create({title: 'c1'}); + var c2 = c1.comments.create({title: 'c2'}); + var c3 = c2.comments.create({title: 'c3'}); p.comments.push(c1); c1.comments.push(c2); @@ -384,8 +387,7 @@ describe('types.documentarray', function() { Post.findById(p._id, function(err, p) { assert.ifError(err); - var c4 = p.comments.create({ title: "c4" }); - p.comments[0].comments[0].comments[0].comments.push(c4); + p.comments[0].comments[0].comments[0].comments.push({title: 'c4'}); p.save(function(err) { assert.ifError(err); @@ -401,7 +403,7 @@ describe('types.documentarray', function() { describe('invalidate()', function() { it('works', function(done) { - var schema = Schema({ docs: [{ name: 'string' }] }); + var schema = new Schema({docs: [{name: 'string'}]}); schema.pre('validate', function(next) { var subdoc = this.docs[this.docs.length - 1]; subdoc.invalidate('name', 'boo boo', '%'); @@ -409,13 +411,13 @@ describe('types.documentarray', function() { }); var T = mongoose.model('embeddedDocument#invalidate_test', schema, 'asdfasdfa' + random()); var t = new T; - t.docs.push({ name: 100 }); + t.docs.push({name: 100}); - var subdoc = t.docs.create({ name: 'yep' }); + var subdoc = t.docs.create({name: 'yep'}); assert.throws(function() { // has no parent array subdoc.invalidate('name', 'junk', 47); - }, /^Error: Unable to invalidate a subdocument/); + }); t.validate(function() { var e = t.errors['docs.0.name']; assert.ok(e); @@ -429,31 +431,31 @@ describe('types.documentarray', function() { it('handles validation failures', function(done) { var db = start(); - var nested = Schema({ v: { type: Number, max: 30 }}); - var schema = Schema({ + var nested = new Schema({v: {type: Number, max: 30}}); + var schema = new Schema({ docs: [nested] - }, { collection: 'embedded-invalidate-' + random() }); + }, {collection: 'embedded-invalidate-' + random()}); var M = db.model('embedded-invalidate', schema); - var m = new M({ docs: [{ v: 900 }] }); + var m = new M({docs: [{v: 900}]}); m.save(function(err) { - assert.equal(900, err.errors['docs.0.v'].value); + assert.equal(err.errors['docs.0.v'].value, 900); db.close(done); }); }); it('removes attached event listeners when creating new doc array', function(done) { var db = start(); - var nested = Schema({ v: { type: Number }}); - var schema = Schema({ + var nested = new Schema({v: {type: Number}}); + var schema = new Schema({ docs: [nested] - }, { collection: 'gh-2159' }); + }, {collection: 'gh-2159'}); var M = db.model('gh-2159', schema); - M.create({ docs: [{v: 900}] }, function(error, m) { + M.create({docs: [{v: 900}]}, function(error, m) { m.shouldPrint = true; assert.ifError(error); var numListeners = m.listeners('save').length; assert.ok(numListeners > 0); - m.docs = [{ v: 9000 }]; + m.docs = [{v: 9000}]; m.save(function(error, m) { assert.ifError(error); assert.equal(numListeners, m.listeners('save').length); diff --git a/test/types.embeddeddocument.test.js b/test/types.embeddeddocument.test.js new file mode 100644 index 00000000000..2859742047d --- /dev/null +++ b/test/types.embeddeddocument.test.js @@ -0,0 +1,55 @@ + +/** + * Module dependencies. + */ + +var assert = require('power-assert'), + start = require('./common'), + mongoose = start.mongoose, + Schema = mongoose.Schema; + +/** + * Test. + */ + +describe('types.embeddeddocument', function() { + var GrandChildSchema; + var ChildSchema; + var ParentSchema; + + before(function() { + GrandChildSchema = new Schema({ + name: String + }); + + ChildSchema = new Schema({ + name: String, + children: [GrandChildSchema] + }); + + ParentSchema = new Schema({ + name: String, + child: ChildSchema + }); + + mongoose.model('Parent-3589-Embedded', ParentSchema); + }); + + it('returns a proper ownerDocument (gh-3589)', function(done) { + var Parent = mongoose.model('Parent-3589-Embedded'); + var p = new Parent({ + name: 'Parent Parentson', + child: { + name: 'Child Parentson', + children: [ + { + name: 'GrandChild Parentson' + } + ] + } + }); + + assert.equal(p._id, p.child.children[0].ownerDocument()._id); + done(); + }); +}); diff --git a/test/types.number.test.js b/test/types.number.test.js index 9e991d6aef4..37c5409e79a 100644 --- a/test/types.number.test.js +++ b/test/types.number.test.js @@ -3,16 +3,15 @@ * Module dependencies. */ -var mongoose = require('./common').mongoose - , SchemaNumber = mongoose.Schema.Types.Number - , assert = require('assert'); +var mongoose = require('./common').mongoose, + SchemaNumber = mongoose.Schema.Types.Number, + assert = require('power-assert'); /** * Test. */ describe('types.number', function() { - it('an empty string casts to null', function(done) { var n = new SchemaNumber(); assert.strictEqual(n.cast(''), null); diff --git a/test/types.subdocument.test.js b/test/types.subdocument.test.js new file mode 100644 index 00000000000..1ea6f09da80 --- /dev/null +++ b/test/types.subdocument.test.js @@ -0,0 +1,90 @@ + +/** + * Module dependencies. + */ + +var assert = require('power-assert'), + start = require('./common'), + mongoose = start.mongoose, + Schema = mongoose.Schema; + +/** + * Test. + */ + +describe('types.subdocument', function() { + var GrandChildSchema; + var ChildSchema; + var ParentSchema; + + before(function() { + GrandChildSchema = new Schema({ + name: String + }); + + ChildSchema = new Schema({ + name: String, + child: GrandChildSchema + }); + + ParentSchema = new Schema({ + name: String, + children: [ChildSchema] + }); + + mongoose.model('Parent-3589-Sub', ParentSchema); + }); + + it('returns a proper ownerDocument (gh-3589)', function(done) { + var Parent = mongoose.model('Parent-3589-Sub'); + var p = new Parent({ + name: 'Parent Parentson', + children: [ + { + name: 'Child Parentson', + child: { + name: 'GrandChild Parentson' + } + } + ] + }); + + assert.equal(p._id, p.children[0].child.ownerDocument()._id); + done(); + }); + it('not setting timestamps in subdocuments', function() { + var Thing = mongoose.model('Thing', new Schema({ + subArray: [{ + testString: String + }] + }, { + timestamps: true + })); + + var thingy = new Thing({ + subArray: [{ + testString: 'Test 1' + }] + }); + var id; + thingy.save(function(err, item) { + assert(!err); + id = item._id; + }) + .then(function() { + var thingy2 = { + subArray: [{ + testString: 'Test 2' + }] + }; + return Thing.update({ + _id: id + }, {$set: thingy2}); + }) + .then(function() { + mongoose.connection.close(); + }, function(reason) { + assert(!reason); + }); + }); +}); diff --git a/test/updateValidators.unit.test.js b/test/updateValidators.unit.test.js index e1424e1cb60..34f645d548f 100644 --- a/test/updateValidators.unit.test.js +++ b/test/updateValidators.unit.test.js @@ -1,4 +1,4 @@ -var assert = require('assert'); +var assert = require('power-assert'); var updateValidators = require('../lib/services/updateValidators'); var emitter = require('events').EventEmitter; @@ -13,8 +13,8 @@ describe('updateValidators', function() { }; schema._getSchema.calls = []; schema.doValidate = function(v, cb) { - schema.doValidate.calls.push({ v: v, cb: cb }); - schema.doValidate.emitter.emit('called', { v: v, cb: cb }); + schema.doValidate.calls.push({v: v, cb: cb}); + schema.doValidate.emitter.emit('called', {v: v, cb: cb}); }; schema.doValidate.calls = []; schema.doValidate.emitter = new emitter(); @@ -22,25 +22,30 @@ describe('updateValidators', function() { describe('validators', function() { it('flattens paths', function(done) { - var fn = updateValidators({}, schema, { test: { a: 1, b: null } }, {}); + var fn = updateValidators({}, schema, {test: {a: 1, b: null}}, {}); schema.doValidate.emitter.on('called', function(args) { args.cb(); }); fn(function(err) { assert.ifError(err); - assert.equal(schema._getSchema.calls.length, 2); - assert.equal(schema.doValidate.calls.length, 2); - assert.equal('test.a', schema._getSchema.calls[0]); - assert.equal('test.b', schema._getSchema.calls[1]); - assert.equal(1, schema.doValidate.calls[0].v); - assert.equal(null, schema.doValidate.calls[1].v); + assert.equal(schema._getSchema.calls.length, 3); + assert.equal(schema.doValidate.calls.length, 3); + assert.equal(schema._getSchema.calls[0], 'test'); + assert.equal(schema._getSchema.calls[1], 'test.a'); + assert.equal(schema._getSchema.calls[2], 'test.b'); + assert.deepEqual(schema.doValidate.calls[0].v, { + a: 1, + b: null + }); + assert.equal(schema.doValidate.calls[1].v, 1); + assert.equal(schema.doValidate.calls[2].v, null); done(); }); }); it('doesnt flatten dates (gh-3194)', function(done) { var dt = new Date(); - var fn = updateValidators({}, schema, { test: dt }, {}); + var fn = updateValidators({}, schema, {test: dt}, {}); schema.doValidate.emitter.on('called', function(args) { args.cb(); }); @@ -48,10 +53,25 @@ describe('updateValidators', function() { assert.ifError(err); assert.equal(schema._getSchema.calls.length, 1); assert.equal(schema.doValidate.calls.length, 1); - assert.equal('test', schema._getSchema.calls[0]); + assert.equal(schema._getSchema.calls[0], 'test'); assert.equal(dt, schema.doValidate.calls[0].v); done(); }); }); + + it('doesnt flatten empty arrays (gh-3554)', function(done) { + var fn = updateValidators({}, schema, {test: []}, {}); + schema.doValidate.emitter.on('called', function(args) { + args.cb(); + }); + fn(function(err) { + assert.ifError(err); + assert.equal(schema._getSchema.calls.length, 1); + assert.equal(schema.doValidate.calls.length, 1); + assert.equal(schema._getSchema.calls[0], 'test'); + assert.deepEqual(schema.doValidate.calls[0].v, []); + done(); + }); + }); }); }); diff --git a/test/utils.test.js b/test/utils.test.js index 98bdf4c113d..edb6f19c411 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -3,14 +3,14 @@ * Module dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , Schema = mongoose.Schema - , utils = require('../lib/utils') - , StateMachine = require('../lib/statemachine') - , ObjectId = require('../lib/types/objectid') - , MongooseBuffer = require('../lib/types/buffer') - , assert = require('assert'); +var start = require('./common'), + mongoose = start.mongoose, + Schema = mongoose.Schema, + utils = require('../lib/utils'), + StateMachine = require('../lib/statemachine'), + ObjectId = require('../lib/types/objectid'), + MongooseBuffer = require('../lib/types/buffer'), + assert = require('power-assert'); /** * Setup. @@ -35,21 +35,21 @@ describe('utils', function() { it('should detect a path as required if it has been required', function(done) { var ar = new ActiveRoster(); ar.require('hello'); - assert.equal(ar.paths['hello'],'require'); + assert.equal(ar.paths.hello, 'require'); done(); }); it('should detect a path as inited if it has been inited', function(done) { var ar = new ActiveRoster(); ar.init('hello'); - assert.equal(ar.paths['hello'],'init'); + assert.equal(ar.paths.hello, 'init'); done(); }); it('should detect a path as modified', function(done) { var ar = new ActiveRoster(); ar.modify('hello'); - assert.equal(ar.paths['hello'],'modify'); + assert.equal(ar.paths.hello, 'modify'); done(); }); @@ -93,7 +93,7 @@ describe('utils', function() { ar.modify('world'); ar.require('foo'); ar.forEach(function(path) { - assert.ok(~['hello', 'goodbye','world', 'foo'].indexOf(path)); + assert.ok(~['hello', 'goodbye', 'world', 'foo'].indexOf(path)); }); done(); }); @@ -119,11 +119,11 @@ describe('utils', function() { var suffixedPaths = ar.map('init', 'modify', function(path) { return path + '-suffix'; }); - assert.deepEqual(suffixedPaths,['hello-suffix', 'world-suffix']); + assert.deepEqual(suffixedPaths, ['hello-suffix', 'world-suffix']); done(); }); - it("should `map` over all states' paths if no states are specified in a `map` invocation", function(done) { + it('should `map` over all states\' paths if no states are specified in a `map` invocation', function(done) { var ar = new ActiveRoster(); ar.init('hello'); ar.modify('world'); @@ -131,22 +131,21 @@ describe('utils', function() { var suffixedPaths = ar.map(function(path) { return path + '-suffix'; }); - assert.deepEqual(suffixedPaths,['iAmTheWalrus-suffix', 'hello-suffix', 'world-suffix']); + assert.deepEqual(suffixedPaths, ['iAmTheWalrus-suffix', 'hello-suffix', 'world-suffix']); done(); }); - }); it('utils.options', function(done) { - var o = { a: 1, b: 2, c: 3, 0: 'zero1' }; - var defaults = { b: 10, d: 20, 0: 'zero2' }; + var o = {a: 1, b: 2, c: 3, 0: 'zero1'}; + var defaults = {b: 10, d: 20, 0: 'zero2'}; var result = utils.options(defaults, o); - assert.equal(1, result.a); - assert.equal(result.b,2); - assert.equal(result.c,3); - assert.equal(result.d,20); - assert.deepEqual(o.d,result.d); - assert.equal(result['0'],'zero1'); + assert.equal(result.a, 1); + assert.equal(result.b, 2); + assert.equal(result.c, 3); + assert.equal(result.d, 20); + assert.deepEqual(o.d, result.d); + assert.equal(result['0'], 'zero1'); var result2 = utils.options(defaults); assert.equal(result2.b, 10); @@ -164,8 +163,8 @@ describe('utils', function() { it('deepEquals on ObjectIds', function(done) { var s = (new ObjectId).toString(); - var a = new ObjectId(s) - , b = new ObjectId(s); + var a = new ObjectId(s), + b = new ObjectId(s); assert.ok(utils.deepEqual(a, b)); assert.ok(utils.deepEqual(a, a)); @@ -174,12 +173,12 @@ describe('utils', function() { }); it('deepEquals on MongooseDocumentArray works', function(done) { - var db = start() - , A = new Schema({ a: String }) - , M = db.model('deepEqualsOnMongooseDocArray', new Schema({ - a1: [A] - , a2: [A] - })); + var db = start(), + A = new Schema({a: String}), + M = db.model('deepEqualsOnMongooseDocArray', new Schema({ + a1: [A], + a2: [A] + })); db.close(); @@ -202,12 +201,12 @@ describe('utils', function() { // gh-688 it('deepEquals with MongooseBuffer', function(done) { - var str = "this is the day"; + var str = 'this is the day'; var a = new MongooseBuffer(str); var b = new MongooseBuffer(str); var c = new Buffer(str); - var d = new Buffer("this is the way"); - var e = new Buffer("other length"); + var d = new Buffer('this is the way'); + var e = new Buffer('other length'); assert.ok(utils.deepEqual(a, b)); assert.ok(utils.deepEqual(a, c)); @@ -245,15 +244,15 @@ describe('utils', function() { assert.strictEqual('0', out.b); assert.strictEqual(1, out.c); assert.strictEqual('1', out.d); - assert.equal(4, Object.keys(out).length); + assert.equal(Object.keys(out).length, 4); done(); }); }); it('array.flatten', function(done) { - var orig = [0,[1,2,[3,4,[5,[6]],7],8],9]; - assert.deepEqual([0,1,2,3,4,5,6,7,8,9], utils.array.flatten(orig)); + var orig = [0, [1, 2, [3, 4, [5, [6]], 7], 8], 9]; + assert.deepEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], utils.array.flatten(orig)); done(); }); @@ -310,17 +309,17 @@ describe('utils', function() { describe('pluralize', function() { var db; - beforeEach(function() { + before(function() { db = start(); }); - afterEach(function(done) { + after(function(done) { db.close(done); }); it('should not pluralize _temp_ (gh-1703)', function(done) { var ASchema = new Schema({ - value: { type: Schema.Types.Mixed } + value: {type: Schema.Types.Mixed} }); var collectionName = '_temp_'; @@ -329,9 +328,8 @@ describe('utils', function() { done(); }); it('should pluralize _temp (gh-1703)', function(done) { - var ASchema = new Schema({ - value: { type: Schema.Types.Mixed } + value: {type: Schema.Types.Mixed} }); var collectionName = '_temp'; @@ -353,7 +351,7 @@ describe('utils', function() { var ASchema = new Schema({value: String}); - var collectionName = 'singular'; + var collectionName = 'one'; var A = db.model(collectionName, ASchema); assert.equal(A.collection.name, collectionName + 's'); done(); @@ -363,7 +361,7 @@ describe('utils', function() { var ASchema = new Schema({value: String}); - var collectionName = 'singular'; + var collectionName = 'two'; var A = db.model(collectionName, ASchema); assert.equal(A.collection.name, collectionName); done(); @@ -374,7 +372,7 @@ describe('utils', function() { // override var ASchema = new Schema({value: String}, {pluralization: true}); - var collectionName = 'singular'; + var collectionName = 'three'; var A = db.model(collectionName, ASchema); assert.equal(A.collection.name, collectionName + 's'); done(); @@ -384,7 +382,7 @@ describe('utils', function() { var ASchema = new Schema({value: String}, {pluralization: false}); - var collectionName = 'singular'; + var collectionName = 'four'; var A = db.model(collectionName, ASchema); assert.equal(A.collection.name, collectionName); done(); @@ -392,7 +390,7 @@ describe('utils', function() { it('should not pluralize when local option set to false and global not set', function(done) { var ASchema = new Schema({value: String}, {pluralization: false}); - var collectionName = 'singular'; + var collectionName = 'five'; var A = db.model(collectionName, ASchema); assert.equal(A.collection.name, collectionName); done(); diff --git a/test/versioning.test.js b/test/versioning.test.js index f8601a44892..4fa34acd7c9 100644 --- a/test/versioning.test.js +++ b/test/versioning.test.js @@ -1,54 +1,62 @@ - /** * Test dependencies. */ -var start = require('./common') - , mongoose = start.mongoose - , assert = require('assert') - , random = require('../lib/utils').random - , Schema = mongoose.Schema - , VersionError = mongoose.Error.VersionError - ; +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + random = require('../lib/utils').random, + Schema = mongoose.Schema, + VersionError = mongoose.Error.VersionError; -/** - * Setup. - */ +describe('versioning', function() { + var db; + var Comments; + var BlogPost; -var Comments = new Schema(); + before(function() { + db = start(); -Comments.add({ - title : String - , date : Date - , comments : [Comments] - , dontVersionMeEither: [] -}); + Comments = new Schema(); -var BlogPost = new Schema({ - title : String - , date : Date - , meta : { - date : Date - , visitors : Number - , nested : [Comments] - , numbers : [Number] - } - , mixed : {} - , numbers : [Number] - , comments : [Comments] - , arr : [] - , dontVersionMe: [] -}, { - collection: 'versioning_' + random() - , skipVersioning: { - dontVersionMe: true - , 'comments.dontVersionMeEither': true - } -}); + Comments.add({ + title: String, + date: Date, + comments: [Comments], + dontVersionMeEither: [] + }); -mongoose.model('Versioning', BlogPost); + BlogPost = new Schema( + { + title: String, + date: Date, + meta: { + date: Date, + visitors: Number, + nested: [Comments], + numbers: [Number] + }, + mixed: {}, + numbers: [Number], + comments: [Comments], + arr: [], + dontVersionMe: [] + }, + { + collection: 'versioning_' + random(), + skipVersioning: { + dontVersionMe: true, + 'comments.dontVersionMeEither': true + } + }); + + mongoose.model('Versioning', BlogPost); + }); + + after(function(done) { + db.close(done); + }); -describe('versioning', function() { it('is only added to parent schema (gh-1265)', function(done) { assert.ok(BlogPost.path('__v')); assert.ok(!BlogPost.path('comments').__v); @@ -57,143 +65,149 @@ describe('versioning', function() { }); it('works', function(done) { - var db = start() - , V = db.model('Versioning'); + var V = db.model('Versioning'); var doc = new V; doc.title = 'testing versioning'; doc.date = new Date; doc.meta.date = new Date; doc.meta.visitors = 34; - doc.meta.numbers = [12,11,10]; + doc.meta.numbers = [12, 11, 10]; doc.meta.nested = [ - { title: 'does it work?', date: new Date } - , { title: '1', comments: [{ title: 'this is sub #1'},{ title: 'this is sub #2'}] } - , { title: '2', comments: [{ title: 'this is sub #3'},{ title: 'this is sub #4'}] } - , { title: 'hi', date: new Date } + {title: 'does it work?', date: new Date}, + {title: '1', comments: [{title: 'this is sub #1'}, {title: 'this is sub #2'}]}, + {title: '2', comments: [{title: 'this is sub #3'}, {title: 'this is sub #4'}]}, + {title: 'hi', date: new Date} ]; - doc.mixed = { arr: [12,11,10] }; - doc.numbers = [3,4,5,6,7]; + doc.mixed = {arr: [12, 11, 10]}; + doc.numbers = [3, 4, 5, 6, 7]; doc.comments = [ - { title: 'comments 0', date: new Date } - , { title: 'comments 1', comments: [{ title: 'comments.1.comments.1'},{ title: 'comments.1.comments.2'}] } - , { title: 'coments 2', comments: [{ title: 'comments.2.comments.1'},{ title: 'comments.2.comments.2'}] } - , { title: 'comments 3', date: new Date } + {title: 'comments 0', date: new Date}, + {title: 'comments 1', comments: [{title: 'comments.1.comments.1'}, {title: 'comments.1.comments.2'}]}, + {title: 'coments 2', comments: [{title: 'comments.2.comments.1'}, {title: 'comments.2.comments.2'}]}, + {title: 'comments 3', date: new Date} ]; doc.arr = [['2d']]; - doc.save(function(err) { - var a , b; - assert.ifError(err); - // test 2 concurrent ops - V.findById(doc, function(err, _a) { - assert.ifError(err); - a = _a; - a && b && test1(a, b); - }); - V.findById(doc, function(err, _b) { - assert.ifError(err); - b = _b; - a && b && test1(a, b); + function save(a, b, cb) { + var e; + function lookup() { + var a1, b1; + V.findById(a, function(err, a_) { + if (err && !e) { + e = err; + } + a1 = a_; + if (a1 && b1) { + cb(e, a1, b1); + } + }); + V.findById(b, function(err, b_) { + if (err && !e) { + e = err; + } + b1 = b_; + if (a1 && b1) { + cb(e, a1, b1); + } + }); + } + // make sure that a saves before b + a.save(function(err) { + if (err) { + e = err; + } + b.save(function(err) { + if (err) { + e = err; + } + lookup(); + }); }); - }); + } - function test1(a, b) { - a.meta.numbers.push(9); - b.meta.numbers.push(8); - save(a, b, test2); + function test15(err, a) { + assert.equal(a._doc.__v, 13, 'version should not be incremented for non-versioned sub-document fields'); + done(); } - function test2(err, a, b) { + function test14(err, a, b) { assert.ifError(err); - assert.equal(a.meta.numbers.length, 5); - assert.equal(a._doc.__v, 2); - a.meta.numbers.pull(10); - b.meta.numbers.push(20); - save(a, b, test3); + assert.equal(a._doc.__v, 13, 'version should not be incremented for non-versioned fields'); + a.comments[0].dontVersionMeEither.push('value1'); + b.comments[0].dontVersionMeEither.push('value2'); + save(a, b, test15); } - function test3(err, a, b) { + function test13(err, a, b) { assert.ifError(err); - assert.equal(a.meta.numbers.length, 5); - assert.equal(b.meta.numbers.length, 5); - assert.equal(-1, a.meta.numbers.indexOf(10)); - assert.ok(~a.meta.numbers.indexOf(20)); - assert.equal(a._doc.__v, 4); - - a.numbers.pull(3, 20); - - // should fail - b.set('numbers.2', 100); - save(a, b, test4); + a.dontVersionMe.push('value1'); + b.dontVersionMe.push('value2'); + save(a, b, test14); } - function test4(err, a, b) { - assert.ok(/No matching document/.test(err), err); - assert.equal(a._doc.__v, 5); - a.set('arr.0.0', 'updated'); + function test12(err, a, b) { + assert.ok(err instanceof VersionError); + assert.ok(err.stack.indexOf('versioning.test.js') !== -1); + assert.ok(/No matching document/.test(err), 'changes to b should not be applied'); + assert.equal(a.comments.length, 5); + + a.comments.addToSet({title: 'aven'}); + a.comments.addToSet({title: 'avengers'}); var d = a.$__delta(); - assert.equal(a._doc.__v, d[0].__v, 'version should be added to where clause'); - assert.ok(!('$inc' in d[1])); - save(a,b,test5); - } - function test5(err, a, b) { - assert.ifError(err); - assert.equal('updated', a.arr[0][0]); - assert.equal(a._doc.__v, 5); - a.set('arr.0', 'not an array'); - // should overwrite a's changes, last write wins - b.arr.pull(10); - b.arr.addToSet('using set'); - save(a, b, test6); + assert.equal(d[0].__v, undefined, 'version should not be included in where clause'); + assert.ok(!d[1].$set); + assert.ok(d[1].$addToSet); + assert.ok(d[1].$addToSet.comments); + + a.comments.$shift(); + d = a.$__delta(); + assert.equal(d[0].__v, 12, 'version should be included in where clause'); + assert.ok(d[1].$set, 'two differing atomic ops on same path should create a $set'); + assert.ok(d[1].$inc, 'a $set of an array should trigger versioning'); + assert.ok(!d[1].$addToSet); + save(a, b, test13); } - function test6(err, a, b) { + function test11(err, a, b) { assert.ifError(err); - assert.equal(a.arr.length, 2); - assert.equal('updated', a.arr[0][0]); - assert.equal('using set', a.arr[1]); - assert.equal(a._doc.__v, 6); - b.set('arr.0', 'not an array'); - // should overwrite b's changes, last write wins - // force a $set - a.arr.pull('using set'); - a.arr.push('woot', 'woot2'); - a.arr.$pop(); - save(a, b, test7); - } + assert.equal(a._doc.__v, 11); + assert.equal(a.mixed.arr.length, 6); + assert.equal(a.mixed.arr[4].x, 1); + assert.equal(a.mixed.arr[5], 'woot'); + assert.equal(a.mixed.arr[3][0], 10); - function test7(err,a,b) { - assert.ok(/No matching document/.test(err), 'changes to b should not be applied'); - assert.equal(a.arr.length, 2); - assert.equal('updated', a.arr[0][0]); - assert.equal('woot', a.arr[1]); - assert.equal(a._doc.__v, 7); - a.meta.nested.$pop(); - b.meta.nested.$pop(); - save(a, b, test8); + a.comments.addToSet({title: 'monkey'}); + b.markModified('comments'); + + var d = b.$__delta(); + assert.ok(d[1].$inc, 'a $set of an array should trigger versioning'); + + save(a, b, test12); } - function test8(err, a, b) { - assert.ok(/No matching document/.test(err), 'changes to b should not be applied'); - assert.equal(a.meta.nested.length, 3); - assert.equal(a._doc.__v, 8); - a.meta.nested.push({ title: 'the' }); - a.meta.nested.push({ title: 'killing' }); - b.meta.nested.push({ title: 'biutiful' }); - save(a, b, test9); + function test10(err, a, b) { + assert.ifError(err); + assert.equal(b.meta.nested[2].title, 'two'); + assert.equal(b.meta.nested[0].title, 'zero'); + assert.equal(b.meta.nested[1].comments[0].title, 'sub one'); + assert.equal(a._doc.__v, 10); + assert.equal(a.mixed.arr.length, 3); + a.mixed.arr.push([10], {x: 1}, 'woot'); + a.markModified('mixed.arr'); + save(a, b, test11); } function test9(err, a, b) { assert.ifError(err); - assert.equal(6, a.meta.nested.length); + assert.equal(a.meta.nested.length, 6); assert.equal(a._doc.__v, 10); // nested subdoc property changes should not trigger version increments a.meta.nested[2].title = 'two'; b.meta.nested[0].title = 'zero'; b.meta.nested[1].comments[0].title = 'sub one'; - save(a,b, function(err, _a, _b) { + save(a, b, function(err, _a, _b) { assert.ifError(err); assert.equal(a._doc.__v, 10); assert.equal(b._doc.__v, 10); @@ -201,126 +215,132 @@ describe('versioning', function() { }); } - function test10(err, a, b) { - assert.ifError(err); - assert.equal('two', b.meta.nested[2].title); - assert.equal('zero', b.meta.nested[0].title); - assert.equal('sub one', b.meta.nested[1].comments[0].title); - assert.equal(a._doc.__v, 10); - assert.equal(3, a.mixed.arr.length); - a.mixed.arr.push([10],{x:1},'woot'); - a.markModified('mixed.arr'); - save(a, b, test11); + function test8(err, a, b) { + assert.ok(/No matching document/.test(err), 'changes to b should not be applied'); + assert.equal(a.meta.nested.length, 3); + assert.equal(a._doc.__v, 8); + a.meta.nested.push({title: 'the'}); + a.meta.nested.push({title: 'killing'}); + b.meta.nested.push({title: 'biutiful'}); + save(a, b, test9); } - function test11(err, a, b) { - assert.ifError(err); - assert.equal(a._doc.__v, 11); - assert.equal(6, a.mixed.arr.length); - assert.equal(1, a.mixed.arr[4].x); - assert.equal('woot', a.mixed.arr[5]); - assert.equal(10, a.mixed.arr[3][0]); - - a.comments.addToSet({ title: 'monkey' }); - b.markModified('comments'); - - var d = b.$__delta(); - assert.ok(d[1].$inc, 'a $set of an array should trigger versioning'); + function test7(err, a, b) { + assert.ok(/No matching document/.test(err), 'changes to b should not be applied'); + assert.equal(a.arr.length, 2); + assert.equal(a.arr[0][0], 'updated'); + assert.equal(a.arr[1], 'woot'); + assert.equal(a._doc.__v, 7); + a.meta.nested.$pop(); + b.meta.nested.$pop(); + save(a, b, test8); + } - save(a, b, test12); + function test6(err, a, b) { + assert.ifError(err); + assert.equal(a.arr.length, 2); + assert.equal(a.arr[0][0], 'updated'); + assert.equal(a.arr[1], 'using set'); + assert.equal(a._doc.__v, 6); + b.set('arr.0', 'not an array'); + // should overwrite b's changes, last write wins + // force a $set + a.arr.pull('using set'); + a.arr.push('woot', 'woot2'); + a.arr.$pop(); + save(a, b, test7); } - function test12(err, a, b) { - assert.ok(err instanceof VersionError); - assert.ok(/No matching document/.test(err), 'changes to b should not be applied'); - assert.equal(5, a.comments.length); + function test5(err, a, b) { + assert.ifError(err); + assert.equal(a.arr[0][0], 'updated'); + assert.equal(a._doc.__v, 5); + a.set('arr.0', 'not an array'); + // should overwrite a's changes, last write wins + b.arr.pull(10); + b.arr.addToSet('using set'); + save(a, b, test6); + } - a.comments.addToSet({ title: 'aven' }); - a.comments.addToSet({ title: 'avengers' }); + function test4(err, a, b) { + assert.ok(/No matching document/.test(err), err); + assert.equal(a._doc.__v, 5); + assert.equal(err.version, b._doc.__v - 1); + assert.deepEqual(err.modifiedPaths, ['numbers', 'numbers.2']); + a.set('arr.0.0', 'updated'); var d = a.$__delta(); - - assert.equal(undefined, d[0].__v, 'version should not be included in where clause'); - assert.ok(!d[1].$set); - assert.ok(d[1].$addToSet); - assert.ok(d[1].$addToSet.comments); - - a.comments.$shift(); - d = a.$__delta(); - assert.equal(12, d[0].__v, 'version should be included in where clause'); - assert.ok(d[1].$set, 'two differing atomic ops on same path should create a $set'); - assert.ok(d[1].$inc, 'a $set of an array should trigger versioning'); - assert.ok(!d[1].$addToSet); - save(a, b, test13); + assert.equal(a._doc.__v, d[0].__v, 'version should be added to where clause'); + assert.ok(!('$inc' in d[1])); + save(a, b, test5); } - function test13(err, a, b) { + function test3(err, a, b) { assert.ifError(err); - a.dontVersionMe.push('value1'); - b.dontVersionMe.push('value2'); - save(a, b, test14); + assert.equal(a.meta.numbers.length, 5); + assert.equal(b.meta.numbers.length, 5); + assert.equal(-1, a.meta.numbers.indexOf(10)); + assert.ok(~a.meta.numbers.indexOf(20)); + assert.equal(a._doc.__v, 4); + + a.numbers.pull(3, 20); + + // should fail + b.set('numbers.2', 100); + save(a, b, test4); } - function test14(err, a, b) { + function test2(err, a, b) { assert.ifError(err); - assert.equal(a._doc.__v, 13, 'version should not be incremented for non-versioned fields'); - a.comments[0].dontVersionMeEither.push('value1'); - b.comments[0].dontVersionMeEither.push('value2'); - save(a, b, test15); + assert.equal(a.meta.numbers.length, 5); + assert.equal(a._doc.__v, 2); + a.meta.numbers.pull(10); + b.meta.numbers.push(20); + save(a, b, test3); } - function test15(err, a) { - assert.equal(a._doc.__v, 13, 'version should not be incremented for non-versioned sub-document fields'); - db.close(); - done(); + function test1(a, b) { + a.meta.numbers.push(9); + b.meta.numbers.push(8); + save(a, b, test2); } - function save(a, b, cb) { - var e; - // make sure that a saves before b - a.save(function(err) { - if (err) e = err; - b.save(function(err) { - if (err) e = err; - lookup(); - }); + doc.save(function(err) { + var a, b; + assert.ifError(err); + // test 2 concurrent ops + V.findById(doc, function(err, _a) { + assert.ifError(err); + a = _a; + if (a && b) { + test1(a, b); + } }); - function lookup() { - var a1, b1; - V.findById(a, function(err, a_) { - if (err && !e) e = err; - a1 = a_; - a1 && b1 && cb(e, a1, b1); - }); - V.findById(b, function(err, b_) { - if (err && !e) e = err; - b1 = b_; - a1 && b1 && cb(e, a1, b1); - }); - } - } - + V.findById(doc, function(err, _b) { + assert.ifError(err); + b = _b; + if (a && b) { + test1(a, b); + } + }); + }); }); it('versioning without version key', function(done) { - var db = start() - , V = db.model('Versioning'); + var V = db.model('Versioning'); var doc = new V; - doc.numbers = [3,4,5,6,7]; + doc.numbers = [3, 4, 5, 6, 7]; doc.comments = [ - { title: 'does it work?', date: new Date } - , { title: '1', comments: [{ title: 'this is sub #1'},{ title: 'this is sub #2'}] } - , { title: '2', comments: [{ title: 'this is sub #3'},{ title: 'this is sub #4'}] } - , { title: 'hi', date: new Date } + {title: 'does it work?', date: new Date}, + {title: '1', comments: [{title: 'this is sub #1'}, {title: 'this is sub #2'}]}, + {title: '2', comments: [{title: 'this is sub #3'}, {title: 'this is sub #4'}]}, + {title: 'hi', date: new Date} ]; - doc.save(test); - function test(err) { assert.ifError(err); // test getting docs back from db missing version key V.findById(doc).select('numbers comments').exec(function(err, doc) { - db.close(); assert.ifError(err); doc.comments[0].title = 'no version was included'; var d = doc.$__delta(); @@ -328,28 +348,28 @@ describe('versioning', function() { done(); }); } + + doc.save(test); }); it('version works with strict docs', function(done) { - var db = start(); - var schema = new Schema({ str: ['string'] }, { strict: true, collection: 'versionstrict_' + random() }); + var schema = new Schema({str: ['string']}, {strict: true, collection: 'versionstrict_' + random()}); var M = db.model('VersionStrict', schema); - var m = new M({ str: ['death', 'to', 'smootchy'] }); + var m = new M({str: ['death', 'to', 'smootchy']}); m.save(function(err) { assert.ifError(err); M.find(m, function(err, m) { assert.ifError(err); - assert.equal(1, m.length); + assert.equal(m.length, 1); m = m[0]; - assert.equal(0, m._doc.__v); + assert.equal(m._doc.__v, 0); m.str.pull('death'); m.save(function(err) { assert.ifError(err); M.findById(m, function(err, m) { - db.close(); assert.ifError(err); - assert.equal(1, m._doc.__v); - assert.equal(2, m.str.length); + assert.equal(m._doc.__v, 1); + assert.equal(m.str.length, 2); assert.ok(!~m.str.indexOf('death')); done(); }); @@ -359,24 +379,22 @@ describe('versioning', function() { }); it('version works with existing unversioned docs', function(done) { - var db = start() - , V = db.model('Versioning'); + var V = db.model('Versioning'); - V.collection.insert({ title: 'unversioned', numbers: [1,2,3] }, {safe:true}, function(err) { + V.collection.insert({title: 'unversioned', numbers: [1, 2, 3]}, {safe: true}, function(err) { assert.ifError(err); - V.findOne({ title: 'unversioned' }, function(err, d) { + V.findOne({title: 'unversioned'}, function(err, d) { assert.ifError(err); assert.ok(!d._doc.__v); d.numbers.splice(1, 1, 10); var o = d.$__delta(); - assert.equal(undefined, o[0].__v); + assert.equal(o[0].__v, undefined); assert.ok(o[1].$inc); - assert.equal(1, o[1].$inc.__v); + assert.equal(o[1].$inc.__v, 1); d.save(function(err, d) { assert.ifError(err); - assert.equal(1, d._doc.__v); + assert.equal(d._doc.__v, 1); V.findById(d, function(err, d) { - db.close(); assert.ifError(err); assert.ok(d); done(); @@ -387,80 +405,75 @@ describe('versioning', function() { }); it('versionKey is configurable', function(done) { - var db = start(); var schema = new Schema( - { configured: 'bool' } - , { versionKey: 'lolwat', collection: 'configuredversion' + random() }); + {configured: 'bool'}, + {versionKey: 'lolwat', collection: 'configuredversion' + random()}); var V = db.model('ConfiguredVersionKey', schema); - var v = new V({ configured: true }); + var v = new V({configured: true}); v.save(function(err) { assert.ifError(err); - V.findById(v, function(err, v) { - db.close(); - assert.ifError(err); - assert.equal(0, v._doc.lolwat); + V.findById(v, function(err1, v) { + assert.ifError(err1); + assert.equal(v._doc.lolwat, 0); done(); }); }); }); it('can be disabled', function(done) { - var db = start(); - var schema = Schema({ x: ['string'] }, { versionKey: false }); + var schema = new Schema({x: ['string']}, {versionKey: false}); var M = db.model('disabledVersioning', schema, 's' + random()); - M.create({ x: ['hi'] }, function(err, doc) { + M.create({x: ['hi']}, function(err, doc) { assert.ifError(err); - assert.equal(false, '__v' in doc._doc); + assert.equal('__v' in doc._doc, false); doc.x.pull('hi'); doc.save(function(err) { assert.ifError(err); - assert.equal(false, '__v' in doc._doc); + assert.equal('__v' in doc._doc, false); doc.set('x.0', 'updated'); var d = doc.$__delta()[0]; - assert.equal(undefined, d.__v, 'version should not be added to where clause'); + assert.equal(d.__v, undefined, 'version should not be added to where clause'); - M.collection.findOne({ _id: doc._id }, function(err, doc) { - assert.equal(false, '__v' in doc); - db.close(done); + M.collection.findOne({_id: doc._id}, function(err, doc) { + assert.equal('__v' in doc, false); + done(); }); }); }); }); it('works with numbericAlpha paths', function(done) { - var db = start(); var M = db.model('Versioning'); - var m = new M({ mixed: {} }); + var m = new M({mixed: {}}); var path = 'mixed.4a'; m.set(path, 2); m.save(function(err) { assert.ifError(err); - db.close(done); + done(); }); }); describe('doc.increment()', function() { it('works without any other changes (gh-1475)', function(done) { - var db = start() - , V = db.model('Versioning'); + var V = db.model('Versioning'); var doc = new V; doc.save(function(err) { assert.ifError(err); - assert.equal(0, doc.__v); + assert.equal(doc.__v, 0); doc.increment(); doc.save(function(err) { assert.ifError(err); - assert.equal(1, doc.__v); + assert.equal(doc.__v, 1); V.findById(doc, function(err, doc) { assert.ifError(err); - assert.equal(1, doc.__v); - db.close(done); + assert.equal(doc.__v, 1); + done(); }); }); }); @@ -468,25 +481,24 @@ describe('versioning', function() { }); describe('versioning is off', function() { - it('when { safe : false } is set (gh-1520)', function(done) { - var schema1 = new Schema({ title : String}, { safe : false }); + it('when { safe: false } is set (gh-1520)', function(done) { + var schema1 = new Schema({title: String}, {safe: false}); assert.equal(schema1.options.versionKey, false); done(); }); - it('when { safe : { w: 0 }} is set (gh-1520)', function(done) { - var schema1 = new Schema({ title : String}, { safe : { w: 0 } }); + it('when { safe: { w: 0 }} is set (gh-1520)', function(done) { + var schema1 = new Schema({title: String}, {safe: {w: 0}}); assert.equal(schema1.options.versionKey, false); done(); }); }); it('gh-1898', function(done) { - var db = start(); - var schema = new Schema({ tags: [String], name: String }); + var schema = new Schema({tags: [String], name: String}); var M = db.model('gh-1898', schema, 'gh-1898'); - var m = new M({ tags: ['eggs'] }); + var m = new M({tags: ['eggs']}); m.save(function(err) { assert.ifError(err); @@ -498,23 +510,49 @@ describe('versioning', function() { assert.equal(m.$__where(m.$__delta()[0]).__v, 0); assert.equal(m.$__delta()[1].$inc.__v, 1); - db.close(done); + done(); }); }); it('can remove version key from toObject() (gh-2675)', function(done) { - var db = start(); - var schema = new Schema({ name: String }); + var schema = new Schema({name: String}); var M = db.model('gh2675', schema, 'gh2675'); var m = new M(); m.save(function(err, m) { assert.ifError(err); var obj = m.toObject(); - assert.equal(0, obj.__v); - obj = m.toObject({ versionKey: false }); - assert.equal(undefined, obj.__v); - db.close(done); + assert.equal(obj.__v, 0); + obj = m.toObject({versionKey: false}); + assert.equal(obj.__v, undefined); + done(); }); }); + + it('copying doc works (gh-5779)', function(done) { + var schema = new Schema({ subdocs: [{ a: Number }] }); + var M = db.model('gh5779', schema, 'gh5779'); + var m = new M({ subdocs: [] }); + var m2; + + m.save(). + then(function() { + m2 = new M(m); + m2.subdocs.push({ a: 2 }); + return m2.save(); + }). + then(function() { + m2.subdocs[0].a = 3; + return m2.save(); + }). + then(function() { + assert.equal(m2.subdocs[0].a, 3); + return M.findById(m._id); + }). + then(function(doc) { + assert.equal(doc.subdocs[0].a, 3); + done(); + }). + catch(done); + }); }); diff --git a/tools/auth.js b/tools/auth.js new file mode 100644 index 00000000000..967f5e1f8b9 --- /dev/null +++ b/tools/auth.js @@ -0,0 +1,30 @@ +'use strict'; + +const Server = require('mongodb-topology-manager').Server; +const co = require('co'); +const mongodb = require('mongodb'); + +co(function*() { + // Create new instance + var server = new Server('mongod', { + auth: null, + dbpath: '/data/db/27017' + }); + + // Purge the directory + yield server.purge(); + + // Start process + yield server.start(); + + const db = yield mongodb.MongoClient.connect('mongodb://localhost:27017/admin'); + + yield db.addUser('passwordIsTaco', 'taco', { + roles: ['dbOwner'] + }); + + console.log('done'); +}).catch(error => { + console.error(error); + process.exit(-1); +}); diff --git a/tools/repl.js b/tools/repl.js new file mode 100644 index 00000000000..ca3b6de5618 --- /dev/null +++ b/tools/repl.js @@ -0,0 +1,36 @@ +'use strict'; + +const co = require('co'); + +co(function*() { + var ReplSet = require('mongodb-topology-manager').ReplSet; + + // Create new instance + var topology = new ReplSet('mongod', [{ + // mongod process options + options: { + bind_ip: 'localhost', port: 31000, dbpath: `/data/db/31000` + } + }, { + // mongod process options + options: { + bind_ip: 'localhost', port: 31001, dbpath: `/data/db/31001` + } + }, { + // Type of node + arbiterOnly: true, + // mongod process options + options: { + bind_ip: 'localhost', port: 31002, dbpath: `/data/db/31002` + } + }], { + replSet: 'rs' + }); + + yield topology.start(); + + console.log('done'); +}).catch(error => { + console.error(error); + process.exit(-1); +}); diff --git a/tools/sharded.js b/tools/sharded.js new file mode 100644 index 00000000000..0883c31b454 --- /dev/null +++ b/tools/sharded.js @@ -0,0 +1,42 @@ +'use strict'; + +const co = require('co'); + +co(function*() { + var Sharded = require('mongodb-topology-manager').Sharded; + + // Create new instance + var topology = new Sharded({ + mongod: 'mongod', + mongos: 'mongos' + }); + + yield topology.addShard([{ + options: { + bind_ip: 'localhost', port: 31000, dbpath: `/data/db/31000` + } + }], { replSet: 'rs1' }); + + yield topology.addConfigurationServers([{ + options: { + bind_ip: 'localhost', port: 35000, dbpath: `/data/db/35000` + } + }], { replSet: 'rs0' }); + + yield topology.addProxies([{ + bind_ip: 'localhost', port: 51000, configdb: 'localhost:35000' + }], { + binary: 'mongos' + }); + + // Start up topology + yield topology.start(); + + // Shard db + yield topology.enableSharding('test'); + + console.log('done'); +}).catch(error => { + console.error(error); + process.exit(-1); +}); diff --git a/website.js b/website.js index c8f2ee3e86f..a1186a559b5 100644 --- a/website.js +++ b/website.js @@ -1,4 +1,3 @@ - var fs = require('fs'); var jade = require('jade'); var package = require('./package'); @@ -10,6 +9,32 @@ var klass = require('./docs/helpers/klass'); // add custom jade filters require('./docs/helpers/filters')(jade); +function getVersion() { + var hist = fs.readFileSync('./History.md', 'utf8').replace(/\r/g, '\n').split('\n'); + for (var i = 0; i < hist.length; ++i) { + var line = (hist[i] || '').trim(); + if (!line) { + continue; + } + var match = /^\s*([^\s]+)\s/.exec(line); + if (match && match[1]) { + return match[1]; + } + } + throw new Error('no match found'); +} + +function getUnstable(ver) { + ver = ver.replace('-pre'); + var spl = ver.split('.'); + spl = spl.map(function(i) { + return parseInt(i, 10); + }); + spl[1]++; + spl[2] = 'x'; + return spl.join('.'); +} + // use last release package.version = getVersion(); package.unstable = getUnstable(package.version); @@ -17,12 +42,36 @@ package.unstable = getUnstable(package.version); var filemap = require('./docs/source'); var files = Object.keys(filemap); +function jadeify(filename, options, newfile) { + options = options || {}; + options.package = package; + options.hl = hl; + options.linktype = linktype; + options.href = href; + options.klass = klass; + jade.renderFile(filename, options, function(err, str) { + if (err) { + console.error(err.stack); + return; + } + + newfile = newfile || filename.replace('.jade', '.html'); + fs.writeFile(newfile, str, function(err) { + if (err) { + console.error('could not write', err.stack); + } else { + console.log('%s : rendered ', new Date, newfile); + } + }); + }); +} + files.forEach(function(file) { var filename = __dirname + '/' + file; jadeify(filename, filemap[file]); - if ('--watch' == process.argv[2]) { - fs.watchFile(filename, { interval: 1000 }, function(cur, prev) { + if (process.argv[2] === '--watch') { + fs.watchFile(filename, {interval: 1000}, function(cur, prev) { if (cur.mtime > prev.mtime) { jadeify(filename, filemap[file]); } @@ -36,44 +85,3 @@ acquitFiles.forEach(function(file) { var filename = __dirname + '/docs/acquit.jade'; jadeify(filename, acquit[file], __dirname + '/docs/' + file); }); - -function jadeify(filename, options, newfile) { - options || (options = {}); - options.package = package; - options.hl = hl; - options.linktype = linktype; - options.href = href; - options.klass = klass; - jade.renderFile(filename, options, function(err, str) { - if (err) return console.error(err.stack); - - newfile = newfile || filename.replace('.jade', '.html'); - fs.writeFile(newfile, str, function(err) { - if (err) return console.error('could not write', err.stack); - console.log('%s : rendered ', new Date, newfile); - }); - }); -} - -function getVersion() { - var hist = fs.readFileSync('./History.md','utf8').replace(/\r/g, '\n').split('\n'); - for (var i = 0; i < hist.length; ++i) { - var line = (hist[i] || '').trim(); - if (!line) continue; - var match = /^\s*([^\s]+)\s/.exec(line); - if (match && match[1]) - return match[1]; - } - throw new Error('no match found'); -} - -function getUnstable(ver) { - ver = ver.replace("-pre"); - var spl = ver.split('.'); - spl = spl.map(function(i) { - return parseInt(i); - }); - spl[1]++; - spl[2] = "x"; - return spl.join('.'); -}