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
================================================
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# Changelog
- [v1.0.0](#v100)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### 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 <hazdiego@gmail.com>",
"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()
})
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
SYMBOL INDEX (27 symbols across 3 files)
FILE: src/index.js
function handler (line 21) | function handler (type, name, fn) {
function parser (line 44) | function parser (name, fn) {
function formatter (line 55) | function formatter (name, fn) {
function validator (line 66) | function validator (name, fn) {
function middleware (line 77) | function middleware (schema, options) {
function errorHandler (line 111) | function errorHandler () {
FILE: src/querymen-param.js
class QuerymenParam (line 5) | class QuerymenParam extends Param {
method constructor (line 14) | constructor (name, value, options = {}) {
method parser (line 38) | parser (name, fn) {
method parse (line 48) | parse (value = this._value, path = this.options.paths) {
FILE: src/querymen-schema.js
class QuerymenSchema (line 8) | class QuerymenSchema {
method constructor (line 15) | constructor (params = {}, options = {}) {
method option (line 151) | option (name, value) {
method handler (line 165) | handler (type, name, fn) {
method parser (line 180) | parser (name, fn) {
method formatter (line 190) | formatter (name, fn) {
method validator (line 200) | validator (name, fn) {
method get (line 209) | get (name) {
method set (line 222) | set (name, value, options) {
method add (line 247) | add (name, value, options) {
method param (line 277) | param (name, value, options) {
method parse (line 290) | parse (values = {}) {
method validate (line 317) | validate (values = {}, next = (error) => !error) {
method _refreshHandlersInParams (line 342) | _refreshHandlersInParams (handlers = this.handlers, params = this.para...
method _getSchemaParamName (line 352) | _getSchemaParamName (paramName) {
method _getQueryParamName (line 356) | _getQueryParamName (paramName) {
method _parseParamOptions (line 360) | _parseParamOptions (options) {
Condensed preview — 15 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (63K chars).
[
{
"path": ".babelrc",
"chars": 36,
"preview": "{\n \"presets\": [\n \"es2015\"\n ]\n}\n"
},
{
"path": ".editorconfig",
"chars": 171,
"preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newli"
},
{
"path": ".eslintrc",
"chars": 124,
"preview": "{\n \"parser\": \"babel-eslint\",\n \"extends\": \"standard\",\n \"env\": {\n \"node\": true,\n \"es6\": true\n },\n \"rules\": {\n "
},
{
"path": ".gitignore",
"chars": 78,
"preview": ".DS_Store\nlogs\n*.log\nnode_modules\ndist\ntmp\ncoverage\n.nyc_output\nnpm-debug.log\n"
},
{
"path": ".travis.yml",
"chars": 90,
"preview": "language: node_js\nservices: mongodb\nnode_js:\n - v6\nafter_script:\n - 'npm run coveralls'\n"
},
{
"path": "CHANGELOG.md",
"chars": 301,
"preview": "<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD "
},
{
"path": "LICENSE",
"chars": 1124,
"preview": "This software is released under the MIT license:\n\nCopyright (c) 2016 Diego Haz hazdiego@gmail.com\n\nPermission is hereby "
},
{
"path": "README.md",
"chars": 8571,
"preview": "# Querymen\n\n[![JS Standard Style][standard-image]][standard-url]\n[![NPM version][npm-image]][npm-url]\n[![Build Status][t"
},
{
"path": "package.json",
"chars": 2295,
"preview": "{\n \"name\": \"querymen\",\n \"version\": \"2.1.4\",\n \"description\": \"Querystring parser middleware for MongoDB, Express and N"
},
{
"path": "src/index.js",
"chars": 2929,
"preview": "/** @module querymen */\nimport _ from 'lodash'\nimport Param from './querymen-param'\nimport Schema from './querymen-schem"
},
{
"path": "src/querymen-param.js",
"chars": 2275,
"preview": "import _ from 'lodash'\nimport Param from 'rich-param'\n\n/** QuerymenParam class. */\nexport default class QuerymenParam ex"
},
{
"path": "src/querymen-schema.js",
"chars": 10529,
"preview": "import _ from 'lodash'\nimport querymen from './'\nimport QuerymenParam from './querymen-param'\n\n/**\n * QuerymenSchema cla"
},
{
"path": "test/index.js",
"chars": 5827,
"preview": "import request from 'supertest'\nimport express from 'express'\nimport mongoose from 'mongoose'\nimport test from 'tape'\nim"
},
{
"path": "test/querymen-param.js",
"chars": 15660,
"preview": "import test from 'tape'\nimport _ from 'lodash'\nimport {Param} from '../src/'\n\nlet param = (value, options) => new Param("
},
{
"path": "test/querymen-schema.js",
"chars": 10794,
"preview": "import test from 'tape'\nimport {Schema, Param} from '../src/'\n\nlet schema = (params, options) => new Schema(params, opti"
}
]
About this extraction
This page contains the full source code of the diegohaz/querymen GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 15 files (59.4 KB), approximately 17.2k tokens, and a symbol index with 27 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.