Repository: diegohaz/querymen Branch: master Commit: 436c11d64755 Files: 15 Total size: 59.4 KB Directory structure: gitextract_bm548csx/ ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src/ │ ├── index.js │ ├── querymen-param.js │ └── querymen-schema.js └── test/ ├── index.js ├── querymen-param.js └── querymen-schema.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "es2015" ] } ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .eslintrc ================================================ { "parser": "babel-eslint", "extends": "standard", "env": { "node": true, "es6": true }, "rules": { } } ================================================ FILE: .gitignore ================================================ .DS_Store logs *.log node_modules dist tmp coverage .nyc_output npm-debug.log ================================================ FILE: .travis.yml ================================================ language: node_js services: mongodb node_js: - v6 after_script: - 'npm run coveralls' ================================================ FILE: CHANGELOG.md ================================================ # Changelog - [v1.0.0](#v100) ### v1.0.0 * menquery initial commit. ================================================ FILE: LICENSE ================================================ This software is released under the MIT license: Copyright (c) 2016 Diego Haz hazdiego@gmail.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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.)) ================================================ FILE: README.md ================================================ # Querymen [![JS Standard Style][standard-image]][standard-url] [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Coveralls Status][coveralls-image]][coveralls-url] [![Dependency Status][depstat-image]][depstat-url] [![Downloads][download-badge]][npm-url] > Querystring parser middleware for MongoDB, Express and Nodejs ## Install ```sh npm install --save querymen ``` ## Examples ### Pagination Querymen has a default schema to handle pagination. This is the most simple and common usage. ```js import { middleware as query } from 'querymen'; app.get('/posts', query(), ({ querymen: { query, select, cursor } }, res) => { Post.find(query, select, cursor).then(posts => { // posts are proper paginated here }); }); ``` User requests `/posts?page=2&limit=20&sort=-createdAt` querymen will be: ```js querymen = { query: {}, select: {}, cursor: { limit: 20, skip: 20, sort: { createdAt: -1 } } } ``` User requests `/posts?q=term&fields=title,desc` querymen will be: > When user requests `/posts?q=term`, querymen parses it to `{keywords: /term/i}`. It was designed to work with [mongoose-keywords](https://github.com/diegohaz/mongoose-keywords) plugin, which adds a `keywords` field to schemas (check that out). ```js querymen = { query: { keywords: /term/i }, select: { title: 1, desc: 1 }, cursor: { // defaults limit: 30, skip: 0, sort: { createdAt: -1 } } } ``` User requests `/posts?fields=-title&sort=name,-createdAt` querymen will be: ```js querymen = { query: {}, select: { title: 0 }, cursor: { limit: 30, skip: 0, sort: { name: 1, createdAt: -1 } } } ``` ### Custom schema You can define a custom schema, which will be merged into querymen default schema (explained above). ```js import { middleware as query } from 'querymen'; app.get('/posts', query({ after: { type: Date, paths: ['createdAt'], operator: '$gte' } }), ({ querymen }, res) => { Post.find(querymen.query).then(posts => { // ... }); }); ``` User requests `/posts?after=2016-04-23` querymen will be: ```js querymen = { query: { createdAt: { $gte: 1461369600000 } }, select: {}, cursor: { // defaults limit: 30, skip: 0, sort: { createdAt: -1 } } } ``` ### Reusable schemas You can create reusable schemas as well. Just instantiate a `Schema` object. ```js import { middleware as query, Schema } from 'querymen'; const schema = new Schema({ tags: { type: [String], } }); // user requests /posts?tags=world,travel // querymen.query is { tags: { $in: ['world', 'travel'] }} app.get('/posts', query(schema)); app.get('/articles', query(schema)); ``` ### Advanced schema ```js import { middleware as query, Schema } from 'querymen'; const schema = new Schema({ active: Boolean, // shorthand to { type: Boolean } sort: '-createdAt', // shorthand to { type: String, default: '-createdAt' } term: { type: RegExp, paths: ['title', 'description'], bindTo: 'search' // default was 'query' }, with_picture: { type: Boolean, paths: ['picture'], operator: '$exists' } }, { page: false, // disable default parameter `page` limit: 'max_items' // change name of default parameter `limit` to `max_items` }); app.get('/posts', query(schema), ({ querymen }, res) => { // user requests /posts?term=awesome&with_picture=true&active=true&max_items=100 // querymen.query is { picture: { $exists: true }, active: true } // querymen.cursor is { limit: 100, sort: { createdAt: -1 } } // querymen.search is { $or: [{ title: /awesome/i }, { description: /awesome/i }]} }); ``` ### Dynamic advanced schema ```js import { middleware as query, Schema } from 'querymen'; const schema = new Schema(); schema.formatter('scream', (scream, value, param) => { if (scream) { value = value.toUpperCase() + '!!!!!!!'; } return value; }); schema.param('text', null, { type: String }); // { type: String } schema.param('text').option('scream', true); // { type: String, scream: true } schema.param('text').value('help'); console.log(schema.param('text').value()); // HELP!!!!!!! schema.validator('isPlural', (isPlural, value, param) => { return { valid: !isPlural || value.substr(-1) === 's', message: param.name + ' must be in plural form.' }; }); schema.param('text').option('isPlural', true); // { type: String, scream: true, isPlural: true } console.log(schema.validate()); // false schema.param('text', 'helps'); console.log(schema.validate()); // true console.log(schema.param('text').value()); // HELPS!!!!!!! schema.parser('elemMatch', (elemMatch, value, path, operator) => { if (elemMatch) { value = { [path]: { $elemMatch: {[elemMatch]: {[operator]: value } }}}; } return value; }); schema.param('text', 'ivegotcontrols'); console.log(schema.param('text').parse()); // { text: 'IVEGOTCONTROLS!!!!!!!' } schema.param('text').option('elemMatch', 'prop'); console.log(schema.param('text').parse()); // { text: { $elemMatch: { prop: { $eq: 'IVEGOTCONTROLS!!!!!!!'} }}} ``` ### Geo queries Querymen also support geo queries, but it's disabled by default. To enable geo queries you just need to set `near` option to true in schema options. ```js import { middleware as query } from 'querymen'; app.get('/places', query({}, { near: true }), (req, res) => { }); ``` Its `paths` option is set to `['location']` by default, but you can change this as well: ```js import { middleware as query } from 'querymen'; app.get('/places', query({ near: { paths: ['loc'] } }, { near: true }), (req, res) => { }); ``` User requests `/places?near=-22.332113,-44.312311` (latitude, longitude), req.querymen.query will be: ```js req.querymen.query = { loc: { $near: { $geometry: { type: 'Point', coordinates: [-44.312311, -22.332113] } } } } ``` User requests `/places?near=-22.332113,-44.312311&min_distance=200&max_distance=2000` (min_distance and max_distance in meters), req.querymen.query will be: ```js req.querymen.query = { loc: { $near: { $geometry: { type: 'Point', coordinates: [-44.312311, -22.332113] }, $minDistace: 200, $maxDistance: 2000 } } } ``` You can also use legacy geo queries as well. Just set `geojson` option in param: ```js import { middleware as query } from 'querymen'; app.get('/places', query({ near: { paths: ['loc'], geojson: false } }, { near: true }), (req, res) => { }); ``` User requests `/places?near=-22.332113,-44.312311&min_distance=200&max_distance=2000`, req.querymen.query will be: ```js req.querymen.query = { loc: { $near: [-44.312311, -22.332113], // convert meters to radians automatically $minDistace: 0.000031, $maxDistance: 0.00031 } } ``` ### Error handling ```js // user requests /posts?category=world import { middleware as query, querymen, Schema } from 'querymen'; const schema = new Schema({ category: { type: String, enum: ['culture', 'general', 'travel'] } }); app.get('/posts', query(schema)); // create your own handler app.use((err, req, res, next) => { res.status(400).json(err); }); // or use querymen error handler app.use(querymen.errorHandler()); ``` Response body will look like: ```json { "valid": false, "name": "enum", "enum": ["culture", "general", "travel"], "value": "world", "message": "category must be one of: culture, general, travel" } ``` ## Contributing This package was created with [generator-rise](https://github.com/bucaran/generator-rise). Please refer to there to understand the codestyle and workflow. Issues and PRs are welcome! ## License MIT © [Diego Haz](http://github.com/diegohaz) [standard-url]: http://standardjs.com [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg [npm-url]: https://npmjs.org/package/querymen [npm-image]: https://img.shields.io/npm/v/querymen.svg?style=flat-square [travis-url]: https://travis-ci.org/diegohaz/querymen [travis-image]: https://img.shields.io/travis/diegohaz/querymen.svg?style=flat-square [coveralls-url]: https://coveralls.io/r/diegohaz/querymen [coveralls-image]: https://img.shields.io/coveralls/diegohaz/querymen.svg?style=flat-square [depstat-url]: https://david-dm.org/diegohaz/querymen [depstat-image]: https://david-dm.org/diegohaz/querymen.svg?style=flat-square [download-badge]: http://img.shields.io/npm/dm/querymen.svg?style=flat-square ================================================ FILE: package.json ================================================ { "name": "querymen", "version": "2.1.4", "description": "Querystring parser middleware for MongoDB, Express and Nodejs", "main": "dist/index.js", "files": [ "bin/", "dist/" ], "scripts": { "clean": "rimraf dist", "lint": "eslint src test", "check": "npm run lint -s && dependency-check package.json --entry src", "watch": "watch 'npm run build' src test", "test": "babel-node test/index.js | tspec", "prebuild": "npm run check -s && npm run clean -s", "build": "babel --optional runtime src -d dist", "postbuild": "npm run test -s", "coverage": "nyc --reporter=lcov --reporter=text npm run test", "coveralls": "npm run coverage -s && coveralls < coverage/lcov.info", "postcoveralls": "rimraf ./coverage", "prepublish": "npm run build -s", "deploy": "git pull --rebase origin master && git push origin master", "patch": "npm version patch && npm publish", "minor": "npm version minor && npm publish", "major": "npm version major && npm publish", "postpublish": "git push origin master --follow-tags", "toc": "doctoc --github --title \"# Changelog\" CHANGELOG.md" }, "repository": { "type": "git", "url": "git+https://github.com/diegohaz/querymen.git" }, "keywords": [ "mongo", "express", "node", "query", "querystring", "param", "mongodb", "expressjs", "nodejs" ], "author": "Diego Haz ", "license": "MIT", "bugs": { "url": "https://github.com/diegohaz/querymen/issues" }, "homepage": "https://github.com/diegohaz/querymen#readme", "devDependencies": { "babel-cli": "^6.7.5", "babel-core": "^6.7.6", "babel-eslint": "^7.1.1", "babel-preset-es2015": "^6.6.0", "blue-tape": "^1.0.0", "coveralls": "^2.11.12", "dependency-check": "^2.6.0", "doctoc": "^1.2.0", "eslint": "^3.14.0", "eslint-config-standard": "^6.2.1", "eslint-plugin-promise": "^3.4.0", "eslint-plugin-standard": "^2.0.1", "express": "^4.13.4", "mongoose": "^4.7.8", "nyc": "^11.0.1", "rimraf": "^2.5.4", "supertest": "^3.0.0", "tap-spec": "^4.1.1", "tape": "^4.6.0", "watch": "^1.0.1" }, "dependencies": { "lodash": "^4.11.1", "rich-param": "^1.0.1" } } ================================================ FILE: src/index.js ================================================ /** @module querymen */ import _ from 'lodash' import Param from './querymen-param' import Schema from './querymen-schema' export { Param, Schema } export let handlers = { parsers: {}, formatters: {}, validators: {} } /** * Get or set a handler. * @memberof querymen * @param {string} type - Handler type. * @param {string} name - Handler name. * @param {Function} [fn] - Set the handler method. */ export function handler (type, name, fn) { if ( type === 'constructor' || type === '__proto__' || name === 'constructor' || name === '__proto__' ) { return } if (arguments.length > 2) { handlers[type][name] = fn } return handlers[type][name] } /** * Get or set a parser. * @memberof querymen * @param {string} name - Parser name. * @param {parserFn} [fn] - Set the parser method. * @return {parserFn} The parser method. */ export function parser (name, fn) { return handler('parsers', ...arguments) } /** * Get or set a formatter. * @memberof querymen * @param {string} name - Formatter name. * @param {formatterFn} [fn] - Set the formatter method. * @return {formatterFn} The formatter method. */ export function formatter (name, fn) { return handler('formatters', ...arguments) } /** * Get or set a validator. * @memberof querymen * @param {string} name - Validator name. * @param {validatorFn} [fn] - Set the validator method. * @return {validatorFn} The validator method. */ export function validator (name, fn) { return handler('validators', ...arguments) } /** * Create a middleware. * @memberof querymen * @param {QuerymenSchema|Object} [schema] - Schema object. * @param {Object} [options] - Options to be passed to schema. * @return {Function} The middleware. */ export function middleware (schema, options) { return function (req, res, next) { let _schema // If option near is enable with make a simple clone // In otherwise we make a _.cloneDeep if (schema && schema.options && schema.options.near) { _schema = schema instanceof Schema ? _.clone(schema) : new Schema(schema, options) } else { _schema = schema instanceof Schema ? _.cloneDeep(schema) : new Schema(schema, options) } _schema.validate(req.query, (err) => { if (err) { req.querymen = { error: err } res.status(400) return next(err.message) } req.querymen = _schema.parse() req.querymen.schema = _schema next() }) } } /** * Error handler middleware. * @memberof querymen * @return {Function} The middleware. */ export function errorHandler () { return function (err, req, res, next) { if (req.querymen && req.querymen.error) { res.status(400).json(req.querymen.error) } else { next(err) } } } export default { Schema, Param, handlers, handler, parser, formatter, validator, middleware, errorHandler } ================================================ FILE: src/querymen-param.js ================================================ import _ from 'lodash' import Param from 'rich-param' /** QuerymenParam class. */ export default class QuerymenParam extends Param { /** * Create a param. * @param {string} name - Param name. * @param {*} [value] - The value of the param. * @param {Object} [options] - Options of the param. * @param {Object} [schema] - Schema of the param. */ constructor (name, value, options = {}) { super(name, value, options) this.handlers.parsers = {} this.options = _.assign({ paths: [name], operator: '$eq', parse: (value, path, operator, param) => { if (operator === '$eq') { return {[path]: value} } else if (_.isRegExp(value)) { return operator === '$ne' ? {[path]: {$not: value}} : {[path]: value} } else { return {[path]: {[operator]: value}} } } }, this.options) } /** * Get or set a parser. * @param {string} name - Parser name. * @param {parserFn} [fn] - Set the parser method. * @return {parserFn} The parser method. */ parser (name, fn) { return this.handler('parsers', ...arguments) } /** * Parse the param. * @param {*} [value] - Set the param value. * @param {string|string[]} path - Set the param path/paths. * @return {Object} The parsed value. */ parse (value = this._value, path = this.options.paths) { let operator = this.options.operator let query = {} if (_.isNil(value)) { return query } else if (value !== this._value) { value = this.value(value) } if (_.isArray(path)) { let paths = path if (paths.length > 1) { query.$or = paths.map((path) => this.parse(value, path)) return query } path = paths[0] } if (_.isArray(value)) { operator = operator === '$ne' ? '$nin' : '$in' } _.forIn(this.options, (optionValue, option) => { let parser if (option === 'parse' && _.isFunction(optionValue)) { parser = optionValue } else if (_.isFunction(this.handlers.parsers[option])) { parser = this.handlers.parsers[option].bind(this, optionValue) } else { return } query = parser(value, path, operator, this) }) return query } } ================================================ FILE: src/querymen-schema.js ================================================ import _ from 'lodash' import querymen from './' import QuerymenParam from './querymen-param' /** * QuerymenSchema class. */ export default class QuerymenSchema { /** * Create a schema. * @param {Object} [params] - Params object. * @param {Object} [options] - Options object. */ constructor (params = {}, options = {}) { this.params = {} this.options = _.assign({ near: false }, options) this.handlers = { parsers: {}, formatters: {}, validators: {} } this._params = { q: { type: RegExp, normalize: true, paths: ['keywords'] }, fields: { type: [String], bindTo: 'select', parse: (value) => { let fields = _.isArray(value) ? value : [value] let query = {} fields.forEach((field) => { if (_.isNil(field) || _.isEmpty(field)) return field = field.replace(/^([-+]?)id/, '$1_id') if (field.charAt(0) === '-') { query[field.slice(1)] = 0 } else if (field.charAt(0) === '+') { query[field.slice(1)] = 1 } else { query[field] = 1 } }) return query } }, near: { type: [Number], maxlength: 2, minlength: 2, max: 180, min: -180, paths: ['location'], max_distance: true, min_distance: true, geojson: true, format: (value, param) => { if (param.option('min_distance') && !this.param('min_distance')) { this.param('min_distance', null, {type: Number, min: 0, parse: () => false}) } if (param.option('max_distance') && !this.param('max_distance')) { this.param('max_distance', null, {type: Number, parse: () => false}) } return value }, parse: (value, path, operator, param) => { let query = {[path]: {$near: {}}} let minDistance = this.param('min_distance') let maxDistance = this.param('max_distance') if (param.option('geojson')) { query[path].$near = {$geometry: {type: 'Point', coordinates: [value[1], value[0]]}} if (minDistance && minDistance.value()) { query[path].$near.$minDistance = minDistance.value() } if (maxDistance && maxDistance.value()) { query[path].$near.$maxDistance = maxDistance.value() } } else { query[path].$near = [value[1], value[0]] if (minDistance && minDistance.value()) { query[path].$minDistance = minDistance.value() / 6371000 } if (maxDistance && maxDistance.value()) { query[path].$maxDistance = maxDistance.value() / 6371000 } } this.option('sort', false) return query } }, page: { type: Number, default: 1, max: 30, min: 1, bindTo: 'cursor', parse: (value, path, operator, param) => { return {skip: this.param('limit').value() * (value - 1)} } }, limit: { type: Number, default: 30, max: 100, min: 1, bindTo: 'cursor', parse: (value) => ({limit: value}) }, sort: { type: [String], default: '-createdAt', bindTo: 'cursor', parse: (value) => { let fields = _.isArray(value) ? value : [value] let sort = {} fields.forEach((field) => { if (field.charAt(0) === '-') { sort[field.slice(1)] = -1 } else if (field.charAt(0) === '+') { sort[field.slice(1)] = 1 } else { sort[field] = 1 } }) return {sort: sort} } } } let keys = _.union(_.keys(this._params), _.keys(params)) keys.forEach((key) => this.add(key, undefined, params[key])) _.forIn(querymen.handlers, (typedHandler, type) => { _.forIn(typedHandler, (handler, name) => { this.handler(type, name, handler) }) }) } /** * Get or set an option. * @param {string} name - Option name. * @param {*} [value] - Set the value of the option. * @return {*} Value of the option. */ option (name, value) { if (arguments.length > 1) { this.options[name] = value } return this.options[name] } /** * Get or set a handler. * @param {string} type - Handler type. * @param {string} name - Handler name. * @param {Function} [fn] - Set the handler method. */ handler (type, name, fn) { if (arguments.length > 2) { this.handlers[type][name] = fn this._refreshHandlersInParams({[type]: {[name]: fn}}) } return this.handlers[type][name] } /** * Get or set a parser. * @param {string} name - Parser name. * @param {parserFn} [fn] - Set the parser method. * @return {parserFn} The parser method. */ parser (name, fn) { return this.handler('parsers', ...arguments) } /** * Get or set a formatter. * @param {string} name - Formatter name. * @param {formatterFn} [fn] - Set the formatter method. * @return {formatterFn} The formatter method. */ formatter (name, fn) { return this.handler('formatters', ...arguments) } /** * Get or set a validator. * @param {string} name - Validator name. * @param {validatorFn} [fn] - Set the validator method. * @return {validatorFn} The validator method. */ validator (name, fn) { return this.handler('validators', ...arguments) } /** * Get a param * @param {string} name - Param name. * @return {QuerymenParam|undefined} The param or undefined if it doesn't exist. */ get (name) { name = this._getSchemaParamName(name) return this.params[name] } /** * Set param value. * @param {string} name - Param name. * @param {*} value - Param value. * @param {Object} [options] - Param options. * @return {QuerymenParam|undefined} The param or undefined if it doesn't exist. */ set (name, value, options) { name = this._getSchemaParamName(name) if (this.params[name]) { let param = this.params[name] param.value(value) _.forIn(options, (optionValue, option) => { param.option(option, optionValue) }) return param } else { return } } /** * Add param. * @param {string} name - Param name. * @param {*} [value] - Param value. * @param {Object} [options] - Param options. * @return {QuerymenParam|boolean} The param or false if param is set to false in schema options. */ add (name, value, options) { if (name instanceof QuerymenParam) { options = name.options value = name.value() name = name.name } name = this._getSchemaParamName(name) if (this.options[name] === false) { return false } options = this._parseParamOptions(options) options = _.assign({bindTo: 'query'}, this._params[name], options) this.params[name] = new QuerymenParam(name, value, options, this) this._refreshHandlersInParams(undefined, {[name]: this.params[name]}) return this.params[name] } /** * Get, set or add param. * @param {string} name - Param name. * @param {*} [value] - Param value. * @param {Object} [options] - Param options. * @return {QuerymenParam|undefined} The param or undefined if it doesn't exist. */ param (name, value, options) { if (arguments.length === 1) { return this.get(name) } return this.set(name, value, options) || this.add(name, value, options) } /** * Parse values of the schema params. * @param {Object} [values] - Object with {param: value} pairs to parse. * @return {Object} Parsed object. */ parse (values = {}) { let query = {} _.forIn(this.params, (param) => { let value = values[this._getQueryParamName(param.name)] if (!_.isNil(value)) { param.value(value) } }) _.forIn(this.params, (param) => { if (this.options[this._getSchemaParamName(param.name)] === false) return let bind = param.options.bindTo query[bind] = _.merge(query[bind], param.parse()) }) return query } /** * Validate values of the schema params. * @param {Object} [values] - Object with {param: value} pairs to validate. * @param {Function} [next] - Callback to be called with error * @return {boolean} Result of the validation. */ validate (values = {}, next = (error) => !error) { let error if (_.isFunction(values)) { next = values values = {} } _.forIn(this.params, (param) => { let value = values[this._getQueryParamName(param.name)] if (!_.isNil(value)) { param.value(value) } }) for (let i in this.params) { if (error) break let param = this.params[i] param.validate((err) => { error = err }) } return next(error) } _refreshHandlersInParams (handlers = this.handlers, params = this.params) { _.forIn(handlers, (typedHandler, type) => { _.forIn(typedHandler, (handler, name) => { _.forIn(params, (param) => { param.handler(type, name, handler) }) }) }) } _getSchemaParamName (paramName) { return _.findKey(this.options, (option) => option === paramName) || paramName } _getQueryParamName (paramName) { return _.isString(this.options[paramName]) ? this.options[paramName] : paramName } _parseParamOptions (options) { if (_.isArray(options) && options.length) { let innerOption = this._parseParamOptions(options[0]) options = {} if (innerOption.type) { options.type = [innerOption.type] } if (innerOption.default) { options.default = innerOption.default } } else if (_.isString(options)) { options = {default: options} } else if (_.isNumber(options)) { options = {type: Number, default: options} } else if (_.isBoolean(options)) { options = {type: Boolean, default: options} } else if (_.isDate(options)) { options = {type: Date, default: options} } else if (_.isRegExp(options)) { options = {type: RegExp, default: options} } else if (_.isFunction(options)) { options = {type: options} } return options || {} } } ================================================ FILE: test/index.js ================================================ import request from 'supertest' import express from 'express' import mongoose from 'mongoose' import test from 'tape' import querymen from '../src' import './querymen-param' import './querymen-schema' mongoose.connect('mongodb://localhost/querymen-test') const entitySchema = mongoose.Schema({}) const testSchema = mongoose.Schema({ title: String, entity: { type: mongoose.Schema.ObjectId, ref: 'Entity' }, createdAt: { type: Date, default: Date.now }, location: { type: [Number], index: '2d' } }) const Entity = mongoose.model('Entity', entitySchema) const Test = mongoose.model('Test', testSchema) const route = (...args) => { const app = express() app.get('/tests', querymen.middleware(...args), (req, res) => { Test.find(req.querymen.query, req.querymen.select, req.querymen.cursor).then((items) => { res.status(200).json(items) }).catch((err) => { res.status(500).send(err) }) }) app.use(querymen.errorHandler()) return app } test('Prototype pollution', (t) => { const { toString } = {} querymen.handler('__proto__', 'toString', 'JHU') t.ok({}.toString === toString, 'should not be vulnerable to prototype pollution') querymen.handler('formatters', '__proto__', { toString: 'JHU' }) t.ok({}.toString === toString, 'should not be vulnerable to prototype pollution') querymen.handler('validators', '__proto__', { toString: 'JHU' }) t.ok({}.toString === toString, 'should not be vulnerable to prototype pollution') t.end() }) test('Querymen handler', (t) => { t.notOk(querymen.parser('testParser'), 'should not get nonexistent parser') t.notOk(querymen.formatter('testFormatter'), 'should not get nonexistent formatter') t.notOk(querymen.validator('testValidator'), 'should not get nonexistent validator') querymen.parser('testParser', () => 'test') querymen.formatter('testFormatter', () => 'test') querymen.validator('testValidator', () => ({valid: false})) t.ok(querymen.parser('testParser'), 'should get parser') t.ok(querymen.formatter('testFormatter'), 'should get formatter') t.ok(querymen.validator('testValidator'), 'should get validator') let schema = new querymen.Schema({test: String}) t.ok(schema.parser('testParser'), 'should get parser in schema') t.ok(schema.formatter('testFormatter'), 'should get formatter in schema') t.ok(schema.validator('testValidator'), 'should get validator in schema') t.ok(schema.param('test').parser('testParser'), 'should get parser in param') t.ok(schema.param('test').formatter('testFormatter'), 'should get formatter in param') t.ok(schema.param('test').validator('testValidator'), 'should get validator in param') t.end() }) test('Querymen middleware', (t) => { t.plan(12) Test.remove({}).then(() => { return Entity.create({}) }).then((entity) => { return Test.create( {title: 'Test', entity, createdAt: new Date('2016-04-25T10:05'), location: [-44.1, -22.0]}, {title: 'Example', createdAt: new Date('2016-04-24T10:05'), location: [-44.3, -22.0]}, {title: 'Spaced test', createdAt: new Date('2016-04-23T10:05'), location: [-44.2, -22.0]}, {title: 'nice!', createdAt: new Date('2016-04-22T10:05'), location: [-44.4, -22.0]} ) }).then((test1) => { let app = route({ entity: testSchema.tree.entity }) request(app) .get('/tests') .query({entity: test1.entity.id}) .expect(200) .end((err, res) => { if (err) throw err t.equal(res.body.length, 1, 'should respond to id filter') }) request(app) .get('/tests') .query({page: 50}) .expect(400) .end((err, res) => { if (err) throw err t.equal(res.body.param, 'page', 'should respond with error object') }) request(app) .get('/tests') .expect(200) .end((err, res) => { if (err) throw err t.equal(res.body.length, 4, 'should respond with 4 items') }) request(app) .get('/tests') .query({fields: 'title,-id'}) .expect(200) .end((err, res) => { if (err) throw err t.equal(res.body.length, 4, 'should respond with 4 items') t.true(res.body[0].title, 'should have title property') t.false(res.body[0]._id, 'should not have _id property') t.false(res.body[0].createdAt, 'should not have createdAt property') }) request(route(new querymen.Schema({ before: { type: Date, operator: '$lte', paths: ['createdAt'] }, after: { type: Date, operator: '$gte', paths: ['createdAt'] } }))) .get('/tests') .query({before: '2016-04-23T10:00', after: '2016-04-22T10:00'}) .expect(200) .end((err, res) => { if (err) throw err t.equal(res.body.length, 1, 'should respond with 1 item') t.equal(res.body[0].title, 'nice!', 'should respond with item after and before date') }) request(route(new querymen.Schema({ after: { type: Date, operator: '$gte', paths: ['createdAt'] } }))) .get('/tests') .query({after: '2016-04-25T10:00'}) .expect(200) .end((err, res) => { if (err) throw err t.equal(res.body.length, 1, 'should respond with 1 item') t.equal(res.body[0].title, 'Test', 'should respond with item after date') }) request(route(new querymen.Schema({ near: { geojson: false } }, {near: true}))) .get('/tests') .query({near: '-22.0,-44.0'}) .expect(200) .end((err, res) => { if (err) throw err t.equal(res.body[1].title, 'Spaced test', 'should respond with item near') }) }) }) test.onFinish(() => { mongoose.disconnect() }) ================================================ FILE: test/querymen-param.js ================================================ import test from 'tape' import _ from 'lodash' import {Param} from '../src/' let param = (value, options) => new Param('test', value, options) let date = new Date('2016-04-24') test('QuerymenParam constructor type', (t) => { let type = (value) => param(value).option('type') t.same(type('test'), String, 'should set type string automatically') t.same(type(['test']), String, 'should set type string automatically by array') t.same(type(123455), Number, 'should set type number automatically') t.same(type([123455]), Number, 'should set type number automatically by array') t.same(type(false), Boolean, 'should set type boolean automatically') t.same(type([false]), Boolean, 'should set type boolean automatically by array') t.same(type(new Date()), Date, 'should set type date automatically') t.same(type([new Date()]), Date, 'should set type date automatically by array') t.same(type(/test/i), RegExp, 'should set type regexp automatically') t.same(type([/test/i]), RegExp, 'should set type regexp automatically by array') t.end() }) test('QuerymenParam constructor options', (t) => { let value = (...args) => param(...args).value() t.equal(value('foo', {default: 'test'}), 'foo', 'should not set default value string') t.equal(value(null, {default: 'test'}), 'test', 'should set default value string') t.equal(value(20, {default: 30}), 20, 'should not set default value number') t.equal(value(null, {default: () => 30}), 30, 'should set default value number by function') t.equal(value(null, {type: String, default: 30}), '30', 'should set default value with different type') t.equal(value('Bé_ free!', {normalize: false}), 'Bé_ free!', 'should not normalize value') t.equal(value('Bé_ free!', {normalize: true}), 'be free', 'should normalize value') t.equal(value('lower', {uppercase: false}), 'lower', 'should not uppercase value') t.equal(value('lower', {uppercase: true}), 'LOWER', 'should uppercase value') t.equal(value('UPPER', {lowercase: false}), 'UPPER', 'should not lowercase value') t.equal(value('UPPER', {lowercase: true}), 'upper', 'should lowercase value') t.equal(value(' trim ', {trim: false}), ' trim ', 'should not trim value') t.equal(value(' trim ', {trim: true}), 'trim', 'should trim value') t.equal(value('123', {format: (value) => value + '!'}), '123!', 'should format value') t.end() }) test('QuerymenParam constructor options with multiple value', (t) => { let value = (value, options) => param(value, _.assign(options, {multiple: true})).value() t.same(value('foo,bar', {default: 'test'}), ['foo', 'bar'], 'should not set default value string') t.same(value('Bé_!, Bê!', {normalize: false}), ['Bé_!', 'Bê!'], 'should not normalize value') t.same(value('Bé_!, Bê!', {normalize: true}), ['be', 'be'], 'should normalize value') t.same(value('low,wol', {uppercase: false}), ['low', 'wol'], 'should not uppercase value') t.same(value('low,wol', {uppercase: true}), ['LOW', 'WOL'], 'should uppercase value') t.same(value('UP,PU', {lowercase: false}), ['UP', 'PU'], 'should not lowercase value') t.same(value('UP,PU', {lowercase: true}), ['up', 'pu'], 'should lowercase value') t.same(value(' tr , rt', {trim: false}), [' tr ', ' rt'], 'should not trim value') t.same(value(' tr , rt', {trim: true}), ['tr', 'rt'], 'should trim value') t.same(value('123,321', {format: (value) => value + '!'}), ['123!', '321!'], 'should format value') t.end() }) test('QuerymenParam value', (t) => { let value = (val, type) => param(val, {type: type}).value() let dateValue = (val) => value(val, Date) t.equal(value(23, String), '23', 'should convert 23 to "23"') t.equal(value('23', Number), 23, 'should convert "23" to 23') t.equal(value('1', Boolean), true, 'should convert "1" to true') t.equal(value('0', Boolean), false, 'should convert "0" to false') t.equal(value(1, Boolean), true, 'should convert 1 to true') t.equal(value(0, Boolean), false, 'should convert 0 to false') t.equal(value('true', Boolean), true, 'should convert "true" to true') t.equal(value('false', Boolean), false, 'should convert "false" to false') t.same(value('test', RegExp), /test/i, 'should convert "test" to /test/i') t.same(value(123, RegExp), /123/i, 'should convert 123 to /123/i') t.same(dateValue('2016-04-24'), date, 'should convert sameo string to date') t.same(dateValue('1461456000000'), date, 'should convert timestamp string to date') t.same(dateValue(1461456000000), date, 'should convert number to date') t.end() }) test('QuerymenParam value with multiple value', (t) => { let value = (val, type) => param(val, {type: type, multiple: true}).value() let dateValue = (val) => value(val, Date) t.same(value('23,24', String), ['23', '24'], 'should convert to string') t.same(value('23,24', Number), [23, 24], 'should convert to number') t.same(value('1,0,true,false', Boolean), [true, false, true, false], 'should convert to boolean') t.same(value('foo,bar', RegExp), [/foo/i, /bar/i], 'should convert to regexp') t.same(dateValue('2016-04-24,1461456000000'), [date, date], 'should convert to date') value = (val, type) => param(val, {type: [type]}).value() t.same(value('23,24', String), ['23', '24'], 'should convert to string with array type') t.same(value('23,24', Number), [23, 24], 'should convert to number with array type') t.same(value('1,0,true,false', Boolean), [true, false, true, false], 'should convert to boolean with array type') t.same(value('foo,bar', RegExp), [/foo/i, /bar/i], 'should convert to regexp with array type') t.same(dateValue('2016-04-24,1461456000000'), [date, date], 'should convert to date with array type') t.end() }) test('QuerymenParam validate', (t) => { let validate = (...args) => param(...args).validate() let minDate = new Date('2016-01-01') let maxDate = new Date('2016-04-25') t.true(validate(), 'should validate no options with success') t.true(validate(null, {required: false}), 'should validate non required with success') t.false(validate(null, {required: true}), 'should validate required with error') t.true(validate('test', {required: true}), 'should validate required with success') t.true(validate(null, {min: 4}), 'should validate null value min with success') t.false(validate(3, {min: 4}), 'should validate min with error') t.true(validate(4, {min: 4}), 'should validate min with success') t.false(validate(new Date('2015'), {min: minDate}), 'should validate min date with error') t.true(validate(minDate, {min: minDate}), 'should validate min date with success') t.true(validate(null, {max: 4}), 'should validate null value max with success') t.false(validate(5, {max: 4}), 'should validate max with error') t.true(validate(4, {max: 4}), 'should validate max with success') t.false(validate(new Date('2017'), {max: maxDate}), 'should validate max date with error') t.true(validate(maxDate, {max: maxDate}), 'should validate max date with success') t.true(validate(null, {minlength: 2}), 'should validate null value minlength with success') t.false(validate('a', {minlength: 2}), 'should validate minlength with error') t.true(validate('ab', {minlength: 2}), 'should validate minlength with success') t.true(validate(null, {maxlength: 2}), 'should validate null value maxlength with success') t.false(validate('abc', {maxlength: 2}), 'should validate maxlength with error') t.true(validate('ab', {maxlength: 2}), 'should validate maxlength with success') t.true(validate(null, {enum: ['ab']}), 'should validate null value enum with success') t.false(validate('a', {enum: ['ab']}), 'should validate enum with error') t.true(validate('ab', {enum: ['ab']}), 'should validate enum with success') t.true(validate(null, {match: /^\D+$/i}), 'should validate null value match with success') t.false(validate('3', {match: /^\D+$/i}), 'should validate match with error') t.true(validate('ab', {match: /^\D+$/i}), 'should validate match with success') t.false(validate(null, {validate: (value) => ({valid: false})}), 'should validate option with error') t.true(validate(null, {validate: (value) => ({valid: true})}), 'should validate option with success') t.false(param().validate((err) => err), 'should validate with callback') t.end() }) test('QuerymenParam validate with multiple value', (t) => { let validate = (value, options) => param(value, _.assign(options, {multiple: true})).validate() t.false(validate('test,', {required: true}), 'should validate required with error') t.true(validate('test', {required: true}), 'should validate required with success') t.false(validate('3,4', {min: 4}), 'should validate min with error') t.true(validate('4,5', {min: 4}), 'should validate min with success') t.false(validate('4,5', {max: 4}), 'should validate max with error') t.true(validate('3,4', {max: 4}), 'should validate max with success') t.false(validate('ab', {minlength: 2}), 'should validate minlength with error') t.true(validate('a,ab', {minlength: 2}), 'should validate minlength with success') t.false(validate('ab', {maxlength: 0}), 'should validate maxlength with error') t.true(validate('ab,abc', {maxlength: 2}), 'should validate maxlength with success') t.true(validate('ab', {maxlength: 2}), 'should validate maxlength with success') t.false(validate('ab,a', {enum: ['ab']}), 'should validate enum with error') t.true(validate('ab,ab', {enum: ['ab']}), 'should validate enum with success') t.false(validate('ab,3', {match: /^\D+$/i}), 'should validate match with error') t.true(validate('ab,a', {match: /^\D+$/i}), 'should validate match with success') t.end() }) test('QuerymenParam parse', (t) => { let parse = (...args) => param(...args).parse() t.same(parse(), {}, 'should parse nothing') t.same(parse(123), {test: 123}, 'should parse $eq as default') t.same(parse('123,456', {multiple: true}), {test: {$in: ['123', '456']}}, 'should parse $in') t.same(parse('123', {operator: '$ne'}), {test: {$ne: '123'}}, 'should parse $ne') t.same(parse('123,456', {operator: '$ne', multiple: true}), {test: {$nin: ['123', '456']}}, 'should parse $nin') t.same(parse(123, {operator: '$gt'}), {test: {$gt: 123}}, 'should parse $gt') t.same(parse(123, {operator: '$gte'}), {test: {$gte: 123}}, 'should parse $gte') t.same(parse(123, {operator: '$lt'}), {test: {$lt: 123}}, 'should parse $lt') t.same(parse(123, {operator: '$lte'}), {test: {$lte: 123}}, 'should parse $lte') t.same(parse(/test/i, {operator: '$ne'}), {test: {$not: /test/i}}, 'should parse $not') t.same(parse(123, {parse: (value, path, operator) => ({[path]: operator})}), {test: '$eq'}, 'should parse option') t.end() }) test('QuerymenParam parse $or', (t) => { let or = (value, options) => param(value, _.assign({paths: ['p1', 'p2']}, options)).parse() let eqMultiple = {$or: [{p1: {$in: ['1', '2']}}, {p2: {$in: ['1', '2']}}]} let neMultiple = {$or: [{p1: {$nin: ['1', '2']}}, {p2: {$nin: ['1', '2']}}]} t.same(or(123), {$or: [{p1: 123}, {p2: 123}]}, 'should parse $eq') t.same(or('1,2', {multiple: true}), eqMultiple, 'should parse $in') t.same(or(123, {operator: '$ne'}), {$or: [{p1: {$ne: 123}}, {p2: {$ne: 123}}]}, 'should parse $ne') t.same(or('1,2', {operator: '$ne', multiple: true}), neMultiple, 'should parse $nin') t.same(or(123, {operator: '$gt'}), {$or: [{p1: {$gt: 123}}, {p2: {$gt: 123}}]}, 'should parse $gt') t.same(or(123, {operator: '$gte'}), {$or: [{p1: {$gte: 123}}, {p2: {$gte: 123}}]}, 'should parse $gte') t.same(or(123, {operator: '$lt'}), {$or: [{p1: {$lt: 123}}, {p2: {$lt: 123}}]}, 'should parse $lt') t.same(or(123, {operator: '$lte'}), {$or: [{p1: {$lte: 123}}, {p2: {$lte: 123}}]}, 'should parse $lte') t.same(or(123, {parse: (value, path, operator) => ({[path]: operator})}), {$or: [{p1: '$eq'}, {p2: '$eq'}]}, 'should parse option') t.end() }) test('QuerymenParam option', (t) => { let optionParam = param() t.true(param().option('paths'), 'should get an option') t.false(param().option('test'), 'should not get a nonexistent option') t.true(optionParam.option('test', true), 'should set a custom option') t.true(optionParam.option('test'), 'should get a custom option') t.end() }) test('QuerymenParam formatter', (t) => { let fParam = param() fParam.formatter('capitalize', (capitalize, value, param) => { t.equal(param.name, 'test', 'should pass param object to formatter method') return capitalize ? _.capitalize(value) : value }) t.true(param().formatter('default'), 'should get formatter') t.false(param().formatter('capitalize'), 'should not get nonexistent formatter') t.true(fParam.formatter('capitalize'), 'should get custom formatter') t.equal(fParam.value('TEST'), 'TEST', 'should not apply custom formatter if option was not set') t.true(fParam.option('capitalize', true), 'should set formatter option to true') t.equal(fParam.value('TEST'), 'Test', 'should apply custom formatter') t.false(fParam.option('capitalize', false), 'should set formatter option to false') t.equal(fParam.value('TEST'), 'TEST', 'should not apply custom formatter if option is false') t.end() }) test('QuerymenParam parser', (t) => { let pParam = param(null, {multiple: true}) pParam.parser('elemMatch', (elemMatch, value, path, operator, param) => { t.true(path, 'should pass path to parser') t.true(operator, 'should pass operator to parser') return elemMatch ? {[path]: {$elemMatch: {[elemMatch]: {[operator]: value}}}} : {[path]: operator !== '$eq' ? {[operator]: value} : value} }) t.false(param().parser('elemMatch'), 'should not get nonexistent parser') t.true(pParam.parser('elemMatch'), 'should get custom parser') t.same(pParam.parse('test'), {test: 'test'}, 'should not apply custom parser if option was not set') t.true(pParam.option('elemMatch', 'path'), 'should set parser option to true') t.same(pParam.parse('test'), {test: {$elemMatch: {path: {$eq: 'test'}}}}, 'should apply custom parser') t.same(pParam.parse('test,foo'), {test: {$elemMatch: {path: {$in: ['test', 'foo']}}}}, 'should apply custom parser to multiple values') t.false(pParam.option('elemMatch', false), 'should set parser option to false') t.same(pParam.parse('test'), {test: 'test'}, 'should not apply custom parser if option is false') t.end() }) test('QuerymenParam validator', (t) => { let vParam = param(null, {multiple: true}) vParam.validator('isPlural', (isPlural, value, param) => { t.equal(param.name, 'test', 'should pass param object to validator method') return {valid: !isPlural || value.toLowerCase().substr(-1) === 's'} }) t.true(param().validator('required'), 'should get validator') t.false(param().validator('isPlural'), 'should not get nonexistent validator') t.true(vParam.validator('isPlural'), 'should get custom validator') t.true(vParam.validate('test'), 'should not apply custom validator if option was not set') t.true(vParam.option('isPlural', true), 'should set validator option to true') t.false(vParam.validate('test'), 'should apply custom validator and validate false') t.false(vParam.validate('tests,FOO'), 'should apply custom validator to multiple values and validate false') t.true(vParam.validate('tests'), 'should apply custom validator and validate true') t.true(vParam.validate('tests,FOOS'), 'should apply custom validator to multiple values and validate true') t.false(vParam.option('isPlural', false), 'should set validator option to false') t.true(vParam.validate('test'), 'should not apply custom validator if option is false') t.end() }) ================================================ FILE: test/querymen-schema.js ================================================ import test from 'tape' import {Schema, Param} from '../src/' let schema = (params, options) => new Schema(params, options) test('QuerymenSchema add', (t) => { let add = (...args) => schema().add('test', ...args).value() t.true(schema().add(new Param('test')), 'should add a param with instance') t.true(schema().add('test'), 'should add a param') t.equal(add('123'), '123', 'should add a param with value') t.true(schema().add('test', null, {test: true}).option('test'), 'should add a param with option') t.equal(add(null, '123'), '123', 'should add a param with default option string') t.equal(add(null, 123), 123, 'should add a param with default option number') t.equal(add(null, true), true, 'should add a param with default option boolean') t.same(add(null, new Date('2016')), new Date('2016'), 'should add a param with default option date') t.same(add(null, /123/i), /123/i, 'should add a param with default option regexp') t.equal(add(123, String), '123', 'should add a param with type option string') t.equal(add('123', Number), 123, 'should add a param with type option number') t.equal(add('123', Boolean), true, 'should add a param with type option boolean') t.same(add('2016', Date), new Date('2016'), 'should add a param with type option date') t.same(add('123', RegExp), /123/i, 'should add a param with type option regexp') t.same(add(null, ['123']), '123', 'should add a param with default option string array') t.same(add(null, [123]), 123, 'should add a param with default option number array') t.same(add(null, [true]), true, 'should add a param with default option boolean array') t.same(add(null, [new Date('2016')]), new Date('2016'), 'should add a param with default option date array') t.same(add(null, [/123/i]), /123/i, 'should add a param with default option regexp array') t.same(add(123, [String]), '123', 'should add a param with type option string array') t.same(add('123,456', [Number]), [123, 456], 'should add a param with type option number array') t.same(add('123,0', [Boolean]), [true, false], 'should add a param with type option boolean array') t.same(add('2016,2017', [Date]), [new Date('2016'), new Date('2017')], 'should add a param with type option date array') t.same(add('123,456', [RegExp]), [/123/i, /456/i], 'should add a param with type option regexp array') t.end() }) test('QuerymenSchema get', (t) => { let mySchema = schema() mySchema.add('test') t.false(schema().get('test'), 'should not get a nonexistent param') t.true(mySchema.get('test'), 'should get a param') t.end() }) test('QuerymenSchema set', (t) => { let mySchema = schema() mySchema.add('test') t.false(schema().set('test', '123'), 'should not set a nonexistent param') t.true(mySchema.set('test', '123'), 'should set a param') t.true(mySchema.set('test', '123', {test: true}).option('test'), 'should set param option') t.end() }) test('QuerymenSchema option', (t) => { let mySchema = schema() t.equal(mySchema.option('test', false), false, 'should set option') t.equal(mySchema.option('test'), false, 'should get option') t.false(mySchema.add('test'), 'should not add disallowed param') t.end() }) test('QuerymenSchema param', (t) => { let mySchema = schema() t.false(mySchema.param('test'), 'should not get a nonexistent param') t.true(mySchema.param('test', null), 'should add a param') t.true(mySchema.param('test'), 'should get a param') t.true(mySchema.param('test', '123'), 'should set a param') t.end() }) test('QuerymenSchema formatter', (t) => { let mySchema = schema({test: '123'}) let formatter = mySchema.formatter('scream', (scream, value) => { return scream ? value.toUpperCase() : value }) t.true(formatter, 'should create a formatter') t.false(schema().formatter('scream'), 'should not get a nonexistent formatter') t.true(mySchema.formatter('scream'), 'should get a formatter') t.true(mySchema.param('test').formatter('scream'), 'should get param formatter') t.equal(mySchema.param('test').value(), '123', 'should not format value') t.true(mySchema.param('test').option('scream', true), 'should set param option') t.equal(mySchema.param('test').value('help'), 'HELP', 'should format value') t.true(mySchema.param('f', null).formatter('scream'), 'should get lazy param formatter') t.end() }) test('QuerymenSchema validator', (t) => { let mySchema = schema({test: 'help'}) let validator = mySchema.validator('isPlural', (isPlural, value, param) => ({ valid: !isPlural || value.toLowerCase().substr(-1) === 's', message: param.name + ' must be in plural form.' })) t.true(validator, 'should create a validator') t.false(schema().validator('isPlural'), 'should not get a nonexistent validator') t.true(mySchema.validator('isPlural'), 'should get a validator') t.true(mySchema.param('test').validator('isPlural'), 'should get param validator') t.true(mySchema.validate(), 'should not apply validator') t.true(mySchema.param('test').option('isPlural', true), 'should set param option') t.false(mySchema.validate(), 'should apply validator and validate false') t.true(mySchema.validate({test: 'helps'}), 'should apply validator and validate true') t.true(mySchema.param('v', null).validator('isPlural'), 'should get lazy param validator') mySchema.validate((err) => t.false(err, 'should call validation success')) mySchema.validate({test: 'help'}, (err) => t.false(err.valid, 'should call validation error')) t.end() }) test('QuerymenSchema parser', (t) => { let mySchema = schema({test: 'help'}) let parser = mySchema.parser('elemMatch', (elemMatch, value, path) => { return elemMatch ? {[path]: {$elemMatch: {[elemMatch]: value}}} : value }) t.true(parser, 'should create a parser') t.false(schema().parser('elemMatch'), 'should not get a nonexistent parser') t.true(mySchema.parser('elemMatch'), 'should get a parser') t.true(mySchema.param('test').parser('elemMatch'), 'should get param parser') t.same(mySchema.parse().query, {test: 'help'}, 'should not apply parser') t.true(mySchema.param('test').option('elemMatch', 'prop'), 'should set param option') t.same(mySchema.parse().query, {test: {$elemMatch: {prop: 'help'}}}, 'should apply parser') t.true(mySchema.param('p', null).parser('elemMatch'), 'should get lazy param parser') t.end() }) test('QuerymenSchema name', (t) => { let mySchema = schema({}, {page: 'p'}) let name = (type, name) => mySchema[`_get${type}ParamName`](name) t.equal(name('Schema', 'p'), 'page', 'should get schema param name by query param name') t.equal(name('Schema', 'page'), 'page', 'should get schema param name by itself') t.equal(name('Query', 'page'), 'p', 'should get query param name by schema param name') t.equal(name('Query', 'p'), 'p', 'should get query param name by itself') mySchema = schema({test: String}, {test: 't'}) t.equal(name('Schema', 't'), 'test', 'should get custom schema param name by query param name') t.equal(name('Schema', 'test'), 'test', 'should get custom schema param name by itself') t.equal(name('Query', 'test'), 't', 'should get custom query param name by schema param name') t.equal(name('Query', 't'), 't', 'should get custom query param name by itself') t.end() }) test('QuerymenSchema default parse', (t) => { let parse = (...args) => schema().parse(...args) t.same(parse({q: 'testing'}).query.keywords, /testing/i, 'should parse q') t.same(parse().select, {}, 'should not parse undefined select') t.same(parse({fields: ''}).select, {}, 'should not parse empty select') t.same(parse({fields: 'a'}).select, {a: 1}, 'should parse one select') t.same(parse({fields: '-id'}).select, {_id: 0}, 'should parse fields=id to fields=_id') t.same(parse({fields: 'id'}).select, {_id: 1}, 'should parse fields=id to fields=_id') t.same(parse({fields: '-a,b,+c'}).select, {a: 0, b: 1, c: 1}, 'should parse multiple select') t.same(parse({page: 2, limit: 10}).cursor.skip, 10, 'should parse page') t.same(parse({limit: 10}).cursor.limit, 10, 'should parse limit') t.same(parse({sort: ''}).cursor.sort, {createdAt: -1}, 'should not parse empty sort') t.same(parse({sort: '-a'}).cursor.sort, {a: -1}, 'should parse sort') t.same(parse({sort: '-a,b,+c'}).cursor.sort, {a: -1, b: 1, c: 1}, 'should parse multiple sort') t.same(schema({distance: 0}).parse({distance: 23}).query.distance, 23, 'should parse any') let near = (params, values) => schema(params, {near: true}).parse(values).query t.false(parse({near: '-22.84377,-44.3213214'}).query.location, 'should not enable near param by default') t.same( near({}, {near: '-22.84377,-44.3213214'}).location, {$near: {$geometry: {type: 'Point', coordinates: [-44.3213214, -22.84377]}}}, 'should parse near') t.same( near({}, {near: '-22.84377,-44.3213214', min_distance: 56}).location, {$near: {$geometry: {type: 'Point', coordinates: [-44.3213214, -22.84377]}, $minDistance: 56}}, 'should parse near min_distance') t.same( near({}, {near: '-22.84377,-44.3213214', max_distance: 56}).location, {$near: {$geometry: {type: 'Point', coordinates: [-44.3213214, -22.84377]}, $maxDistance: 56}}, 'should parse near max_distance') t.same( near({near: {min_distance: false}}, {near: '-22.84377,-44.3213214', min_distance: 56}).location, {$near: {$geometry: {type: 'Point', coordinates: [-44.3213214, -22.84377]}}}, 'should not parse near min_distance') t.same( near({near: {max_distance: false}}, {near: '-22.84377,-44.3213214', max_distance: 56}).location, {$near: {$geometry: {type: 'Point', coordinates: [-44.3213214, -22.84377]}}}, 'should not parse near max_distance') t.false(near({}, {min_distance: 56}).min_distance, 'should not parse min_distance without near') t.false(near({}, {max_distance: 56}).max_distance, 'should not parse max_distance without near') t.false(near({}, {near: '-22.84377,-44.3213214', min_distance: 56}).min_distance, 'should not parse min_distance') t.false(near({}, {near: '-22.84377,-44.3213214', max_distance: 56}).max_distance, 'should not parse max_distance') t.same( near({near: {geojson: false}}, {near: '-22.84377,-44.3213214'}).location, {$near: [-44.3213214, -22.84377]}, 'should parse near no geojson') t.same( near({near: {geojson: false}}, {near: '-22.84377,-44.3213214', min_distance: 56}).location, {$near: [-44.3213214, -22.84377], $minDistance: 56 / 6371000}, 'should parse near min_distance no geojson') t.same( near({near: {geojson: false}}, {near: '-22.84377,-44.3213214', max_distance: 56}).location, {$near: [-44.3213214, -22.84377], $maxDistance: 56 / 6371000}, 'should parse near max_distance no geojson') t.end() })