├── .npmrc ├── .mocharc.json ├── .travis.yml ├── History.md ├── .gitignore ├── LICENSE ├── index.js ├── package.json ├── README.md └── test └── test.spec.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["js"], 3 | "require": "should", 4 | "reporter": "spec", 5 | "timeout": "2000", 6 | "watch-files": ["test/**/*.js"], 7 | "exit": "true" 8 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 10 5 | - 12 6 | - stable 7 | script: 8 | - npm run ci 9 | after_script: 10 | - npm install coveralls@2 11 | - cat ./coverage/lcov.info | coveralls 12 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 2.0.0 / 2015-02-28 3 | ================== 4 | 5 | * break: Make strict to array item and add first and last mode for strict string item 6 | * remove peerDependencies qs 7 | 8 | 1.1.0 / 2015-02-27 9 | ================== 10 | 11 | * feat: add strict mode for return string format query param only 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS # 2 | ################### 3 | .DS_Store 4 | .idea 5 | Thumbs.db 6 | tmp/ 7 | temp/ 8 | 9 | 10 | # Node.js # 11 | ################### 12 | node_modules 13 | package-lock.json 14 | yarn.lock 15 | npm-debug.log 16 | yarn-debug.log 17 | yarn-error.log 18 | dist 19 | 20 | 21 | # NYC # 22 | ################### 23 | coverage 24 | *.lcov 25 | .nyc_output -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jonathan Ong me@jongleberry.com 4 | Copyright (c) 2015-present koajs and other contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const merge = require('merge-descriptors'); 2 | 3 | module.exports = function (app, mode, options) { 4 | mode = mode || 'extended'; 5 | const qs = (mode === 'extended') ? require('qs') : require('querystring'); 6 | 7 | const converter = ( 8 | mode === 'strict' && function (value) { return !Array.isArray(value) ? [value] : value } 9 | || 10 | mode === 'first' && function (value) { return Array.isArray(value) ? value[0] : value } 11 | ); 12 | 13 | merge(app.request, { 14 | 15 | /** 16 | * Get parsed query-string. 17 | * 18 | * @return {Object} 19 | * @api public 20 | */ 21 | get query() { 22 | let str = this.querystring; 23 | if (!str) return {}; 24 | 25 | let c = this._querycache = this._querycache || {}; 26 | let query = c[str]; 27 | if (!query) { 28 | c[str] = query = qs.parse(str, options); 29 | if (converter) { 30 | for (let key in query) { 31 | query[key] = converter(query[key]); 32 | } 33 | } 34 | } 35 | return query; 36 | }, 37 | 38 | /** 39 | * Set query-string as an object. 40 | * 41 | * @param {Object} obj 42 | * @api public 43 | */ 44 | set query(obj) { 45 | this.querystring = qs.stringify(obj); 46 | }, 47 | }); 48 | 49 | return app; 50 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-qs", 3 | "description": "qs for koa", 4 | "version": "3.0.1", 5 | "author": { 6 | "name": "Jonathan Ong", 7 | "email": "me@jongleberry.com", 8 | "url": "http://jongleberry.com", 9 | "twitter": "https://twitter.com/jongleberry" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Nick Baugh", 14 | "email": "niftylettuce@gmail.com", 15 | "url": "http://niftylettuce.com/" 16 | }, 17 | { 18 | "name": "Imed Jaberi", 19 | "email": "imed_jebari@hotmail.fr", 20 | "url": "https://www.3imed-jaberi.com/" 21 | } 22 | ], 23 | "dependencies": { 24 | "merge-descriptors": "^1.0.1", 25 | "qs": "^6.12.3" 26 | }, 27 | "devDependencies": { 28 | "koa": "^2.11.0", 29 | "koa-convert": "^1.2.0", 30 | "mocha": "^7.1.2", 31 | "nyc": "^15.0.1", 32 | "rimraf": "^3.0.2", 33 | "should": "^13.2.3", 34 | "supertest": "^4.0.2", 35 | "urllib": "^2.34.2" 36 | }, 37 | "engines": { 38 | "node": ">= 8" 39 | }, 40 | "files": [ 41 | "index.js" 42 | ], 43 | "license": "MIT", 44 | "repository": "koajs/qs", 45 | "scripts": { 46 | "ci": "npm run coverage", 47 | "coverage": "nyc --reporter=lcov --reporter=text-lcov npm test", 48 | "postcoverage": "nyc report", 49 | "precoverage": "rimraf .nyc_output coverage", 50 | "test": "mocha" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koa Querystring 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][travis-image]][travis-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![David deps][david-image]][david-url] 7 | [![node version][node-image]][node-url] 8 | [![npm download][download-image]][download-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/koa-qs.svg?style=flat-square 11 | [npm-url]: https://npmjs.org/package/koa-qs 12 | [travis-image]: https://img.shields.io/travis/koajs/qs.svg?style=flat-square 13 | [travis-url]: https://travis-ci.org/koajs/qs 14 | [coveralls-image]: https://img.shields.io/coveralls/koajs/qs.svg?style=flat-square 15 | [coveralls-url]: https://coveralls.io/r/koajs/qs?branch=master 16 | [david-image]: https://img.shields.io/david/koajs/qs.svg?style=flat-square 17 | [david-url]: https://david-dm.org/koajs/qs 18 | [node-image]: https://img.shields.io/badge/node.js-%3E=_8-green.svg?style=flat-square 19 | [node-url]: http://nodejs.org/download/ 20 | [download-image]: https://img.shields.io/npm/dm/koa-qs.svg?style=flat-square 21 | [download-url]: https://npmjs.org/package/koa-qs 22 | 23 | By default, Koa uses the native `querystring` module which does not provide nesting support. 24 | This patches a koa app with nesting support via the [qs](https://github.com/ljharb/qs) support, 25 | which is also used by Connect and Express. 26 | 27 | Simply wrap a koa app with this module: 28 | 29 | ```js 30 | // Koa 1.x.x 31 | const koa = require('koa') 32 | const app = koa() 33 | require('koa-qs')(app) 34 | // Koa 2.x.x 35 | const Koa = require('koa') 36 | const app = new Koa() 37 | require('koa-qs')(app) 38 | ``` 39 | 40 | ## Optional parse mode 41 | 42 | There're three parse mode. 43 | 44 | ## `extended` mode 45 | 46 | The default mode, use [qs] module. 47 | 48 | ```js 49 | require('koa-qs')(app, 'extended') 50 | ``` 51 | 52 | ## `simple` mode 53 | 54 | Use `querystring` module, same as koa does by default. 55 | If you want to use this mode, don't use this module. 56 | 57 | ## `strict` mode 58 | 59 | This mode make `this.query.foo` return strict `array`. 60 | 61 | ```js 62 | require('koa-qs')(app, 'strict') 63 | ``` 64 | 65 | #### What's different 66 | 67 | A normal request `GET /foo?p=a&q=foo&q=bar`. 68 | 69 | - before patch 70 | 71 | ```js 72 | console.log('%j', this.query); 73 | { 74 | "p": "a", 75 | "q": ["foo", "bar"] 76 | } 77 | ``` 78 | 79 | - after patch 80 | 81 | ```js 82 | console.log('%j', this.query); 83 | { 84 | "p": ["a"], 85 | "q": ["foo", "bar"] 86 | } 87 | ``` 88 | 89 | ## `first` mode 90 | 91 | This mode make `this.query.foo` return strict `string`. Disable multi values. 92 | 93 | If querystring contains multi same name params, return the **first** item. 94 | 95 | ```js 96 | require('koa-qs')(app, 'first') 97 | ``` 98 | 99 | In 95% use cases, application only want `string` query params. 100 | 101 | This patch can avoid some stupid `TypeError` and some security issues like [MongoDB inject](http://www.wooyun.org/bugs/wooyun-2010-086474) 102 | when the developers forget handling query params type check. 103 | 104 | #### What's different 105 | 106 | A normal request `GET /foo?p=a,b&p=b,c`. 107 | 108 | - before patch 109 | 110 | ```js 111 | console.log('%j', this.query.p); 112 | ["a,b", "b,c"] 113 | ``` 114 | 115 | - after patch 116 | 117 | ```js 118 | console.log('%j', this.query.p); 119 | "a,b" 120 | ``` 121 | 122 | ## License 123 | 124 | [MIT](LICENSE) 125 | -------------------------------------------------------------------------------- /test/test.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const Koa = require('koa') 3 | const urllib = require('urllib') 4 | const convert = require('koa-convert') 5 | const qs = require('..') 6 | 7 | describe('Koa Querystring', function () { 8 | it('should work with extended mode by default', function (done) { 9 | let app = qs(new Koa()) 10 | 11 | app.use(convert(function* (next) { 12 | try { 13 | yield* next 14 | } catch (err) { 15 | console.error(err.stack) 16 | } 17 | })) 18 | 19 | app.use(convert(function* () { 20 | this.query.should.eql({ 21 | a: ['1', '2', '3'] 22 | }) 23 | this.query = { 24 | a: ['1', '2'] 25 | } 26 | this.query.should.eql({ 27 | a: ['1', '2'] 28 | }) 29 | this.querystring = 'a[0]=1&a[1]=2&a[2]=3' 30 | this.query.should.eql({ 31 | a: ['1', '2', '3'] 32 | }) 33 | 34 | this.status = 204 35 | })) 36 | 37 | request(app.listen()) 38 | .get('/?a[0]=1&a[1]=2&a[2]=3') 39 | .expect(204, done) 40 | }) 41 | 42 | describe('strict mode: array item', function () { 43 | let app = qs(new Koa(), 'strict') 44 | 45 | app.use(convert(function* () { 46 | this.body = this.query; 47 | })) 48 | 49 | let host 50 | before(function (done) { 51 | app.listen(0, function () { 52 | host = `http://localhost:${this.address().port}` 53 | done() 54 | }) 55 | }) 56 | 57 | it('should return the query params array', function (done) { 58 | urllib.request(`${host}/foo?p=a,b&p=b,c&empty=&empty=&empty=&n=1&n=2&n=1&ok=true`, { 59 | dataType: 'json' 60 | }, function (err, body, res) { 61 | res.statusCode.should.equal(200) 62 | body.should.eql( 63 | { 64 | p: ['a,b', 'b,c'], 65 | empty: ['', '', ''], 66 | n: ['1', '2', '1'], 67 | ok: ['true'] 68 | } 69 | ) 70 | done(err) 71 | }) 72 | }) 73 | 74 | it('should return empty query', function (done) { 75 | urllib.request(host, { 76 | dataType: 'json' 77 | }, function (err, body, res) { 78 | res.statusCode.should.equal(200) 79 | body.should.eql({}) 80 | done(err) 81 | }) 82 | }) 83 | }) 84 | 85 | describe('first mode: first string item', function () { 86 | let app = qs(new Koa(), 'first') 87 | 88 | app.use(convert(function* () { 89 | this.body = this.query; 90 | })) 91 | 92 | let host 93 | before(function (done) { 94 | app.listen(0, function () { 95 | host = `http://localhost:${this.address().port}` 96 | done() 97 | }) 98 | }) 99 | 100 | it('should return the first query params string', function (done) { 101 | urllib.request(`${host}/foo?p=a,b&p=b,c&empty=&empty=&empty=&n=1&n=2&n=1&ok=true`, { 102 | dataType: 'json' 103 | }, function (err, body, res) { 104 | res.statusCode.should.equal(200) 105 | body.should.eql( 106 | { 107 | p: 'a,b', 108 | empty: '', 109 | n: '1', 110 | ok: 'true' 111 | } 112 | ) 113 | done(err) 114 | }) 115 | }) 116 | }) 117 | 118 | describe('custom qs options', function () { 119 | it('should correctly parse numbers and booleans', function (done) { 120 | let app = qs(new Koa(), null, { 121 | decoder: function (str) { 122 | switch (str) { 123 | case 'true': return true; 124 | case 'false': return false; 125 | case 'null': return null; 126 | } 127 | 128 | if (/^[\d.]+$/.test(str)) { 129 | return Number(str); 130 | } 131 | 132 | return str; 133 | } 134 | }) 135 | 136 | app.use(convert(function* (next) { 137 | try { 138 | yield* next 139 | } catch (err) { 140 | console.error(err.stack) 141 | } 142 | })) 143 | 144 | app.use(convert(function* () { 145 | this.body = this.query; 146 | })) 147 | 148 | request(app.listen()) 149 | .get('/?a=str&b=1&c=3.1415&d=true&e=false&f=null&g=2.7182e') 150 | .expect(function (res) { 151 | res.body.should.eql({ 152 | a: 'str', 153 | b: 1, 154 | c: 3.1415, 155 | d: true, 156 | e: false, 157 | f: null, 158 | g: '2.7182e', 159 | }) 160 | }) 161 | .expect(200, done); 162 | }) 163 | }) 164 | }) 165 | --------------------------------------------------------------------------------