├── .gitignore ├── .npmignore ├── index.js ├── lib └── mongoose-api-query.js ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | app.js 4 | controller.js 5 | fixtures.js 6 | load_fixtures.js 7 | model.js 8 | routes.js -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/mongoose-api-query'); -------------------------------------------------------------------------------- /lib/mongoose-api-query.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = function apiQueryPlugin (schema) { 2 | 3 | schema.statics.apiQuery = function(rawParams, cb) { 4 | var model = this 5 | , params = model.apiQueryParams(rawParams), 6 | 7 | // Create the Mongoose Query object. 8 | query = model 9 | .find(params.searchParams) 10 | .limit(params.per_page) 11 | .skip((params.page - 1) * params.per_page); 12 | 13 | if (params.sort) query = query.sort(params.sort) 14 | 15 | if (cb) { 16 | query.exec(cb); 17 | } else { 18 | return query; 19 | } 20 | }; 21 | 22 | schema.statics.apiQueryParams = function(rawParams) { 23 | 24 | var model = this; 25 | 26 | var convertToBoolean = function (str) { 27 | if (str.toLowerCase() === "true" || 28 | str.toLowerCase() === "t" || 29 | str.toLowerCase() === "yes" || 30 | str.toLowerCase() === "y" || 31 | str === "1"){ 32 | return true; 33 | } else { 34 | return false; 35 | } 36 | }; 37 | 38 | //changed 39 | var searchParams = {} 40 | , query 41 | , page = 1 42 | , per_page = 100 43 | , sort = false; 44 | 45 | var parseSchemaForKey = function (schema, keyPrefix, lcKey, val, operator) { 46 | 47 | var paramType = false; 48 | 49 | var addSearchParam = function (val) { 50 | var key = keyPrefix + lcKey; 51 | 52 | if (typeof searchParams[key] !== 'undefined') { 53 | for (i in val) { 54 | searchParams[key][i] = val[i]; 55 | } 56 | } else { 57 | searchParams[key] = val; 58 | } 59 | }; 60 | 61 | if (matches = lcKey.match(/(.+)\.(.+)/)) { 62 | // parse subschema 63 | if (schema.paths[matches[1]].constructor.name === "DocumentArray" || 64 | schema.paths[matches[1]].constructor.name === "Mixed") { 65 | parseSchemaForKey(schema.paths[matches[1]].schema, matches[1] + ".", matches[2], val, operator) 66 | } 67 | 68 | } else if (typeof schema === "undefined") { 69 | paramType = "String"; 70 | 71 | } else if (typeof schema.paths[lcKey] === "undefined"){ 72 | // nada, not found 73 | } else if (operator === "near") { 74 | paramType = "Near"; 75 | } else if (schema.paths[lcKey].constructor.name === "SchemaBoolean") { 76 | paramType = "Boolean"; 77 | } else if (schema.paths[lcKey].constructor.name === "SchemaString") { 78 | paramType = "String"; 79 | } else if (schema.paths[lcKey].constructor.name === "SchemaNumber") { 80 | paramType = "Number"; 81 | } else if (schema.paths[lcKey].constructor.name === "ObjectId") { 82 | paramType = "ObjectId"; 83 | }//changed 84 | else if (schema.paths[lcKey].constructor.name === "SchemaArray") { 85 | paramType = "Array"; 86 | } 87 | 88 | if (paramType === "Boolean") { 89 | addSearchParam(convertToBoolean(val)); 90 | } else if (paramType === "Number") { 91 | if (val.match(/([0-9]+,?)/) && val.match(',')) { 92 | if (operator === "all") { 93 | addSearchParam({$all: val.split(',')}); 94 | } else if (operator === "nin") { 95 | addSearchParam({$nin: val.split(',')}); 96 | } else if (operator === "mod") { 97 | addSearchParam({$mod: [val.split(',')[0], val.split(',')[1]]}); 98 | } else { 99 | addSearchParam({$in: val.split(',')}); 100 | } 101 | } else if (val.match(/([0-9]+)/)) { 102 | if (operator === "gt" || 103 | operator === "gte" || 104 | operator === "lt" || 105 | operator === "lte" || 106 | operator === "ne") { 107 | var newParam = {}; 108 | newParam["$" + operator] = val; 109 | addSearchParam(newParam); 110 | } else {//changed 111 | addSearchParam(parseInt(val)); 112 | } 113 | } 114 | } else if (paramType === "String") { 115 | if (val.match(',')) { 116 | var options = val.split(',').map(function(str){ 117 | return new RegExp(str, 'i'); 118 | }); 119 | 120 | if (operator === "all") { 121 | addSearchParam({$all: options}); 122 | } else if (operator === "nin") { 123 | addSearchParam({$nin: options}); 124 | } else { 125 | addSearchParam({$in: options}); 126 | } 127 | } else if (val.match(/^([0-9]+)$/)) { 128 | if (operator === "gt" || 129 | operator === "gte" || 130 | operator === "lt" || 131 | operator === "lte") { 132 | var newParam = {}; 133 | newParam["$" + operator] = val; 134 | addSearchParam(newParam); 135 | } else { 136 | addSearchParam(val); 137 | } 138 | } else if (operator === "ne" || operator === "not") { 139 | var neregex = new RegExp(val,"i"); 140 | addSearchParam({'$not': neregex}); 141 | } else if (operator === "exact") { 142 | addSearchParam(val); 143 | } else { 144 | addSearchParam({$regex: val, $options: "-i"}); 145 | } 146 | } else if (paramType === "Near") { 147 | // divide by 69 to convert miles to degrees 148 | var latlng = val.split(','); 149 | var distObj = {$near: [parseFloat(latlng[0]), parseFloat(latlng[1])]}; 150 | if (typeof latlng[2] !== 'undefined') { 151 | distObj.$maxDistance = parseFloat(latlng[2]) / 69; 152 | } 153 | addSearchParam(distObj); 154 | } else if (paramType === "ObjectId") { 155 | addSearchParam(val); 156 | } else if (paramType === "Array") { 157 | addSearchParam(val); 158 | console.log(lcKey ) 159 | 160 | } 161 | 162 | }; 163 | 164 | var parseParam = function (key, val) { 165 | var lcKey = key 166 | , operator = val.match(/\{(.*)\}/) 167 | , val = val.replace(/\{(.*)\}/, ''); 168 | 169 | if (operator) operator = operator[1]; 170 | 171 | if (val === "") { 172 | return; 173 | } else if (lcKey === "page") { 174 | page = val; 175 | } else if (lcKey === "per_page") { 176 | per_page = parseInt(val); 177 | } else if (lcKey === "sort_by") { 178 | var parts = val.split(','); 179 | sort = {}; 180 | sort[parts[0]] = parts.length > 1 ? parts[1] : 1; 181 | } else { 182 | parseSchemaForKey(model.schema, "", lcKey, val, operator); 183 | } 184 | } 185 | 186 | // Construct searchParams 187 | for (var key in rawParams) { 188 | var separatedParams = rawParams[key].match(/\{\w+\}(.[^\{\}]*)/g); 189 | 190 | if (separatedParams === null) { 191 | parseParam(key, rawParams[key]); 192 | } else { 193 | for (var i = 0, len = separatedParams.length; i < len; ++i) { 194 | parseParam(key, separatedParams[i]); 195 | } 196 | } 197 | } 198 | 199 | return { 200 | searchParams:searchParams, 201 | page:page, 202 | per_page:per_page, 203 | sort:sort 204 | } 205 | 206 | }; 207 | 208 | }; 209 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-string-query", 3 | "version": "0.2.6", 4 | "author": "Adam Hodara ", 5 | "description": "Updated Fork from original, Given a Mongoose model and an array of URI params, construct a query for use in a search API.", 6 | "repository": "surfer77/mongoose-string-query", 7 | "keywords": [ 8 | "mongoose", 9 | "query", 10 | "search", 11 | "parameters" 12 | ], 13 | "scripts": { 14 | "start": "node ./bin/www", 15 | "pretest": "node load_fixtures.js; node app.js", 16 | "test": "mocha" 17 | }, 18 | "devDependencies": { 19 | "expect.js": "*", 20 | "express": "^4.16.4", 21 | "mocha": "*", 22 | "mongoose": "^4.10.4", 23 | "zombie": "*" 24 | }, 25 | "dependencies": { 26 | "body-parser": "^1.18.3", 27 | "debug": "~2.6.3", 28 | "morgan": "^1.9.1", 29 | "serve-favicon": "~2.4.2" 30 | }, 31 | "homepage": "https://github.com/surfer77/mongoose-query" 32 | } 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This is a working and updated fork from the great mongoose-api-query package that is deprecated and hasn't been updated for 3 years. 4 | 5 | If you use Mongoose to help serve results for API calls, you might be used to handling calls like: 6 | 7 | /monsters?color=purple&eats_humans=true 8 | 9 | mongoose-query handles some of that busywork for you. Pass in a vanilla object (e.g. req.query) and query conditions will be cast to their appropriate types according to your Mongoose schema. For example, if you have a boolean defined in your schema, we'll convert the `eats_humans=true` to a boolean for searching. 10 | 11 | It also adds a ton of additional search operators, like `less than`, `greater than`, `not equal`, `near` (for geosearch), `in`, and `all`. You can find a full list below. 12 | 13 | When searching strings, by default it does a partial, case-insensitive match. (Which is not the default in MongoDB.) 14 | 15 | ## Usage 16 | 17 | Apply the plugin to any schema in the usual Mongoose fashion: 18 | 19 | ``` 20 | monsterSchema.plugin(mongooseStringQuery); 21 | ``` 22 | 23 | Then call it like you would using `Model.find`. This returns a Mongoose.Query: 24 | 25 | ``` 26 | Monster.apiQuery(req.query).exec(... 27 | ``` 28 | 29 | Or pass a callback in and it will run `.exec` for you: 30 | 31 | ``` 32 | Monster.apiQuery(req.query, function(err, monsters){... 33 | ``` 34 | 35 | ## Examples 36 | 37 | ``` 38 | const mongooseStringQuery = require('mongoose-string-query') 39 | 40 | ``` 41 | 42 | `t`, `y`, and `1` are all aliases for `true`: 43 | 44 | ``` 45 | /monsters?eats_humans=y&scary=1 46 | ``` 47 | 48 | Match on a nested property: 49 | 50 | ``` 51 | /monsters?foods.name=kale 52 | ``` 53 | 54 | Use exact matching: 55 | 56 | ``` 57 | /monsters?foods.name={exact}KALE 58 | ``` 59 | 60 | Matches either `kale` or `beets`: 61 | 62 | ``` 63 | /monsters?foods.name=kale,beets 64 | ``` 65 | 66 | Matches only where `kale` and `beets` are both present: 67 | 68 | ``` 69 | /monsters?foods.name={all}kale,beets 70 | ``` 71 | 72 | Numeric operators: 73 | 74 | ``` 75 | /monsters?monster_id={gte}30&age={lt}50 76 | ``` 77 | 78 | Combine operators: 79 | 80 | ``` 81 | /monsters?monster_id={gte}30{lt}50 82 | ``` 83 | 84 | geo near, with (optional) radius in miles: 85 | 86 | ``` 87 | /monsters?latlon={near}38.8977,-77.0366 88 | /monsters?latlon={near}38.8977,-77.0366,10 89 | ``` 90 | 91 | ##### Pagination 92 | 93 | ``` 94 | /monsters?page=2 95 | /monsters?page=4&per_page=25 // per_page defaults to 10 96 | ``` 97 | 98 | ##### Sorting results 99 | 100 | ``` 101 | /monsters?sort_by=name 102 | /monsters?sort_by=name,desc 103 | ``` 104 | 105 | ##### Schemaless search 106 | 107 | Do you have a property defined in your schema like `data: {}`, that can have anything inside it? You can search that, too, and it will be treated as a string. 108 | 109 | ## Search Operators 110 | 111 | This is a list of the optional search operators you can use for each SchemaType. 112 | 113 | #### Number 114 | 115 | - `number={all}123,456` - Both 123 and 456 must be present 116 | - `number={nin}123,456` - Neither 123 nor 456 117 | - `number={in}123,456` - Either 123 or 456 118 | - `number={gt}123` - > 123 119 | - `number={gte}123` - >= 123 120 | - `number={lt}123` - < 123 121 | - `number={lte}123` - <=123 122 | - `number={ne}123` - Not 123 123 | - `number={mod}10,2` - Where (number / 10) has remainder 2 124 | 125 | #### String 126 | 127 | - `string={all}match,batch` - Both match *and* batch must be present 128 | - `string={nin}match,batch` - Neither match nor batch 129 | - `string={in}match,batch` - Either match or batch 130 | - `string={not}coffee` - Not coffee 131 | - `string={exact}CoFeEe` - Case-sensitive exact match of "CoFeEe" 132 | 133 | #### Array 134 | 135 | - `array={all}match,batch` - Both match *and* batch must be present 136 | - `array={nin}match,batch` - Neither match nor batch 137 | - `array={in}match,batch` - Either match or batch 138 | - `array={not}coffee` - Not coffee 139 | - `array={exact}CoFeEe` - Case-sensitive exact match of "CoFeEe" 140 | 141 | 142 | #### Latlon 143 | 144 | - `latlon={near}37,-122,5` Near 37,-122, with a 5 mile max radius 145 | - `latlon={near}37,-122` Near 37,-122, no radius limit. Automatically sorts by distance 146 | 147 | 148 | 149 | ## To run tests 150 | 151 | ```shell 152 | node load_fixtures.js 153 | node app.js 154 | mocha 155 | ``` 156 | 157 | ## License 158 | 159 | MIT http://mit-license.org/ 160 | --------------------------------------------------------------------------------