├── .babelrc ├── .coveralls.yml ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── gulpfile.babel.js ├── history.md ├── package-lock.json ├── package.json ├── readme.md ├── src ├── field.js ├── filter.js ├── index.js ├── keyword.js ├── order.js ├── page.js └── utils.js └── test └── src ├── field.js ├── filter.js ├── index.js ├── keyword.js ├── order.js ├── page.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | "istanbul" 6 | ] 7 | } 8 | }, 9 | "presets": [ 10 | [ 11 | "@babel/preset-env" 12 | ] 13 | ], 14 | "plugins": [ 15 | [ 16 | "@babel/plugin-transform-runtime", 17 | { 18 | "corejs": 2, 19 | "helpers": true, 20 | "regenerator": true, 21 | "useESModules": false 22 | } 23 | ] 24 | ], 25 | "sourceMaps": true 26 | } -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | browser: false 4 | es6: true 5 | mocha: true 6 | node: true 7 | parser: "babel-eslint" 8 | parserOptions: 9 | ecmaVersion: 6 10 | sourceType: "module" 11 | plugins: 12 | - "babel" 13 | rules: 14 | array-bracket-spacing: 15 | - 2 16 | - "never" 17 | array-callback-return: 2 18 | arrow-parens: 2 19 | arrow-spacing: 20 | - 2 21 | - before: true 22 | after: true 23 | babel/new-cap: 2 24 | babel/object-curly-spacing: 25 | - 2 26 | - "always" 27 | block-spacing: 28 | - 2 29 | - "always" 30 | brace-style: 31 | - 2 32 | - "1tbs" 33 | callback-return: 2 34 | camelcase: 2 35 | comma-dangle: 36 | - 2 37 | - "never" 38 | comma-spacing: 39 | - 2 40 | - before: false 41 | after: true 42 | comma-style: 43 | - 1 44 | - "last" 45 | curly: 2 46 | default-case: 2 47 | eqeqeq: 2 48 | generator-star-spacing: 49 | - 2 50 | - before: true 51 | after: false 52 | guard-for-in: 2 53 | handle-callback-err: 2 54 | no-await-in-loop: 2 55 | no-caller: 2 56 | no-case-declarations: 2 57 | no-cond-assign: 2 58 | no-confusing-arrow: 2 59 | no-console: 2 60 | no-const-assign: 2 61 | no-constant-condition: 2 62 | no-control-regex: 2 63 | no-debugger: 2 64 | no-dupe-args: 2 65 | no-dupe-keys: 2 66 | no-duplicate-case: 2 67 | no-empty: 2 68 | no-empty-character-class: 2 69 | no-empty-pattern: 2 70 | no-eq-null: 2 71 | no-eval: 2 72 | no-ex-assign: 2 73 | no-extra-bind: 2 74 | no-extra-boolean-cast: 2 75 | no-extra-label: 2 76 | no-extra-parens: 77 | - 2 78 | - "functions" 79 | no-extra-semi: 2 80 | no-fallthrough: 2 81 | no-func-assign: 2 82 | no-implicit-coercion: 83 | - 2 84 | - boolean: true 85 | number: true 86 | string: true 87 | no-implied-eval: 2 88 | no-inner-declarations: 2 89 | no-invalid-regexp: 2 90 | no-invalid-this: 2 91 | no-irregular-whitespace: 2 92 | no-iterator: 2 93 | no-labels: 2 94 | no-lone-blocks: 2 95 | no-loop-func: 2 96 | no-magic-numbers: 97 | - 2 98 | - ignore: [-1, 0, 1, 2] 99 | ignoreArrayIndexes: true 100 | no-mixed-requires: 101 | - 0 102 | - grouping: true 103 | allowCall: true 104 | no-mixed-spaces-and-tabs: 2 105 | no-multi-spaces: 2 106 | no-multi-str: 2 107 | no-native-reassign: 2 108 | no-negated-in-lhs: 2 109 | no-new: 2 110 | no-new-func: 2 111 | no-new-require: 2 112 | no-new-wrappers: 2 113 | no-obj-calls: 2 114 | no-octal: 2 115 | no-octal-escape: 2 116 | no-param-reassign: 0 117 | no-path-concat: 2 118 | no-proto: 2 119 | no-redeclare: 2 120 | no-regex-spaces: 2 121 | no-return-assign: 2 122 | no-self-assign: 2 123 | no-self-compare: 2 124 | no-sequences: 2 125 | no-sparse-arrays: 2 126 | no-sync: 2 127 | no-throw-literal: 2 128 | no-undef: 2 129 | no-undefined: 2 130 | no-unexpected-multiline: 2 131 | no-unmodified-loop-condition: 2 132 | no-unreachable: 2 133 | no-unused-expressions: 2 134 | no-unused-labels: 2 135 | no-unused-vars: 136 | - 2 137 | - vars: all 138 | no-useless-call: 2 139 | no-useless-concat: 2 140 | no-void: 2 141 | no-with: 2 142 | object-shorthand: 2 143 | quotes: 144 | - 2 145 | - "single" 146 | radix: 2 147 | semi: 2 148 | sort-imports: 149 | - 2 150 | - ignoreCase: true 151 | ignoreMemberSort: true 152 | sort-keys: 153 | - 2 154 | - "asc" 155 | - caseSensitive: false 156 | natural: true 157 | sort-vars: 158 | - 2 159 | - ignoreCase: true 160 | space-before-function-paren: 161 | - 2 162 | - "always" 163 | spaced-comment: ["error", "always"] 164 | use-isnan: 2 165 | valid-jsdoc: 2 166 | valid-typeof: 2 167 | vars-on-top: 2 168 | wrap-iife: 2 169 | yoda: 2 170 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | dist 4 | npm-debug.log 5 | lib-cov 6 | node_modules 7 | reports 8 | .idea/ 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | 4 | # Compiled source # 5 | ################### 6 | *.com 7 | *.class 8 | *.dll 9 | *.exe 10 | *.o 11 | *.so 12 | 13 | # Packages # 14 | ############ 15 | # it's better to unpack these files and commit the raw source 16 | # git has its own built in compression methods 17 | *.7z 18 | *.dmg 19 | *.gz 20 | *.iso 21 | *.jar 22 | *.rar 23 | *.tar 24 | *.zip 25 | 26 | # Logs and databases # 27 | ###################### 28 | *.log 29 | *.sql 30 | *.sqlite 31 | 32 | # OS generated files # 33 | ###################### 34 | .DS_Store? 35 | ehthumbs.db 36 | Icon? 37 | Thumbs.db 38 | 39 | # Everything else # 40 | ###################### 41 | .nyc_output 42 | .babelrc 43 | .coveralls.yml 44 | .gitignore 45 | .travis.yml 46 | .vscode 47 | gulpfile.babel.js 48 | lib-cov 49 | node_modules 50 | reports 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 10 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 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: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | 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 OR COPYRIGHT HOLDERS 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. -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import { dest, series, src } from 'gulp'; 2 | import babel from 'gulp-babel'; 3 | import del from 'gulp-clean'; 4 | import eslint from 'gulp-eslint'; 5 | import sourcemaps from 'gulp-sourcemaps'; 6 | 7 | function build () { 8 | return src('src/**/*.js') 9 | .pipe(sourcemaps.init()) 10 | .pipe(babel()) 11 | .pipe(sourcemaps.write('.')) 12 | .pipe(dest('dist')); 13 | } 14 | 15 | function clean () { 16 | return src(['dist', 'reports'], { allowEmpty : true, read : false }) 17 | .pipe(del()); 18 | } 19 | 20 | function lint () { 21 | return src(['gulpfile.babel.js', 'src/**/*.js', 'test/**/*.js']) 22 | .pipe(eslint()) 23 | .pipe(eslint.format()) 24 | .pipe(eslint.failAfterError()); 25 | } 26 | 27 | exports.build = build; 28 | exports.clean = clean; 29 | exports.default = series(clean, lint, build); 30 | exports.lint = lint; 31 | -------------------------------------------------------------------------------- /history.md: -------------------------------------------------------------------------------- 1 | # v2.0.0 / 2019-03-13 2 | 3 | * Modified to support ES6 4 | * Removed jshint and replaced with eslint 5 | * Adjusted to support Mongoose v5.x 6 | 7 | # v1.0.0 / 2016-12-19 8 | 9 | * Refactored sort parameter to comply with JSON API spec 10 | 11 | # v0.3.0 / 2016-10-19 12 | 13 | * Introduced fix for keyword filter where empty values caused a runtime exception (#33) 14 | * Introduced support for Promises (#26) 15 | 16 | # v0.2.20 / 2016-05-16 17 | 18 | * Added new function to intelligently merge filters for when you want to 19 | programmatically add new filters. 20 | 21 | # v0.2.19 / 2016-04-27 22 | 23 | * Addressed an issue that certain strings were incorrectly parsed as legitimate 24 | numbers and caused filters with EXACT phrases would fail 25 | 26 | # v0.2.18 / 2016-04-13 27 | 28 | * Adding support for `exists` mandatory filters 29 | 30 | # v0.2.17 / 2016-03-07 31 | 32 | * fix bug were not supplying `optional` search options would cause an exception 33 | 34 | # v0.2.16 / 2016-03-07 35 | 36 | * modern versions of mongoose expect the skip and limit parameter to be an int. 37 | * remove ability to specify gt,gte,lt,lte and ne parameters with an optional filter 38 | 39 | # v0.2.15 / 2016-03-04 40 | 41 | * Fixed issue where there was an incompatibility with mquery module in mongoose 42 | * Updated dependencies 43 | 44 | # v0.2.14 / 2016-02-22 45 | 46 | * Increasing code coverage of unit tests with minor refactors 47 | * Adding `gulp coveralls` task to end of Travis build 48 | * Adding support for `notEqual` mandatory and optional filters 49 | 50 | # v0.2.13 / 2015-11-11 51 | 52 | * Adding ability to specify filters as arrays via comma-delim strings 53 | 54 | # 0.2.12 / 2014-04-28 55 | 56 | * Fixing a bug with the sanitization of values prior to creating a regex match 57 | * Adding build support for Node v0.12 58 | 59 | # 0.2.11 / 2015-04-28 60 | 61 | * @schiang introduced fix for exact match on boolean values 62 | 63 | # 0.2.10 / 2014-12-31 64 | 65 | * Moving to gulp for build and testing 66 | * Fixed bug where `exact` matches to number values was not working 67 | * Introduced support for `greaterThan` and `lessThan` mandatory filters 68 | 69 | # 0.2.9 / 2014-08-28 70 | 71 | * Adding support for `endsWith` optional and mandatory filters 72 | 73 | # 0.2.8 / 2014-08-26 74 | 75 | * Modifying how Mongoose `#where` method is used in filters to be compliant with Mongoose 3.x 76 | 77 | # 0.2.7 / 2014-06-28 78 | 79 | * Removing testing on Node v0.8 from Travis CI 80 | 81 | # 0.2.6 / 2014-06-27 82 | 83 | * Fixing documentation to remove references to `execFind` 84 | * Fixing documentation to appropriately use `filters` in the input options 85 | 86 | # 0.2.5 / 2014-04-29 87 | 88 | * Further refined support for boolean properties 89 | 90 | # 0.2.4 / 2014-04-27 91 | 92 | * Fixed defect to enable support for boolean properties 93 | 94 | # 0.2.3 / 2014-02-11 95 | 96 | * Changed use of private Mongoose query method `execFind` to public method `exec` 97 | * Now compatible with Mongoose 3.7x and above 98 | 99 | # 0.2.2 / 2013-06-17 100 | 101 | * Fixed issue to allow count to accept a value of `0` 102 | 103 | # 0.2.1 / 2013-04-23 104 | 105 | * Initial release to public 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-middleware", 3 | "description": "Middleware for mongoose that makes filtering, sorting, pagination and projection chainable and simple to apply", 4 | "version": "2.0.1", 5 | "keywords": [ 6 | "mongo", 7 | "mongoose", 8 | "mongoose middlware", 9 | "mongoose-middleware" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:PlayNetwork/mongoose-middleware.git" 15 | }, 16 | "main": "dist", 17 | "scripts": { 18 | "lint": "gulp lint", 19 | "posttest": "nyc report --reporter=text-lcov | coveralls", 20 | "prepare": "gulp build", 21 | "pretest": "gulp clean && gulp lint", 22 | "test": "NODE_ENV=test nyc mocha ./test/src", 23 | "test:unit": "NODE_ENV=test nyc mocha ./test/src" 24 | }, 25 | "nyc": { 26 | "all": true, 27 | "include": [ 28 | "src" 29 | ], 30 | "instrument": false, 31 | "report-dir": "./reports", 32 | "reporter": [ 33 | "lcov", 34 | "text", 35 | "text-summary" 36 | ], 37 | "require": [ 38 | "@babel/register" 39 | ], 40 | "sourceMap": false 41 | }, 42 | "devDependencies": { 43 | "@babel/cli": "^7.2.3", 44 | "@babel/core": "^7.3.4", 45 | "@babel/plugin-transform-runtime": "^7.3.4", 46 | "@babel/preset-env": "^7.3.4", 47 | "@babel/register": "^7.0.0", 48 | "babel-eslint": "^10.0.1", 49 | "babel-plugin-istanbul": "^5.1.1", 50 | "chai": "^4.2.0", 51 | "coveralls": "^3.0.3", 52 | "eslint-plugin-babel": "^5.3.0", 53 | "gulp": "^4.0.0", 54 | "gulp-babel": "^8.0.0", 55 | "gulp-clean": "^0.4.0", 56 | "gulp-eslint": "^5.0.0", 57 | "gulp-sourcemaps": "^2.6.5", 58 | "mocha": "^6.0.2", 59 | "mongoose": "^5.4.19", 60 | "nyc": "^13.3.0" 61 | }, 62 | "dependencies": { 63 | "@babel/runtime-corejs2": "^7.3.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Mongoose Middleware 2 | 3 | [![Build Status](https://secure.travis-ci.org/PlayNetwork/mongoose-middleware.png?branch=master)](http://travis-ci.org/PlayNetwork/mongoose-middleware?branch=master) [![Coverage Status](https://coveralls.io/repos/PlayNetwork/mongoose-middleware/badge.png)](https://coveralls.io/r/PlayNetwork/mongoose-middleware) 4 | 5 | ## Features 6 | 7 | * Pagination (start, count and total matching) 8 | * Filtering (mandatory matches, optional matches and keyword search) 9 | * Sorting (ascending and descending) 10 | * Projection (response field filtering) 11 | * Promise support 12 | 13 | ## Install 14 | 15 | ```javascript 16 | npm install mongoose-middleware 17 | ``` 18 | 19 | Then, simply require the library and pass in the instance of the `require('mongoose')` statement to the initialize method as follows: 20 | 21 | ```javascript 22 | var mongoose = require('mongoose'); 23 | 24 | require('mongoose-middleware').initialize(mongoose); 25 | ``` 26 | 27 | Optionally configure max documents for pagination: 28 | 29 | ```javascript 30 | var mongoose = require('mongoose'); 31 | 32 | require('mongoose-middleware') 33 | .initialize({ 34 | maxDocs : 1000 35 | }, mongoose); 36 | ``` 37 | 38 | ## Overview 39 | 40 | This project aims to make basic searching, sorting, filtering and projection tasks against documents stored in MongoDB trivial via Mongoose middleware. The middle exposes a set of Mongoose Query object chainable methods for ease and simplicity of use. 41 | 42 | The following example shows usage of field projections, mandatory and optional search filters, sorting and pagination. 43 | 44 | ```javascript 45 | var 46 | mongoose = require('mongoose'), 47 | Schema = mongoose.Schema, 48 | KittehModel = mongoose.model( 49 | 'kittehs', 50 | new Schema({ 51 | birthday : { type : Date, default : Date.now }, 52 | features : { 53 | color : String, 54 | isFurreh : Boolean 55 | }, 56 | home : String, 57 | name : String, 58 | peePatches : [String] 59 | }) 60 | ); 61 | 62 | require('mongoose-middleware').initialize(mongoose); 63 | 64 | /* 65 | Retrieve the name, home and features.color of kittehs that live in Seattle, 66 | that are named "Hamish" and that are brindle, black or white in color and born 67 | prior to January 1st, 2014. The results should be sorted by birthday in 68 | descending order and name in ascending order. 69 | */ 70 | var options = { 71 | filters : { 72 | field : ['name', 'home', 'features.color'], 73 | mandatory : { 74 | contains : { 75 | home : 'seattle' 76 | }, 77 | exact : { 78 | name : 'Hamish' 79 | }, 80 | lessThan : { 81 | birthday : new Date(2014, 1, 1) 82 | } 83 | }, 84 | optional : { 85 | contains : { 86 | 'features.color' : ['brindle', 'black', 'white'] 87 | } 88 | } 89 | }, 90 | sort : ['-birthday', 'name'], 91 | start : 0, 92 | count : 500 93 | }; 94 | 95 | KittehModel 96 | .find() 97 | .field(options) 98 | .keyword(options) 99 | .filter(options) 100 | .order(options) 101 | .page(options, 102 | function (err, kittehs) { 103 | if (!err) { 104 | console.log('we haz kittehs!'); 105 | console.log(kittehs); 106 | } else { 107 | console.log(err); 108 | } 109 | }); 110 | ``` 111 | 112 | ### Promise Support 113 | 114 | When using `mongoose-middleware`, the library does not interfere with existing [Mongoose support for Promises](http://mongoosejs.com/docs/promises.html). The [`#page`](#pagination) method will return a native Promise if the `callback` argument is not specified. 115 | 116 | ```javascript 117 | var options = { 118 | start : 0, 119 | count : 500 120 | }; 121 | 122 | KittehModel 123 | .find() 124 | .page(options) 125 | .then((kittehs) => { 126 | console.log('we haz kittehs!'); 127 | console.log(kittehs); 128 | }) 129 | .catch(console.error); 130 | ``` 131 | 132 | ### Results 133 | 134 | The options submitted to the `page(options, callback)` middleware method are echoed back in the response along with the results of the query and the total count of results matching the specified filters. 135 | 136 | ```javascript 137 | { 138 | options : { 139 | count : 500, 140 | filters : { 141 | field : ['name', 'home', 'features.color'], 142 | mandatory : { 143 | contains : { 144 | 'features.color' : ['brindle', 'black', 'white'] 145 | }, 146 | exact : { 147 | name : 'Hamish' 148 | } 149 | }, 150 | optional : { 151 | contains : { 152 | home : 'seattle' 153 | } 154 | } 155 | }, 156 | sort : ['-birthday', 'name'], 157 | start : 0 158 | }, 159 | results : [ ... ], // the first 500 brindled, black or white kittehs named Hamish in Seattle 160 | total : 734 161 | } 162 | ``` 163 | 164 | ## API 165 | 166 | ### Initialization 167 | 168 | The maxDocs property may optionally be specified on initialize to ensure no more than the specified number of documents are ever returned from a query. Please note that this does not affect the ability for the component to return the correct total count of results when using the pagination middleware function. 169 | 170 | ```javascript 171 | var mongoose = require('mongoose'); 172 | 173 | require('mongoose-middleware').initialize({ 174 | maxDocs : 1000 175 | }, mongoose); 176 | ``` 177 | 178 | ### Projection (Field Filters) 179 | 180 | In order specify specific fields from a document in Mongo to be returned, the fields filter may be used. 181 | 182 | ```javascript 183 | var options = { 184 | filters : { 185 | field : ['name', 'home', 'qualities.demeanor'] 186 | } 187 | }; 188 | 189 | KittehModel 190 | .find() 191 | .field(options) 192 | .exec(function (err, results) { 193 | // work with response... 194 | }); 195 | ``` 196 | 197 | Alternatively, a single field can be specified (not in an array): 198 | 199 | ```javascript 200 | KittehModel 201 | .find() 202 | .field({ filters : { field : '_id' } }) 203 | .exec(callback); 204 | ``` 205 | 206 | ### Filters 207 | 208 | Filters can be used in three ways: mandatory, optional and keyword searches. Additionally, for mandatory and optional searches, exact, equals, contains and startsWith string pattern matches may be used. 209 | 210 | The following filters can be used for *mandatory*, *optional*, and *keyword* searches. 211 | 212 | * `equals` - Matches for string identity 213 | * `exact` - Matches the string letter for letter, but is not case sensitive 214 | * `contains` - Matches documents where the string exists as a substring of the field (similar to a where field like '%term%' query in a relational datastore) 215 | * `startsWith` - Matches documents where field begins with the string supplied (similar to a where field like 'term%' query in a relational datastore) 216 | * `endsWith` - Matches documents where field ends with the string supplied (similar to a where field like '%term' query in a relational datastore) 217 | 218 | The following filters can *ONLY* be used for *mandatory* and *keyword* searches. 219 | * `greaterThan` (or `gt`) - Matches documents where field value is greater than supplied number or Date value in query 220 | * `greaterThanEqual` (or `gte`) - Matches documents where field value is greater than or equal to supplied number or Date value in query 221 | * `lessThan` (or `lt`) - Matches documents where field value is less than supplied number or Date value in query 222 | * `lessThanEqual` (or `lte`) - Matches documents where field value is less than or equal to supplied number or Date value in query 223 | * `notEqual` (or `ne`) - Matches documents where field value is not equal to the supplied value 224 | 225 | #### Mandatory 226 | 227 | Mandatory filters require that the document matches the specified search options or they will not be returned. 228 | 229 | #### Optional 230 | 231 | Optional searches allow you to specify more than one filter that you would like to match results for. This type of search is great for cases where you need to find documents that either match "this" *OR* "that". As an example, image you are searching for cats that are either manx, siamese or tabby, you would configure the filter as follows: 232 | 233 | ```javascript 234 | var options = { 235 | filters : { 236 | optional : { 237 | exact : { 238 | breed : ['manx', 'siamese', 'tabby'] 239 | } 240 | } 241 | } 242 | }; 243 | 244 | KittehModel 245 | .find() 246 | .filter(options) 247 | .exec(function (err, results) { 248 | // work with response... 249 | }); 250 | ``` 251 | 252 | #### Keyword 253 | 254 | Keyword searches provide a convenient way to search more than one field with a single string. Additionally, keyword filters work differently from mandatory and optional filters in that they do not support `exact`, `contains` or `startsWith`. Instead the matches look for occurrences in a similar way to `contains` but with the ability to specify multiple terms in the query. 255 | 256 | The following query will search for documents where the name, description or knownAliases contain Heathcliff the Cat. If the name (or description and knownAliases) contains "Cat, the Heathcliff", "the Cat, Heathcliff", "Heathcliff Cat, the" and "the Heathcliff Cat", those results will also be returned. 257 | 258 | ```javascript 259 | var options = { 260 | filters : { 261 | keyword : { 262 | fields : ['name', 'description', 'knownAliases'], 263 | term : 'Heathcliff the Cat' 264 | } 265 | } 266 | }; 267 | 268 | KittehModel 269 | .find() 270 | .filter(options) 271 | .exec(function (err, results) { 272 | // work with response... 273 | }); 274 | ``` 275 | 276 | If you would like to ensure that matches of "Heathcliff the Cat" in that exact format are returned, simply enclose the term in quotes: 277 | 278 | ```javascript 279 | var options = { 280 | filters : { 281 | keyword : { 282 | fields : ['name', 'description', 'knownAliases'], 283 | term : '"Heathcliff the Cat"' 284 | } 285 | } 286 | }; 287 | ``` 288 | 289 | ### Sorting 290 | 291 | Sorting, at this point, is fairly basic. All descending sorts will be applied prior to ascending sorts when specifying multiple sorts of each direction. Supports JSON API specs. 292 | 293 | #### Descending 294 | 295 | ```javascript 296 | var options = { 297 | sort : ['-name', '-description', '-knownAliases'] 298 | }; 299 | 300 | KittehModel 301 | .find() 302 | .order(options) 303 | .exec(function (err, results) { 304 | // work with response... 305 | }); 306 | ``` 307 | 308 | You may also specify a single field (not an array) as well as an object for both descending and ascending sorts: 309 | 310 | ```javascript 311 | var options = { 312 | sort : '-name' 313 | }; 314 | ``` 315 | 316 | ```javascript 317 | var options = { 318 | sort : { 319 | 'name': -1, 320 | 'description': 1 321 | } 322 | }; 323 | ``` 324 | 325 | #### Ascending 326 | 327 | ```javascript 328 | var options = { 329 | sort : ['name', 'description', 'knownAliases'] 330 | }; 331 | 332 | KittehModel 333 | .find() 334 | .order(options) 335 | .exec(function (err, results) { 336 | // work with response... 337 | }); 338 | ``` 339 | 340 | You may also specify ascending and descending sorts together: 341 | 342 | ```javascript 343 | var options = { 344 | sort : ['name', '-birthday', '-home'] 345 | }; 346 | ``` 347 | 348 | ### Pagination 349 | 350 | Pagination is performed by swapping the `exec()` function of Mongoose with `page()`. Pagination may be specified as follows: 351 | 352 | ```javascript 353 | var options = { 354 | start : 0, 355 | count : 100 356 | }; 357 | 358 | KittehModel 359 | .find() 360 | .page(options, function (err, results) { 361 | // work with response... 362 | }); 363 | ``` 364 | 365 | When using pagination, maxDocs may specified via the `initialize()` function of the library which will result in no more than that maximum number of documents being returned. 366 | 367 | ```javascript 368 | var 369 | mongoose = require('mongoose'), 370 | KittehModel = require('./models/kitteh'); 371 | 372 | require('mongoose-middleware').initialize({ maxDocs : 50 }, mongoose); 373 | 374 | var options = { 375 | start : 0, 376 | count : 100 377 | }; 378 | 379 | KittehModel 380 | .find() 381 | .page(options, function (err, results) { 382 | // results.options.count === 50 383 | }); 384 | ``` 385 | 386 | *Please note*: While the maxDocs will limit the number of returned documents, it will not affect the total count value of matching documents. 387 | 388 | #### Response 389 | 390 | Pagination returns the specified start, count and overall total numer of matching documents as a wrapper to the results from Mongo. 391 | 392 | ```javascript 393 | { 394 | options : { 395 | count : 50, 396 | start : 0 397 | }, 398 | results : [ ... ], 399 | total : 734 400 | } 401 | ``` 402 | 403 | ## Utility Methods 404 | 405 | ### mergeFilters 406 | 407 | mongoose-middleware provides a helper function if you need to programmatically 408 | add filters to the query. It will intelligently merge structures, and ensure 409 | that elements are turned into Arrays when they need to be. 410 | 411 | #### Example 412 | 413 | ```javascript 414 | var base = { 415 | filters : { 416 | mandatory : { 417 | exact : { 418 | breed : ['manx', 'siamese', 'tabby'], 419 | name : 'Ballard' 420 | } 421 | } 422 | } 423 | }, 424 | model = { 425 | filters : { 426 | mandatory : { 427 | exact : { 428 | breed : 'calico', 429 | name : 'Fremont' 430 | } 431 | } 432 | } 433 | }, 434 | merged = require('mongoose-middleware').mergeFilters(base, model); 435 | ``` 436 | 437 | #### Result 438 | 439 | ```javascript 440 | { 441 | filters : { 442 | mandatory : { 443 | exact : { 444 | breed : ['manx', 'siamese', 'tabby', 'calico'], 445 | name : ['Ballard', 'Fremont'] 446 | } 447 | } 448 | } 449 | } 450 | ``` 451 | 452 | 453 | ## License 454 | 455 | MIT Style 456 | 457 | ```text 458 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 459 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 460 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 461 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 462 | 463 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 464 | of the Software. 465 | 466 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 467 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 468 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 469 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 470 | IN THE SOFTWARE. 471 | ``` 472 | -------------------------------------------------------------------------------- /src/field.js: -------------------------------------------------------------------------------- 1 | export default (mongoose) => { 2 | mongoose.Query.prototype.field = function (options) { 3 | let query = this; 4 | 5 | if (options && options.filters && options.filters.field) { 6 | if (typeof options.filters.field === 'string' && /\,/g.test(options.filters.field)) { 7 | options.filters.field = options.filters.field.split(/\,/g); 8 | } 9 | 10 | if (Array.isArray(options.filters.field) && options.filters.field.length) { 11 | options.filters.field.forEach((field) => { 12 | if (query.model.schema.path(field)) { 13 | query.select(field.trim()); 14 | } 15 | }); 16 | } else if (query.model.schema.path(options.filters.field)) { 17 | query.select(options.filters.field.trim()); 18 | } 19 | } 20 | 21 | return query; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | export default (mongoose) => { 2 | function analyzeWhereSpec (val) { 3 | if (typeof val === 'string') { 4 | switch (val.toLowerCase()) { 5 | case 'null' : return null; 6 | case 'true' : return true; 7 | case 'false' : return false; 8 | default : 9 | // use a regex to validate if val is a real parse-able number 10 | // javascript isNaN() treats a string such as '100000329e97' as 11 | // a legitimate number, which is not a desirable result 12 | // e.g. both Number('100000329e97'), parseInt('100000329e97') 13 | // yield a number 100000329, and isNaN('100000329e97') === false 14 | if (/^[-+]?[0-9]*\.?[0-9]+$/.test(val)) { 15 | // val is a number 16 | if (val.indexOf('.') > -1) { 17 | // val is a float 18 | return parseFloat(val); 19 | } else { 20 | return parseInt(val, 10); 21 | } 22 | } 23 | } 24 | } 25 | 26 | return val; 27 | } 28 | 29 | function applyExists (query, spec = {}) { 30 | Object.keys(spec).forEach((key) => { 31 | query.where(key).exists(analyzeWhereSpec(spec[key])); 32 | }); 33 | } 34 | 35 | function applyGreaterThan (query, spec = {}) { 36 | Object.keys(spec).forEach((key) => { 37 | query.where(key).gt(spec[key]); 38 | }); 39 | } 40 | 41 | function applyGreaterThanEqual (query, spec = {}) { 42 | Object.keys(spec).forEach((key) => { 43 | query.where(key).gte(spec[key]); 44 | }); 45 | } 46 | 47 | function applyLesserThan (query, spec = {}) { 48 | Object.keys(spec).forEach((key) => { 49 | query.where(key).lt(spec[key]); 50 | }); 51 | } 52 | 53 | function applyLesserThanEqual (query, spec = {}) { 54 | Object.keys(spec).forEach((key) => { 55 | query.where(key).lte(spec[key]); 56 | }); 57 | } 58 | 59 | function applyNotEqual (query, spec = {}) { 60 | Object.keys(spec).forEach((key) => { 61 | query.where(key).ne(analyzeWhereSpec(spec[key])); 62 | }); 63 | } 64 | 65 | function applyRegex (query, spec = {}, buildRegex) { 66 | Object.keys(spec).forEach((key) => { 67 | let val = buildRegex(spec[key]); 68 | if (Array.isArray(val)) { 69 | val.forEach((term) => { 70 | query.where(key, term); 71 | }); 72 | } else { 73 | query.where(key, val); 74 | } 75 | }); 76 | } 77 | 78 | function applyRegexAsOptional (query, spec = {}, buildRegex) { 79 | let 80 | orOptions = [], 81 | orOptionsNode = {}; 82 | 83 | Object.keys(spec).forEach((key) => { 84 | let val = buildRegex(spec[key]); 85 | 86 | if (Array.isArray(val)) { 87 | orOptions = orOptions.concat((() => { 88 | let 89 | node = {}, 90 | nodeOptions = []; 91 | 92 | val.forEach((term) => { 93 | node = {}; 94 | node[key] = term; 95 | nodeOptions.push(node); 96 | }); 97 | 98 | return nodeOptions; 99 | })()); 100 | } else { 101 | orOptionsNode = {}; 102 | orOptionsNode[key] = val; 103 | orOptions.push(orOptionsNode); 104 | } 105 | }); 106 | 107 | if (orOptions.length > 0) { 108 | query.or(orOptions); 109 | } 110 | } 111 | 112 | function regexContains (val) { 113 | if (Array.isArray(val) && val.length) { 114 | return val.map(function (term) { 115 | return regexContains(term); 116 | }); 117 | } 118 | 119 | if (typeof val === 'string') { 120 | return new RegExp(sanitize(val), 'i'); 121 | } 122 | 123 | return val; 124 | } 125 | 126 | function regexEndsWith (val) { 127 | if (Array.isArray(val) && val.length) { 128 | return val.map(function (term) { 129 | return regexEndsWith(term); 130 | }); 131 | } 132 | 133 | return new RegExp(sanitize(val) + '$', 'i'); 134 | } 135 | 136 | function regexExact (val) { 137 | if (Array.isArray(val) && val.length) { 138 | return val.map(function (term) { 139 | return regexExact(term); 140 | }); 141 | } 142 | 143 | val = analyzeWhereSpec(val); 144 | 145 | if (typeof val === 'string') { 146 | return new RegExp('^' + sanitize(val) + '$', 'i'); 147 | } 148 | 149 | return val; 150 | } 151 | 152 | function regexStartsWith (val) { 153 | if (Array.isArray(val) && val.length) { 154 | return val.map(function (term) { 155 | return regexStartsWith(term); 156 | }); 157 | } 158 | 159 | return new RegExp('^' + sanitize(val), 'i'); 160 | } 161 | 162 | function regexEquals (val) { 163 | if (Array.isArray(val) && val.length) { 164 | return val.map(function (term) { 165 | return regexEquals(term); 166 | }); 167 | } 168 | 169 | val = analyzeWhereSpec(val); 170 | 171 | return val; 172 | } 173 | 174 | function sanitize (str) { 175 | // sanitizes regex escapes 176 | return str.replace(/[\W\s]/ig, '\\$&'); 177 | } 178 | 179 | mongoose.Query.prototype.filter = function (options) { 180 | if (!options || !options.filters) { 181 | return this; 182 | } 183 | 184 | let 185 | mandatory = options.filters.mandatory || {}, 186 | optional = options.filters.optional || {}, 187 | query = this; 188 | 189 | // MANDATORY 190 | applyRegex(query, mandatory.contains, regexContains); 191 | applyRegex(query, mandatory.endsWith, regexEndsWith); 192 | applyRegex(query, mandatory.startsWith, regexStartsWith); 193 | applyRegex(query, mandatory.equals, regexEquals); 194 | applyRegex(query, mandatory.exact, regexExact); 195 | 196 | applyExists( 197 | query, 198 | mandatory.exists || {}); 199 | applyGreaterThan( 200 | query, 201 | mandatory.greaterThan || mandatory.gt || {}); 202 | applyGreaterThanEqual( 203 | query, 204 | mandatory.greaterThanEqual || mandatory.gte || {}); 205 | applyLesserThan( 206 | query, 207 | mandatory.lessThan || mandatory.lt || {}); 208 | applyLesserThanEqual( 209 | query, 210 | mandatory.lessThanEqual || mandatory.lte || {}); 211 | applyNotEqual( 212 | query, 213 | mandatory.notEqual || mandatory.notEqualTo || mandatory.ne || {}); 214 | 215 | // OPTIONAL 216 | applyRegexAsOptional(query, optional.contains, regexContains); 217 | applyRegexAsOptional(query, optional.endsWith, regexEndsWith); 218 | applyRegexAsOptional(query, optional.startsWith, regexStartsWith); 219 | applyRegexAsOptional(query, optional.equals, regexEquals); 220 | applyRegexAsOptional(query, optional.exact, regexExact); 221 | 222 | return query; 223 | }; 224 | }; 225 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import field from './field'; 2 | import filter from './filter'; 3 | import keyword from './keyword'; 4 | import order from './order'; 5 | import page from './page'; 6 | 7 | export default ((self = {}) => { 8 | self.initialize = function (options, mongoose) { 9 | 10 | if (typeof mongoose === 'undefined') { 11 | mongoose = options; 12 | options = null; 13 | } 14 | 15 | // require all modules 16 | field(mongoose); 17 | filter(mongoose); 18 | keyword(mongoose); 19 | order(mongoose); 20 | page(mongoose).initialize(options); 21 | }; 22 | 23 | self.utils = require('./utils'); 24 | 25 | return self; 26 | })(); -------------------------------------------------------------------------------- /src/keyword.js: -------------------------------------------------------------------------------- 1 | export default (mongoose) => { 2 | const 3 | Query = mongoose.Query, 4 | Schema = mongoose.Schema; 5 | 6 | function getKeywordRegex (term) { 7 | let 8 | matches = [], 9 | pattern = ''; 10 | 11 | // this splits the string at each space except those within double quotes 12 | matches = term.match(/\w+|"[^"]+"/g); 13 | 14 | // fix for #33 - empty keywords cause exception 15 | if (matches) { 16 | matches.forEach((t) => { 17 | // remove quotes 18 | t = t.replace(/\"/g, ''); 19 | 20 | // sanitize for regex (strips everything except letters, numbers, underscores, single quotes and whitespace) 21 | t = t.replace(/\W\s/ig, '\\$&'); 22 | 23 | // replace spaces with escapes 24 | t = t.replace(' ', '\\s'); 25 | 26 | pattern += '(?=.*' + t + ')'; 27 | }); 28 | } 29 | 30 | return pattern; 31 | } 32 | 33 | 34 | Query.prototype.keyword = function (options) { 35 | // ensure keyword exists in query 36 | if (!options || !options.filters || !options.filters.keyword) { 37 | return this; 38 | } 39 | 40 | let 41 | fields = options.filters.keyword.fields || [], 42 | find = null, 43 | or = [], 44 | query = this, 45 | re = null, 46 | term = options.filters.keyword.term || ''; 47 | 48 | if (!fields.length || term === '') { 49 | return query; 50 | } 51 | 52 | re = new RegExp(getKeywordRegex(term), 'i'); 53 | fields.forEach((field) => { 54 | // field is an Array; use $in to incorperate keyword for search 55 | if (query.model.schema.path(field) && query.model.schema.path(field) instanceof Schema.Types.Array) { 56 | find = {}; 57 | find[field] = {}; 58 | find[field].$in = [re]; 59 | or.push(find); 60 | } else { 61 | find = {}; 62 | find[field] = re; 63 | or.push(find); 64 | } 65 | }); 66 | 67 | query.or(or); 68 | 69 | return query; 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /src/order.js: -------------------------------------------------------------------------------- 1 | export default (mongoose) => { 2 | mongoose.Query.prototype.order = function (options) { 3 | if (!options || !options.sort) { 4 | return this; 5 | } 6 | 7 | let 8 | fields = [], 9 | query = this, 10 | sort = options.sort || {}, 11 | value = null; 12 | 13 | if (typeof sort === 'string') { 14 | fields = sort 15 | .split(/\,/g) 16 | .map((field) => field.trim()); 17 | } else if (Array.isArray(sort) && sort.length) { 18 | fields = sort; 19 | } else if (typeof sort === 'object') { 20 | Object.keys(sort).forEach((property) => { 21 | if (!isNaN(sort[property])) { 22 | if (parseInt(sort[property], 10) < 0) { 23 | fields.push('-' + property); 24 | } else { 25 | fields.push(property); 26 | } 27 | } else { 28 | // property supplied is NaN; default to 1/ascending 29 | fields.push(property); 30 | } 31 | }); 32 | } 33 | 34 | fields.forEach((field) => { 35 | value = {}; 36 | if (field.startsWith('-')) { 37 | value[field.substring(1)] = -1; 38 | } else { 39 | value[field] = 1; 40 | } 41 | 42 | query.sort(value); 43 | }); 44 | 45 | return query; 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/page.js: -------------------------------------------------------------------------------- 1 | export default (mongoose) => { 2 | let 3 | maxDocs = -1, 4 | self = {}; 5 | 6 | function page (query, options) { 7 | return new Promise(function (resolve, reject) { 8 | return query.model.countDocuments(query._conditions, function (err, total) { 9 | if (err) { 10 | return reject(err); 11 | } 12 | 13 | query.setOptions({ 14 | limit : options.count, 15 | skip : options.start 16 | }); 17 | 18 | return query.exec((err, results) => { 19 | if (err) { 20 | return reject(err); 21 | } 22 | 23 | return resolve({ 24 | options, 25 | results : results || [], 26 | total 27 | }); 28 | }); 29 | }); 30 | }); 31 | } 32 | 33 | mongoose.Query.prototype.page = function (options, callback) { 34 | let 35 | defaults = { 36 | count : maxDocs, 37 | start : 0 38 | }, 39 | query = this; 40 | 41 | options = options || defaults; 42 | // this might be getting a little long; 43 | options.start = (options && options.start && parseInt(options.start, 10) ? parseInt(options.start, 10) : defaults.start); 44 | options.count = (options && options.count && parseInt(options.count, 10) ? parseInt(options.count, 10) : defaults.count); 45 | 46 | if (maxDocs > 0 && (options.count > maxDocs || options.count === 0)) { 47 | options.count = maxDocs; 48 | } 49 | 50 | // if no callback is supplied, return a Promise 51 | if (typeof callback === 'undefined') { 52 | return page(query, options); 53 | } 54 | 55 | // execute and utilize the callback 56 | return page(query, options) 57 | .then(function (result) { 58 | return callback(null, result); 59 | }) 60 | .catch(function (err) { 61 | return callback(err); 62 | }); 63 | }; 64 | 65 | self.initialize = (options) => { 66 | if (options) { 67 | maxDocs = options.maxDocs || maxDocs; 68 | } 69 | }; 70 | 71 | return self; 72 | }; 73 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function mergeFilters (base, model) { 2 | Object.keys(model).forEach(function (key) { 3 | if (!base[key]) { // base[key] is not present 4 | base[key] = model[key]; 5 | } else if (Array.isArray(base[key])) { // base[key] is an array 6 | if (Array.isArray(model[key])) { 7 | base[key] = base[key].concat(model[key]); 8 | } else { 9 | base[key].push(model[key]); 10 | } 11 | } else if (Array.isArray(model[key])) { 12 | model[key].push(base[key]); 13 | base[key] = model[key]; 14 | } else if (typeof base[key] !== 'object') { 15 | base[key] = [base[key], model[key]]; // turn into array 16 | } else { // base[key] is likely JSON 17 | if (typeof model[key] === 'object') { 18 | base[key] = mergeFilters(base[key], model[key]); 19 | } 20 | // if base[key] is an object, and model[key], ignore 21 | } 22 | }); 23 | 24 | return base; 25 | } 26 | 27 | export default { 28 | mergeFilters 29 | }; 30 | -------------------------------------------------------------------------------- /test/src/field.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import fieldLib from '../../src/field'; 3 | import mongoose from 'mongoose'; 4 | 5 | const should = chai.should(); 6 | 7 | describe('field', () => { 8 | let 9 | fieldsSelected = [], 10 | Kitteh = mongoose.model('kittehs-field', new mongoose.Schema({ 11 | birthday : { 12 | default : Date.now, 13 | type : Date 14 | }, 15 | features : { 16 | color : String, 17 | isFurreh : Boolean 18 | }, 19 | home : String, 20 | isDead: Boolean, 21 | name : String, 22 | peePatches : [String] 23 | })); 24 | 25 | before(() => { 26 | fieldLib(mongoose); 27 | 28 | mongoose.Query.prototype.select = (field) => { 29 | if (field) { 30 | fieldsSelected.push(field); 31 | } 32 | }; 33 | }); 34 | 35 | beforeEach(() => { 36 | fieldsSelected = []; 37 | }); 38 | 39 | it ('should return a query when created', () => { 40 | let query = Kitteh 41 | .find() 42 | .field(null); 43 | 44 | (query instanceof mongoose.Query).should.equals(true); 45 | }); 46 | 47 | it ('should only select fields when multiple fields are supplied', () => { 48 | let options = { 49 | filters : { 50 | field : ['birthday', 'name'] 51 | } 52 | }; 53 | 54 | let query = Kitteh 55 | .find() 56 | .field(options); 57 | 58 | should.exist(query); 59 | fieldsSelected.should.have.length(2); 60 | }); 61 | 62 | it ('should only select one field when one field is supplied', () => { 63 | let options = { 64 | filters : { 65 | field : ['name'] 66 | } 67 | }; 68 | 69 | let query = Kitteh 70 | .find() 71 | .field(options); 72 | 73 | should.exist(query); 74 | fieldsSelected.should.have.length(1); 75 | fieldsSelected[0].should.equals('name'); 76 | }); 77 | 78 | it ('should select one field when one field is supplied and not an array', () => { 79 | let options = { 80 | filters : { 81 | field : 'name' 82 | } 83 | }; 84 | 85 | let query = Kitteh 86 | .find() 87 | .field(options); 88 | 89 | should.exist(query); 90 | fieldsSelected.should.have.length(1); 91 | fieldsSelected[0].should.equals('name'); 92 | }); 93 | 94 | it ('should select all fields when one field is supplied and not an array but does not exist in schema', () => { 95 | let options = { 96 | filters : { 97 | field : 'notinschema' 98 | } 99 | }; 100 | 101 | let query = Kitteh 102 | .find() 103 | .field(options); 104 | 105 | should.exist(query); 106 | fieldsSelected.should.have.length(0); 107 | }); 108 | 109 | it ('should select all model fields when options are null', () => { 110 | let query = Kitteh 111 | .find() 112 | .field(null); 113 | 114 | should.exist(query); 115 | fieldsSelected.should.have.length(0); 116 | }); 117 | 118 | it ('should select all model fields when options contain filters, but not field', () => { 119 | let query = Kitteh 120 | .find() 121 | .field({ filters : {} }); 122 | 123 | should.exist(query); 124 | fieldsSelected.should.have.length(0); 125 | }); 126 | 127 | it ('should split comma delim strings when supplied for field', () => { 128 | let query = Kitteh 129 | .find() 130 | .field({ filters : { field : 'home,name,unknownField' } }); 131 | 132 | should.exist(query); 133 | fieldsSelected.should.have.length(2); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/src/filter.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers : 0 */ 2 | import chai from 'chai'; 3 | import filterLib from '../../src/filter'; 4 | import mongoose from 'mongoose'; 5 | 6 | const should = chai.should(); 7 | 8 | describe('filter', () => { 9 | let 10 | Kitteh = mongoose.model('kittehs-filter', new mongoose.Schema({ 11 | birthday : { 12 | default : Date.now, 13 | type : Date 14 | }, 15 | features : { 16 | color : String, 17 | isFurreh : Boolean 18 | }, 19 | home : String, 20 | isDead: Boolean, 21 | name : String, 22 | peePatches : [String] 23 | })), 24 | orClause = {}, 25 | whereClause = {}; 26 | 27 | before(() => { 28 | filterLib(mongoose); 29 | 30 | mongoose.Query.prototype.or = (orOptions) => { 31 | 32 | if (Array.isArray(orOptions)) { 33 | orOptions.forEach((elem) => { 34 | for (let x in elem) { 35 | if (elem.hasOwnProperty(x)) { 36 | if (orClause[x]) { 37 | let newVal = [orClause[x], elem[x]]; 38 | orClause[x] = newVal; 39 | } else { 40 | orClause[x] = elem[x]; 41 | } 42 | } 43 | } 44 | }); 45 | } 46 | 47 | // it doesn't seem the mquery/mongoose supports subsequent gt,lt, 48 | // gte,lte,ne filtering for or queries, however prior to v0.2.16 of 49 | // mongoose-middleware some features were built as though it was 50 | // supported. this will give us some indication if any code remains 51 | // that tries to use these filtering options 52 | return { 53 | gt : () => { 54 | throw new Error( 55 | 'mongoose.Query.prototype.or does not support gt'); 56 | }, 57 | gte : () => { 58 | throw new Error( 59 | 'mongoose.Query.prototype.or does not support gte'); 60 | }, 61 | lt : () => { 62 | throw new Error( 63 | 'mongoose.Query.prototype.or does not support lt'); 64 | }, 65 | lte : () => { 66 | throw new Error( 67 | 'mongoose.Query.prototype.or does not support lte'); 68 | }, 69 | ne : () => { 70 | throw new Error( 71 | 'mongoose.Query.prototype.or does not support ne'); 72 | } 73 | }; 74 | }; 75 | 76 | mongoose.Query.prototype.where = (key, val) => { 77 | if (typeof val === 'undefined') { 78 | val = { expr : '', val : null }; 79 | } 80 | 81 | if (whereClause[key]) { 82 | let newVal = [whereClause[key], val]; 83 | whereClause[key] = newVal; 84 | } else { 85 | whereClause[key] = val; 86 | } 87 | 88 | return { 89 | exists : (v) => { 90 | whereClause[key].expr = 'exists'; 91 | whereClause[key].val = v; 92 | }, 93 | gt : (v) => { 94 | whereClause[key].expr = 'gt'; 95 | whereClause[key].val = v; 96 | }, 97 | gte : (v) => { 98 | whereClause[key].expr = 'gte'; 99 | whereClause[key].val = v; 100 | }, 101 | lt : (v) => { 102 | whereClause[key].expr = 'lt'; 103 | whereClause[key].val = v; 104 | }, 105 | lte : (v) => { 106 | whereClause[key].expr = 'lte'; 107 | whereClause[key].val = v; 108 | }, 109 | ne : (v) => { 110 | whereClause[key].expr = 'ne'; 111 | whereClause[key].val = v; 112 | } 113 | }; 114 | }; 115 | }); 116 | 117 | beforeEach(() => { 118 | orClause = {}; 119 | whereClause = {}; 120 | }); 121 | 122 | it ('should return a query when created', () => { 123 | let query = Kitteh 124 | .find() 125 | .filter(null); 126 | 127 | (query instanceof mongoose.Query).should.equals(true); 128 | }); 129 | 130 | it ('should apply both mandatory and optional filters when both are supplied', () => { 131 | let options = { 132 | filters : { 133 | mandatory : { 134 | contains : { 135 | name : 'cat' 136 | } 137 | }, 138 | optional : { 139 | exact : { 140 | 'features.color' : 'brindle' 141 | } 142 | } 143 | } 144 | }; 145 | 146 | let query = Kitteh 147 | .find() 148 | .filter(options); 149 | 150 | should.exist(query); 151 | should.exist(whereClause.name); 152 | should.exist(orClause['features.color']); 153 | }); 154 | 155 | describe('mandatory filters', () => { 156 | it ('should look for occurrences of a term within a string using contains', () => { 157 | let options = { 158 | filters : { 159 | mandatory : { 160 | contains : { 161 | name : 'cat' 162 | } 163 | } 164 | } 165 | }; 166 | 167 | let query = Kitteh 168 | .find() 169 | .filter(options); 170 | 171 | should.exist(query); 172 | should.exist(whereClause.name); 173 | whereClause.name.test('cat').should.equals(true); 174 | whereClause.name.test('a cat exists').should.equals(true); 175 | whereClause.name.test('dog').should.equals(false); 176 | }); 177 | 178 | it ('should look for occurrences of a term at the start of a string using endsWith', () => { 179 | let options = { 180 | filters : { 181 | mandatory : { 182 | endsWith : { 183 | name : 'cat' 184 | } 185 | } 186 | } 187 | }; 188 | 189 | let query = Kitteh 190 | .find() 191 | .filter(options); 192 | 193 | should.exist(query); 194 | should.exist(whereClause.name); 195 | whereClause.name.test('cat').should.equals(true); 196 | whereClause.name.test('cool cat').should.equals(true); 197 | whereClause.name.test('this cat is sick').should.equals(false); 198 | }); 199 | 200 | it ('should look for occurrences of a term at the start of a string using startsWith', () => { 201 | let options = { 202 | filters : { 203 | mandatory : { 204 | startsWith : { 205 | name : 'cat' 206 | } 207 | } 208 | } 209 | }; 210 | 211 | let query = Kitteh 212 | .find() 213 | .filter(options); 214 | 215 | should.exist(query); 216 | should.exist(whereClause.name); 217 | whereClause.name.test('cat').should.equals(true); 218 | whereClause.name.test('cat exists').should.equals(true); 219 | whereClause.name.test('this cat is sick').should.equals(false); 220 | }); 221 | 222 | it ('should look for occurrences of an exact match of the term when using exact', () => { 223 | let options = { 224 | filters : { 225 | mandatory : { 226 | exact : { 227 | name : 'cat' 228 | } 229 | } 230 | } 231 | }; 232 | 233 | let query = Kitteh 234 | .find() 235 | .filter(options); 236 | 237 | should.exist(query); 238 | should.exist(whereClause.name); 239 | whereClause.name.test('cat').should.equals(true); 240 | whereClause.name.test('cat litter').should.equals(false); 241 | whereClause.name.test('the cat').should.equals(false); 242 | }); 243 | 244 | it ('should look for occurrences of an equals match of the term when using equals', () => { 245 | let options = { 246 | filters : { 247 | mandatory : { 248 | equals : { 249 | name : 'cat' 250 | } 251 | } 252 | } 253 | }; 254 | 255 | let query = Kitteh 256 | .find() 257 | .filter(options); 258 | 259 | should.exist(query); 260 | should.exist(whereClause.name); 261 | whereClause.name.should.equals('cat'); 262 | }); 263 | 264 | it ('should look for occurrences of an exact match of the object when using exact', () => { 265 | let options = { 266 | filters : { 267 | mandatory : { 268 | exact : { 269 | isDead : false 270 | } 271 | } 272 | } 273 | }; 274 | 275 | let query = Kitteh 276 | .find() 277 | .filter(options); 278 | 279 | should.exist(query); 280 | should.exist(whereClause.isDead); 281 | whereClause.isDead.should.equals(false); 282 | }); 283 | 284 | it ('should look for occurrences of an exact match of a number when using exact', () => { 285 | let options = { 286 | filters : { 287 | mandatory : { 288 | exact : { 289 | id : 12345 290 | } 291 | } 292 | } 293 | }; 294 | 295 | let query = Kitteh 296 | .find() 297 | .filter(options); 298 | 299 | should.exist(query); 300 | should.exist(whereClause.id); 301 | whereClause.id.should.equals(12345); 302 | }); 303 | 304 | it('should look for occurrences where given field exists when using exists', () => { 305 | let options = { 306 | filters : { 307 | mandatory : { 308 | exists : { 309 | name : true 310 | } 311 | } 312 | } 313 | }; 314 | 315 | let query = Kitteh 316 | .find() 317 | .filter(options); 318 | 319 | should.exist(query); 320 | should.exist(whereClause.name); 321 | whereClause.name.val.should.equals(true); 322 | }); 323 | 324 | 325 | it ('should properly apply where clause when using greaterThan filter', () => { 326 | let options = { 327 | filters : { 328 | mandatory : { 329 | greaterThan : { 330 | birthday : new Date(2014, 12, 1) 331 | } 332 | } 333 | } 334 | }; 335 | 336 | let query = Kitteh 337 | .find() 338 | .filter(options); 339 | 340 | should.exist(query); 341 | should.exist(whereClause.birthday); 342 | whereClause.birthday.expr.should.equal('gt'); 343 | }); 344 | 345 | it ('should properly apply where clause when using gt filter', () => { 346 | let options = { 347 | filters : { 348 | mandatory : { 349 | gt : { 350 | birthday : new Date(2014, 12, 1) 351 | } 352 | } 353 | } 354 | }; 355 | 356 | let query = Kitteh 357 | .find() 358 | .filter(options); 359 | 360 | should.exist(query); 361 | should.exist(whereClause.birthday); 362 | whereClause.birthday.expr.should.equal('gt'); 363 | }); 364 | 365 | it ('should properly apply where clause when using greaterThanEqual filter', () => { 366 | let options = { 367 | filters : { 368 | mandatory : { 369 | greaterThanEqual : { 370 | birthday : new Date(2014, 12, 1) 371 | } 372 | } 373 | } 374 | }; 375 | 376 | let query = Kitteh 377 | .find() 378 | .filter(options); 379 | 380 | should.exist(query); 381 | should.exist(whereClause.birthday); 382 | whereClause.birthday.expr.should.equal('gte'); 383 | }); 384 | 385 | it ('should properly apply where clause when using gte filter', () => { 386 | let options = { 387 | filters : { 388 | mandatory : { 389 | gte : { 390 | birthday : new Date(2014, 12, 1) 391 | } 392 | } 393 | } 394 | }; 395 | 396 | let query = Kitteh 397 | .find() 398 | .filter(options); 399 | 400 | should.exist(query); 401 | should.exist(whereClause.birthday); 402 | whereClause.birthday.expr.should.equal('gte'); 403 | }); 404 | 405 | it ('should properly apply where clause when using lessThan filter', () => { 406 | let options = { 407 | filters : { 408 | mandatory : { 409 | lessThan : { 410 | birthday : new Date(2014, 12, 1) 411 | } 412 | } 413 | } 414 | }; 415 | 416 | let query = Kitteh 417 | .find() 418 | .filter(options); 419 | 420 | should.exist(query); 421 | should.exist(whereClause.birthday); 422 | whereClause.birthday.expr.should.equal('lt'); 423 | }); 424 | 425 | it ('should properly apply where clause when using lt filter', () => { 426 | let options = { 427 | filters : { 428 | mandatory : { 429 | lt : { 430 | birthday : new Date(2014, 12, 1) 431 | } 432 | } 433 | } 434 | }; 435 | 436 | let query = Kitteh 437 | .find() 438 | .filter(options); 439 | 440 | should.exist(query); 441 | should.exist(whereClause.birthday); 442 | whereClause.birthday.expr.should.equal('lt'); 443 | }); 444 | 445 | it ('should properly apply where clause when using lessThanEqual filter', () => { 446 | let options = { 447 | filters : { 448 | mandatory : { 449 | lessThanEqual : { 450 | birthday : new Date(2014, 12, 1) 451 | } 452 | } 453 | } 454 | }; 455 | 456 | let query = Kitteh 457 | .find() 458 | .filter(options); 459 | 460 | should.exist(query); 461 | should.exist(whereClause.birthday); 462 | whereClause.birthday.expr.should.equal('lte'); 463 | }); 464 | 465 | it ('should properly apply where clause when using lte filter', () => { 466 | let options = { 467 | filters : { 468 | mandatory : { 469 | lte : { 470 | birthday : new Date(2014, 12, 1) 471 | } 472 | } 473 | } 474 | }; 475 | 476 | let query = Kitteh 477 | .find() 478 | .filter(options); 479 | 480 | should.exist(query); 481 | should.exist(whereClause.birthday); 482 | whereClause.birthday.expr.should.equal('lte'); 483 | }); 484 | 485 | it ('should properly apply where clause when using notEqual filter', () => { 486 | let options = { 487 | filters : { 488 | mandatory : { 489 | notEqual : { 490 | name : 'cat' 491 | } 492 | } 493 | } 494 | }; 495 | 496 | let query = Kitteh 497 | .find() 498 | .filter(options); 499 | 500 | should.exist(query); 501 | should.exist(whereClause.name); 502 | whereClause.name.expr.should.equal('ne'); 503 | }); 504 | 505 | it ('should properly apply where clause when using ne filter', () => { 506 | let options = { 507 | filters : { 508 | mandatory : { 509 | notEqual : { 510 | name : 'cat' 511 | } 512 | } 513 | } 514 | }; 515 | 516 | let query = Kitteh 517 | .find() 518 | .filter(options); 519 | 520 | should.exist(query); 521 | should.exist(whereClause.name); 522 | whereClause.name.expr.should.equal('ne'); 523 | }); 524 | 525 | it ('should look for multiple occurrences of a match when supplying an array', () => { 526 | let options = { 527 | filters : { 528 | mandatory : { 529 | endsWith : { 530 | name : ['dog', 'brown'] 531 | }, 532 | startsWith : { 533 | breed : ['short', 'manx'] 534 | } 535 | } 536 | } 537 | }; 538 | 539 | let query = Kitteh 540 | .find() 541 | .filter(options); 542 | 543 | should.exist(query); 544 | should.exist(whereClause.breed); 545 | should.exist(whereClause.name); 546 | 547 | whereClause.name[0].test('the dog').should.equals(true); 548 | whereClause.name[1].test('is brown').should.equals(true); 549 | whereClause.breed[0].test('shorthair').should.equals(true); 550 | whereClause.breed[1].test('manx').should.equals(true); 551 | }); 552 | }); 553 | 554 | describe('optional filters', () => { 555 | it ('should look for occurrences of a term within a string using contains', () => { 556 | let options = { 557 | filters : { 558 | optional : { 559 | contains : { 560 | name : 'cat' 561 | } 562 | } 563 | } 564 | }; 565 | 566 | let query = Kitteh 567 | .find() 568 | .filter(options); 569 | 570 | should.exist(query); 571 | should.exist(orClause.name); 572 | orClause.name.test('cat').should.equals(true); 573 | orClause.name.test('a cat exists').should.equals(true); 574 | orClause.name.test('dog').should.equals(false); 575 | }); 576 | 577 | it ('should look for occurrences of a term at the start of a string using endsWith', () => { 578 | let options = { 579 | filters : { 580 | optional : { 581 | endsWith : { 582 | name : 'cat' 583 | } 584 | } 585 | } 586 | }; 587 | 588 | let query = Kitteh 589 | .find() 590 | .filter(options); 591 | 592 | should.exist(query); 593 | should.exist(orClause.name); 594 | orClause.name.test('cat').should.equals(true); 595 | orClause.name.test('cool cat').should.equals(true); 596 | orClause.name.test('this cat is sick').should.equals(false); 597 | }); 598 | 599 | it ('should look for occurrences of a term at the start of a string using startsWith', () => { 600 | let options = { 601 | filters : { 602 | optional : { 603 | startsWith : { 604 | name : 'cat' 605 | } 606 | } 607 | } 608 | }; 609 | 610 | let query = Kitteh 611 | .find() 612 | .filter(options); 613 | 614 | should.exist(query); 615 | should.exist(orClause.name); 616 | orClause.name.test('cat').should.equals(true); 617 | orClause.name.test('cat exists').should.equals(true); 618 | orClause.name.test('this cat is sick').should.equals(false); 619 | }); 620 | 621 | it ('should look for occurrences of an exact match of the term when using exact', () => { 622 | let options = { 623 | filters : { 624 | optional : { 625 | exact : { 626 | name : 'cat' 627 | } 628 | } 629 | } 630 | }; 631 | 632 | let query = Kitteh 633 | .find() 634 | .filter(options); 635 | 636 | should.exist(query); 637 | should.exist(orClause.name); 638 | orClause.name.test('cat').should.equals(true); 639 | orClause.name.test('cat litter').should.equals(false); 640 | orClause.name.test('the cat').should.equals(false); 641 | }); 642 | 643 | it ('should look for occurrences of an exact match of the object when using exact', () => { 644 | let options = { 645 | filters : { 646 | optional : { 647 | exact : { 648 | isDead : true 649 | } 650 | } 651 | } 652 | }; 653 | 654 | let query = Kitteh 655 | .find() 656 | .filter(options); 657 | 658 | should.exist(query); 659 | should.exist(orClause.isDead); 660 | orClause.isDead.should.equals(true); 661 | }); 662 | 663 | it ('should look for occurrences of an exact match of the object when using exact', () => { 664 | let options = { 665 | filters : { 666 | optional : { 667 | exact : { 668 | doubleField : '99.99', 669 | intField : '0100', 670 | isAlive : 'true', 671 | isDead : 'false', 672 | randomField : 'null' 673 | } 674 | } 675 | } 676 | }; 677 | 678 | let query = Kitteh 679 | .find() 680 | .filter(options); 681 | 682 | should.exist(query); 683 | should.exist(orClause.isDead); 684 | orClause.isAlive.should.equals(true); 685 | orClause.isDead.should.equals(false); 686 | should.not.exist(orClause.randomField); 687 | orClause.intField.should.equals(100); 688 | orClause.doubleField.should.equals(99.99); 689 | }); 690 | 691 | it ('should look for occurrences of an equals match of the object when using equals', () => { 692 | let options = { 693 | filters : { 694 | optional : { 695 | equals : { 696 | doubleField : '99.99', 697 | intField : '0100', 698 | isAlive : 'true', 699 | isDead : 'false', 700 | randomField : 'null' 701 | } 702 | } 703 | } 704 | }; 705 | 706 | let query = Kitteh 707 | .find() 708 | .filter(options); 709 | 710 | should.exist(query); 711 | should.exist(orClause.isDead); 712 | orClause.isAlive.should.equals(true); 713 | orClause.isDead.should.equals(false); 714 | should.not.exist(orClause.randomField); 715 | orClause.intField.should.equals(100); 716 | orClause.doubleField.should.equals(99.99); 717 | }); 718 | 719 | it ('should look for multiple occurrences of a match when supplying an array', () => { 720 | let options = { 721 | filters : { 722 | optional : { 723 | exact : { 724 | name : ['cat', 'Kitteh'] 725 | } 726 | } 727 | } 728 | }; 729 | 730 | let query = Kitteh 731 | .find() 732 | .filter(options); 733 | 734 | should.exist(query); 735 | should.exist(orClause.name); 736 | orClause.name[0].test('cat').should.equals(true); 737 | orClause.name[1].test('Kitteh').should.equals(true); 738 | }); 739 | 740 | it ('should look for multiple occurrences of a equals match when supplying an array', () => { 741 | let options = { 742 | filters : { 743 | optional : { 744 | equals : { 745 | name : ['cat', 'Kitteh'] 746 | } 747 | } 748 | } 749 | }; 750 | 751 | let query = Kitteh 752 | .find() 753 | .filter(options); 754 | 755 | should.exist(query); 756 | should.exist(orClause.name); 757 | orClause.name[0].should.equals('cat'); 758 | orClause.name[1].should.equals('Kitteh'); 759 | }); 760 | }); 761 | }); 762 | -------------------------------------------------------------------------------- /test/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-invalid-this : 0 */ 2 | /* eslint no-magic-numbers : 0 */ 3 | import chai from 'chai'; 4 | import indexLib from '../../src/index'; 5 | import mongoose from 'mongoose'; 6 | 7 | const should = chai.should(); 8 | 9 | describe('index', () => { 10 | let 11 | fieldsSelected = [], 12 | Kitteh = mongoose.model('kittehs-index', new mongoose.Schema({ 13 | birthday : { 14 | default : Date.now, 15 | type : Date 16 | }, 17 | features : { 18 | color : String, 19 | isFurreh : Boolean 20 | }, 21 | home : String, 22 | isDead: Boolean, 23 | name : String, 24 | peePatches : [String] 25 | })), 26 | orClauseItems = [], 27 | sortClauseItems = [], 28 | whereClause = {}; 29 | 30 | before(() => { 31 | indexLib.initialize(mongoose); 32 | 33 | Kitteh.countDocuments = (search, countCallback) => { 34 | countCallback(null, 0); 35 | }; 36 | 37 | mongoose.Query.prototype.exec = (findCallback) => { 38 | findCallback(null, []); 39 | }; 40 | 41 | mongoose.Query.prototype.limit = () => { 42 | return this; 43 | }; 44 | 45 | mongoose.Query.prototype.select = (field) => { 46 | if (field) { 47 | fieldsSelected.push(field); 48 | } 49 | }; 50 | 51 | mongoose.Query.prototype.skip = () => { 52 | return this; 53 | }; 54 | 55 | mongoose.Query.prototype.or = (clause) => { 56 | if (clause) { 57 | orClauseItems.push(clause); 58 | } 59 | }; 60 | 61 | mongoose.Query.prototype.sort = (clause) => { 62 | if (clause) { 63 | sortClauseItems.push(clause); 64 | } 65 | }; 66 | 67 | mongoose.Query.prototype.where = (key) => { 68 | return { 69 | equals : (value) => { 70 | whereClause[key] = value; 71 | }, 72 | regex : (value) => { 73 | whereClause[key] = value; 74 | } 75 | }; 76 | }; 77 | }); 78 | 79 | beforeEach(() => { 80 | fieldsSelected = []; 81 | orClauseItems = []; 82 | sortClauseItems = []; 83 | whereClause = {}; 84 | }); 85 | 86 | it('should properly initialize options', (done) => { 87 | let options = { 88 | maxDocs : 1000 89 | }; 90 | 91 | indexLib.initialize(options, mongoose); 92 | 93 | Kitteh 94 | .find() 95 | .page(null, (err, data) => { 96 | should.not.exist(err); 97 | should.exist(data); 98 | data.options.count.should.equals(1000); 99 | 100 | return done(); 101 | }); 102 | }); 103 | 104 | it('should properly require all middleware components', (done) => { 105 | let options = { 106 | count : 500, 107 | filters : { 108 | field : ['name', 'home', 'features.color'], 109 | mandatory : { 110 | contains : { 111 | 'features.color' : ['brindle', 'black', 'white'] 112 | }, 113 | exact : { 114 | name : 'Hamish' 115 | } 116 | }, 117 | optional : { 118 | contains : { 119 | home : 'seattle' 120 | } 121 | } 122 | }, 123 | sort: ['-birthday', 'name'], 124 | start : 0 125 | }; 126 | 127 | Kitteh 128 | .find() 129 | .field(options) 130 | .filter(options) 131 | .keyword(options) 132 | .order(options) 133 | .page(options, (err, data) => { 134 | should.not.exist(err); 135 | should.exist(data); 136 | 137 | return done(); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /test/src/keyword.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers : 0 */ 2 | import chai from 'chai'; 3 | import keywordLib from '../../src/keyword'; 4 | import mongoose from 'mongoose'; 5 | 6 | const should = chai.should(); 7 | 8 | describe('keyword', () => { 9 | let 10 | Kitteh = mongoose.model('kittehs-keyword', new mongoose.Schema({ 11 | birthday : { 12 | default : Date.now, 13 | type : Date 14 | }, 15 | features : { 16 | color : String, 17 | isFurreh : Boolean 18 | }, 19 | home : String, 20 | isDead: Boolean, 21 | name : String, 22 | peePatches : [String] 23 | })), 24 | orClauseItems = []; 25 | 26 | before(() => { 27 | keywordLib(mongoose); 28 | 29 | mongoose.Query.prototype.or = (clause) => { 30 | if (clause) { 31 | orClauseItems.push(clause); 32 | } 33 | }; 34 | }); 35 | 36 | beforeEach(() => { 37 | orClauseItems = []; 38 | }); 39 | 40 | it ('should return a query when created', () => { 41 | let query = Kitteh 42 | .find() 43 | .keyword(null); 44 | 45 | (query instanceof mongoose.Query).should.equals(true); 46 | }); 47 | 48 | it ('should not filter if there are no fields specified', () => { 49 | let options = { 50 | filters : { 51 | keyword : { 52 | fields : null, 53 | term : 'cat' 54 | } 55 | } 56 | }; 57 | 58 | let query = Kitteh 59 | .find() 60 | .keyword(options); 61 | 62 | should.exist(query); 63 | orClauseItems.should.have.length(0); 64 | }); 65 | 66 | it ('should not filter if there is no term specified', () => { 67 | let options = { 68 | filters : { 69 | keyword : { 70 | fields : null, 71 | term : '' 72 | } 73 | } 74 | }; 75 | 76 | let query = Kitteh 77 | .find() 78 | .keyword(options); 79 | 80 | should.exist(query); 81 | orClauseItems.should.have.length(0); 82 | }); 83 | 84 | it ('should apply search of keyword to specified fields', () => { 85 | let options = { 86 | filters : { 87 | keyword : { 88 | fields : ['name', 'features.color', 'home'], 89 | term : 'cat' 90 | } 91 | } 92 | }; 93 | 94 | let query = Kitteh 95 | .find() 96 | .keyword(options); 97 | 98 | should.exist(query); 99 | orClauseItems[0].should.have.length(3); 100 | orClauseItems[0][0].name.test('cat').should.equals(true); 101 | orClauseItems[0][0].name.test('spec-cat-acular').should.equals(true); 102 | }); 103 | 104 | it ('should search matches in arrays when a property within a schema is an array', () => { 105 | let options = { 106 | filters : { 107 | keyword : { 108 | fields : ['name', 'peePatches'], 109 | term : 'lawn' 110 | } 111 | } 112 | }; 113 | 114 | let query = Kitteh 115 | .find() 116 | .keyword(options); 117 | 118 | should.exist(query); 119 | orClauseItems[0].should.have.length(2); 120 | should.exist(orClauseItems[0][1].peePatches.$in); 121 | }); 122 | 123 | it ('should search for keyword occurrences with multiple words', () => { 124 | let options = { 125 | filters : { 126 | keyword : { 127 | fields : ['name', 'features.color', 'home'], 128 | term : 'ceiling cat' 129 | } 130 | } 131 | }; 132 | 133 | let query = Kitteh 134 | .find() 135 | .keyword(options); 136 | 137 | should.exist(query); 138 | orClauseItems[0].should.have.length(3); 139 | orClauseItems[0][0].name.test('floor cat').should.equals(false); 140 | orClauseItems[0][0].name.test('cat ceiling').should.equals(true); 141 | orClauseItems[0][0].name.test('ceilings are not for cats').should.equals(true); 142 | }); 143 | 144 | it ('should search for exact match of multiple word keywords enclosed in quotes', () => { 145 | let options = { 146 | filters : { 147 | keyword : { 148 | fields : ['name', 'features.color', 'home'], 149 | term : '"ceiling cat"' 150 | } 151 | } 152 | }; 153 | 154 | let query = Kitteh 155 | .find() 156 | .keyword(options); 157 | 158 | should.exist(query); 159 | orClauseItems[0].should.have.length(3); 160 | orClauseItems[0][0].name.test('floor cat').should.equals(false); 161 | orClauseItems[0][0].name.test('cat ceiling').should.equals(false); 162 | orClauseItems[0][0].name.test('ceilings are not for cats').should.equals(false); 163 | orClauseItems[0][0].name.test('does ceiling cat exist?').should.equals(true); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/src/order.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import mongoose from 'mongoose'; 3 | import orderLib from '../../src/order'; 4 | 5 | const should = chai.should(); 6 | 7 | describe('order', () => { 8 | let 9 | Kitteh = mongoose.model('kittehs-order', new mongoose.Schema({ 10 | birthday : { 11 | default : Date.now, 12 | type : Date 13 | }, 14 | features : { 15 | color : String, 16 | isFurreh : Boolean 17 | }, 18 | home : String, 19 | isDead: Boolean, 20 | name : String, 21 | peePatches : [String] 22 | })), 23 | sortClauseItems = []; 24 | 25 | before(() => { 26 | orderLib(mongoose); 27 | 28 | mongoose.Query.prototype.sort = (clause) => { 29 | if (clause) { 30 | sortClauseItems.push(clause); 31 | } 32 | }; 33 | }); 34 | 35 | beforeEach(() => { 36 | sortClauseItems = []; 37 | }); 38 | 39 | it ('should return a query when created', () => { 40 | let query = Kitteh 41 | .find() 42 | .order(null); 43 | 44 | should.exist(query); 45 | (query instanceof mongoose.Query).should.equals(true); 46 | }); 47 | 48 | it ('should sort fields in descending order when supplied', () => { 49 | let options = { 50 | sort : '-name' 51 | }; 52 | 53 | Kitteh 54 | .find() 55 | .order(options); 56 | 57 | sortClauseItems.should.have.length(1); 58 | sortClauseItems[0].name.should.equals(-1); 59 | }); 60 | 61 | it ('should sort fields in descending order when an array is supplied', () => { 62 | let options = { 63 | sort : ['-name', '-birthday'] 64 | }; 65 | 66 | Kitteh 67 | .find() 68 | .order(options); 69 | 70 | sortClauseItems.should.have.length(2); 71 | sortClauseItems[0].name.should.equals(-1); 72 | sortClauseItems[1].birthday.should.equals(-1); 73 | }); 74 | 75 | it ('should sort fields in descending order when an object is supplied', () => { 76 | let options = { 77 | sort : { 78 | birthday : -1, 79 | name : -1 80 | } 81 | }; 82 | 83 | Kitteh 84 | .find() 85 | .order(options); 86 | 87 | sortClauseItems.should.have.length(2); 88 | sortClauseItems[0].birthday.should.equals(-1); 89 | sortClauseItems[1].name.should.equals(-1); 90 | }); 91 | 92 | it ('should sort fields in ascending order when supplied', () => { 93 | let options = { 94 | sort : ['name'] 95 | }; 96 | 97 | Kitteh 98 | .find() 99 | .order(options); 100 | 101 | sortClauseItems.should.have.length(1); 102 | sortClauseItems[0].name.should.equals(1); 103 | }); 104 | 105 | it ('should sort fields in ascending order when an array is supplied', () => { 106 | let options = { 107 | sort : ['birthday', 'name'] 108 | }; 109 | 110 | Kitteh 111 | .find() 112 | .order(options); 113 | 114 | sortClauseItems.should.have.length(2); 115 | sortClauseItems[0].birthday.should.equals(1); 116 | sortClauseItems[1].name.should.equals(1); 117 | }); 118 | 119 | it ('should sort fields in ascending order when an object is supplied', () => { 120 | let options = { 121 | sort : { 122 | birthday: 1, 123 | name: 1 124 | } 125 | }; 126 | 127 | Kitteh 128 | .find() 129 | .order(options); 130 | 131 | sortClauseItems.should.have.length(2); 132 | sortClauseItems[0].birthday.should.equals(1); 133 | sortClauseItems[1].name.should.equals(1); 134 | }); 135 | 136 | it ('should sort fields in both ascending and descending order when supplied as a string', () => { 137 | let options = { 138 | sort : 'home, -name' 139 | }; 140 | 141 | Kitteh 142 | .find() 143 | .order(options); 144 | 145 | sortClauseItems.should.have.length(2); 146 | sortClauseItems[0].home.should.equals(1); 147 | sortClauseItems[1].name.should.equals(-1); 148 | }); 149 | 150 | it ('should sort fields in both ascending and descending order when supplied as an array', () => { 151 | let options = { 152 | sort : ['home', '-name'] 153 | }; 154 | 155 | Kitteh 156 | .find() 157 | .order(options); 158 | 159 | sortClauseItems.should.have.length(2); 160 | sortClauseItems[0].home.should.equals(1); 161 | sortClauseItems[1].name.should.equals(-1); 162 | }); 163 | 164 | it ('should sort fields in both ascending and descending order when supplied as an object', () => { 165 | let options = { 166 | sort : { 167 | 'home': 1, 168 | 'name': -1 169 | } 170 | }; 171 | 172 | Kitteh 173 | .find() 174 | .order(options); 175 | 176 | sortClauseItems.should.have.length(2); 177 | sortClauseItems[0].home.should.equals(1); 178 | sortClauseItems[1].name.should.equals(-1); 179 | }); 180 | 181 | it ('should sort fields in default ascending order when values in supplied object are not valid numbers', () => { 182 | let options = { 183 | sort : { 184 | 'home': 1, 185 | 'name': 'invalid number' 186 | } 187 | }; 188 | 189 | Kitteh 190 | .find() 191 | .order(options); 192 | 193 | sortClauseItems.should.have.length(2); 194 | sortClauseItems[0].home.should.equals(1); 195 | sortClauseItems[1].name.should.equals(1); 196 | }); 197 | 198 | }); 199 | -------------------------------------------------------------------------------- /test/src/page.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers : 0 */ 2 | /* eslint no-unused-expressions : 0 */ 3 | import chai from 'chai'; 4 | import mongoose from 'mongoose'; 5 | import pageLib from '../../src/page'; 6 | 7 | const should = chai.should(); 8 | 9 | describe('page', () => { 10 | let 11 | countError = null, 12 | execError = null, 13 | Kitteh = mongoose.model('kittehs-page', new mongoose.Schema({ 14 | birthday : { 15 | default : Date.now, 16 | type : Date 17 | }, 18 | features : { 19 | color : String, 20 | isFurreh : Boolean 21 | }, 22 | home : String, 23 | isDead: Boolean, 24 | name : String, 25 | peePatches : [String] 26 | })), 27 | limit = 0, 28 | skip = 0, 29 | total = 1000; 30 | 31 | before(() => { 32 | pageLib(mongoose); 33 | 34 | Kitteh.countDocuments = (search, countCallback) => { 35 | countCallback(countError, total); 36 | }; 37 | 38 | mongoose.Query.prototype.exec = function (findCallback) { 39 | findCallback(execError, []); 40 | }; 41 | 42 | mongoose.Query.prototype.limit = function (input) { 43 | limit = input; 44 | return this; 45 | }; 46 | 47 | mongoose.Query.prototype.skip = function (input) { 48 | skip = input; 49 | return this; 50 | }; 51 | }); 52 | 53 | beforeEach(() => { 54 | countError = null; 55 | execError = null; 56 | limit = 0; 57 | skip = 0; 58 | }); 59 | 60 | it('should pass search information to page', (done) => { 61 | Kitteh 62 | .find() 63 | .page(null, (err, data) => { 64 | should.not.exist(err); 65 | data.should.not.be.empty; 66 | skip.should.equals(0); 67 | 68 | return done(); 69 | }); 70 | }); 71 | 72 | it('should default limit to maxDocs when specified at initialization', (done) => { 73 | pageLib(mongoose).initialize({ maxDocs: 25 }); 74 | 75 | Kitteh 76 | .find() 77 | .page(null, (err, data) => { 78 | should.not.exist(err); 79 | data.should.not.be.empty; 80 | limit.should.equals(25); 81 | skip.should.equals(0); 82 | 83 | return done(); 84 | }); 85 | }); 86 | 87 | it('should default limit to maxDocs when 0 is supplied as count', (done) => { 88 | pageLib(mongoose).initialize({ maxDocs: 25 }); 89 | 90 | let options = { 91 | count : 0, 92 | start : 0 93 | }; 94 | 95 | Kitteh 96 | .find() 97 | .page(options, (err, data) => { 98 | should.not.exist(err); 99 | data.should.not.be.empty; 100 | limit.should.equals(25); 101 | skip.should.equals(0); 102 | 103 | return done(); 104 | }); 105 | }); 106 | 107 | it('should properly return error when one occurs during count', (done) => { 108 | countError = new Error('icanhazacounterr'); 109 | 110 | Kitteh 111 | .find() 112 | .page(null, (err, data) => { 113 | should.exist(err); 114 | should.not.exist(data); 115 | 116 | return done(); 117 | }); 118 | }); 119 | 120 | it('should properly return error when one occurs during exec', (done) => { 121 | execError = new Error('icanhazanexecerr'); 122 | 123 | Kitteh 124 | .find() 125 | .page(null, (err, data) => { 126 | should.exist(err); 127 | should.not.exist(data); 128 | 129 | return done(); 130 | }); 131 | }); 132 | 133 | it('should properly wrap the return data with input options', (done) => { 134 | let options = { 135 | count : 50, 136 | start : 0 137 | }; 138 | 139 | pageLib(mongoose).initialize({ maxDocs : -1 }); 140 | 141 | Kitteh 142 | .find() 143 | .page(options, (err, data) => { 144 | should.not.exist(err); 145 | should.exist(data); 146 | 147 | data.options.start.should.equals(0); 148 | data.options.count.should.equals(50); 149 | data.results.should.be.empty; 150 | data.total.should.equals(total); 151 | 152 | return done(); 153 | }); 154 | }); 155 | 156 | it('should not allow more than the maxDocs to be returned from a page request', (done) => { 157 | let options = { 158 | count : 100, 159 | start : 0 160 | }; 161 | 162 | pageLib(mongoose).initialize({ maxDocs: 50 }); 163 | 164 | Kitteh 165 | .find() 166 | .page(options, (err, data) => { 167 | should.not.exist(err); 168 | should.exist(data); 169 | 170 | data.options.count.should.equals(50); 171 | 172 | return done(); 173 | }); 174 | }); 175 | 176 | it('should return results when start is a string', (done) => { 177 | let options = { 178 | count : 100, 179 | start : '0' 180 | }; 181 | 182 | pageLib(mongoose).initialize({ maxDocs: 50 }); 183 | 184 | Kitteh 185 | .find() 186 | .page(options, (err, data) => { 187 | should.not.exist(err); 188 | should.exist(data); 189 | 190 | return done(); 191 | }); 192 | }); 193 | 194 | it('should return results when start is NaN', (done) => { 195 | let options = { 196 | count : 100, 197 | start : 'start' 198 | }; 199 | 200 | pageLib(mongoose).initialize({ maxDocs: 50 }); 201 | 202 | Kitteh 203 | .find() 204 | .page(options, (err, data) => { 205 | should.not.exist(err); 206 | should.exist(data); 207 | 208 | return done(); 209 | }); 210 | }); 211 | 212 | it('should return results when count is a string', (done) => { 213 | let options = { 214 | count : '100', 215 | start : 0 216 | }; 217 | 218 | pageLib(mongoose).initialize({ maxDocs: 50 }); 219 | 220 | Kitteh 221 | .find() 222 | .page(options, (err, data) => { 223 | should.not.exist(err); 224 | should.exist(data); 225 | 226 | return done(); 227 | }); 228 | }); 229 | 230 | it('should return results when count is NaN', (done) => { 231 | let options = { 232 | count : 'count', 233 | start : 0 234 | }; 235 | 236 | pageLib(mongoose).initialize({ maxDocs: 50 }); 237 | 238 | Kitteh 239 | .find() 240 | .page(options, (err, data) => { 241 | should.not.exist(err); 242 | should.exist(data); 243 | 244 | data.options.count.should.equals(50); 245 | 246 | return done(); 247 | }); 248 | }); 249 | 250 | it('should return a Promise when callback is not specified', (done) => { 251 | let options = { 252 | count : 50, 253 | start : 0 254 | }; 255 | 256 | pageLib(mongoose).initialize({ maxDocs : -1 }); 257 | 258 | Kitteh 259 | .find() 260 | .page(options) 261 | .then((data) => { 262 | should.exist(data); 263 | 264 | data.options.start.should.equals(0); 265 | data.options.count.should.equals(50); 266 | data.results.should.be.empty; 267 | data.total.should.equals(total); 268 | 269 | return done(); 270 | }) 271 | .catch(done); 272 | }); 273 | }); 274 | -------------------------------------------------------------------------------- /test/src/utils.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import utils from '../../src/utils'; 3 | 4 | const should = chai.should(); 5 | 6 | describe('utils', () => { 7 | it('should do things', () => { 8 | let 9 | base = { 10 | filter : { 11 | mandatory : { 12 | contains : { 13 | deviceId : ['100010001002'] 14 | }, 15 | endsWith : { 16 | deviceId : '100010001006' 17 | }, 18 | exact : { 19 | deviceId : '100010001000' 20 | }, 21 | ne : { 22 | deviceId : '100010001007' 23 | }, 24 | startsWith : { 25 | deviceId : ['100010001003'] 26 | } 27 | } 28 | } 29 | }, 30 | merged, 31 | model = { 32 | filter : { 33 | mandatory : { 34 | contains : { 35 | deviceId : ['100010001003'] 36 | }, 37 | endsWith : { 38 | deviceId : ['100010001005'] 39 | }, 40 | exact : { 41 | deviceId : '100010001001' 42 | }, 43 | ne : 'testbrokenstring', 44 | startsWith : { 45 | deviceId : '100010001004' 46 | } 47 | }, 48 | optional : { 49 | exact : { 50 | deviceId : '100010001002' 51 | } 52 | } 53 | } 54 | }; 55 | 56 | merged = utils.mergeFilters(base, model); 57 | 58 | should.exist(merged); 59 | 60 | merged.filter.mandatory.exact.deviceId.should.be.an('Array'); 61 | merged.filter.mandatory.exact.deviceId.length.should.equal(2); 62 | 63 | merged.filter.optional.exact.deviceId.should.equal('100010001002'); 64 | 65 | merged.filter.mandatory.contains.deviceId.should.be.an('Array'); 66 | merged.filter.mandatory.contains.deviceId.length.should.equal(2); 67 | 68 | merged.filter.mandatory.startsWith.deviceId.should.be.an('Array'); 69 | merged.filter.mandatory.startsWith.deviceId.length.should.equal(2); 70 | 71 | merged.filter.mandatory.endsWith.deviceId.length.should.equal(2); 72 | 73 | merged.filter.mandatory.ne.deviceId.should.equal('100010001007'); 74 | 75 | 76 | }); 77 | }); 78 | --------------------------------------------------------------------------------