├── .babelrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── package.json ├── src ├── QueryBuilder.js ├── index.js ├── query.js └── utils │ └── gettype.js └── tests ├── QueryBuilder.test.js ├── gettype.test.js └── query.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist 3 | node_modules 4 | coverage 5 | npm-debug.log 6 | yarn.lock 7 | yarn-error.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public/ 2 | vendor/ 3 | dist/ 4 | coverage/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "trailingComma": "es5" 6 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Requirements 8 | 9 | If the project maintainer has any additional requirements, you will find them listed here. 10 | 11 | 12 | - **[PSR-2 Coding Standard.](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** The easiest way to apply the conventions is to install [PHP CS Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer). 13 | - **Add tests!** Your patch won't be accepted if it doesn't have tests. 14 | - **Document any change in behaviour.** Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | - **Consider our release cycle.** We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 16 | - **Create feature branches.** Don't ask us to pull from your master branch. 17 | - **One pull request per feature.** If you want to do more than one thing, send multiple pull requests. 18 | - **Send coherent history.** Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 19 | 20 | **Happy coding!** 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Coderello 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

JavaScript Query Builder

2 | 3 |

JavaScript Query Builder provides an easy way to build a query string compatible with spatie/laravel-query-builder.

4 | 5 | ## Install 6 | 7 | You can install package using yarn (or npm): 8 | 9 | ```bash 10 | yarn add js-query-builder 11 | ``` 12 | 13 | ## Usage 14 | 15 | Usage of this package is quite convenient. 16 | 17 | ### General example 18 | 19 | Here is a simple example of query building: 20 | 21 | ```js 22 | import { query } from 'js-query-builder'; 23 | 24 | const url = query('/users') 25 | .filter('age', 20) 26 | .sort('-created_at', 'name') 27 | .include('posts', 'comments') 28 | .append('fullname', 'ranking') 29 | .fields({ 30 | posts: ['id', 'name'], 31 | comments: ['id', 'content'], 32 | }) 33 | .param('custom_param', 'value') 34 | .page(1) 35 | .build(); 36 | 37 | console.log(url); 38 | // /users?append=fullname%2Cranking&custom_param=value&fields%5Bcomments%5D=id%2Ccontent&fields%5Bposts%5D=id%2Cname&filter%5Bage%5D=20&include=posts%2Ccomments&page=1&sort=-created_at%2Cname 39 | 40 | console.log(decodeURIComponent(url)); 41 | // /users?append=fullname,ranking&custom_param=value&fields[comments]=id,content&fields[posts]=id,name&filter[age]=20&include=posts,comments&page=1&sort=-created_at,name 42 | ``` 43 | 44 | ### Making requests 45 | 46 | This package does not provide ability to make requests because there is no need. You are not limited to any particular HTTP client. Use can use the one use want. 47 | 48 | Here is an example with `axios`: 49 | 50 | ```js 51 | import axios from 'axios'; 52 | import { query } from 'js-query-builder'; 53 | 54 | const activeUsers = axios.get( 55 | query('/users') 56 | .filter('status', 'active') 57 | .sort('-id') 58 | .page(1) 59 | .build() 60 | ); 61 | ``` 62 | 63 | ### Conditions 64 | 65 | Let's imagine that you need to filter by username only if its length is more that 3 symbols. 66 | 67 | Yeah, you can do it like this: 68 | 69 | ```js 70 | import { query } from 'js-query-builder'; 71 | 72 | const username = 'hi'; 73 | 74 | const q = query('/users'); 75 | 76 | if (username.length > 3) { 77 | q.filter('name', username); 78 | } 79 | 80 | const url = q.build(); 81 | ``` 82 | 83 | But in such case it would be better to chain `.when()` method: 84 | 85 | ```js 86 | import { query } from 'js-query-builder'; 87 | 88 | const username = 'hi'; 89 | 90 | const url = query('/users') 91 | .when( 92 | username.length > 3, 93 | q => q.filter('name', username) 94 | ) 95 | .build(); 96 | ``` 97 | 98 | Looks much more clear, does not it? 99 | 100 | ### Tapping 101 | 102 | Sometimes you may want to tap the builder. `.tap()` method is almost the same as `.when()` but does not require condition. 103 | 104 | ```js 105 | import { query } from 'js-query-builder'; 106 | 107 | const url = query('/users') 108 | .sort('id') 109 | .tap(q => { 110 | console.log(q.build()); 111 | }) 112 | .include('comments') 113 | .build(); 114 | ``` 115 | 116 | ### Forgetting 117 | 118 | You need to forget some filters, sorts, includes etc.? 119 | 120 | Here you are: 121 | 122 | ```js 123 | import { query } from 'js-query-builder'; 124 | 125 | const url = query('/users') 126 | .include('comments', 'posts') 127 | .sort('name') 128 | .forgetInclude('comments') 129 | .build(); 130 | ``` 131 | 132 | ### Customizing parameter names 133 | 134 | There may be cases when you need to customize parameter names. 135 | 136 | You can define custom parameter names globally this way: 137 | 138 | ```js 139 | import { query, QueryBuilder } from 'js-query-builder'; 140 | 141 | // you may make such call is application bootstrapping file 142 | QueryBuilder.defineCustomParameterNames({ 143 | page: 'p', 144 | sort: 's', 145 | }); 146 | 147 | const url = query('/users') 148 | .sort('name') 149 | .page(5) 150 | .tap(q => console.log(decodeURIComponent(q.build()))); 151 | 152 | // /users?p=5&s=name 153 | ``` 154 | 155 | ## Testing 156 | 157 | ```bash 158 | yarn run test 159 | ``` 160 | 161 | ## Contributing 162 | 163 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 164 | 165 | ## Credits 166 | 167 | - [Ilya Sakovich](https://github.com/hivokas) 168 | - [All Contributors](../../contributors) 169 | 170 | Inspired by [robsontenorio/vue-api-query](https://github.com/robsontenorio/vue-api-query). 171 | 172 | ## License 173 | 174 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-query-builder", 3 | "version": "0.2.0", 4 | "description": "An easy way to build a query string compatible with \"spatie/laravel-query-builder\".", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "/src", 8 | "/dist" 9 | ], 10 | "scripts": { 11 | "build": "rm -rf dist/* && babel src -d dist", 12 | "prepare": "jest && npm run format && npm run build", 13 | "test": "jest --watchAll --coverage", 14 | "format": "prettier --write \"**/*.{css,js,vue}\"" 15 | }, 16 | "keywords": [ 17 | "js", 18 | "query", 19 | "builder", 20 | "laravel", 21 | "spatie", 22 | "build" 23 | ], 24 | "author": "Ilya Sakovich", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@babel/cli": "^7.2.3", 28 | "@babel/core": "^7.2.2", 29 | "@babel/preset-env": "^7.3.1", 30 | "@types/jest": "^24.0.0" 31 | }, 32 | "dependencies": { 33 | "@babel/polyfill": "^7.2.5", 34 | "jest": "^24.1.0", 35 | "prettier": "^1.16.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/QueryBuilder.js: -------------------------------------------------------------------------------- 1 | import gettype from './utils/gettype'; 2 | 3 | export default class QueryBuilder { 4 | constructor(baseUrl = '') { 5 | this._baseUrl = baseUrl; 6 | this._filters = {}; 7 | this._sorts = []; 8 | this._includes = []; 9 | this._appends = []; 10 | this._fields = {}; 11 | this._page = null; 12 | this._params = {}; 13 | } 14 | 15 | static defineCustomParameterNames(customParameterNames) { 16 | this._customParameterNames = customParameterNames; 17 | } 18 | 19 | static forgetCustomParameterNames() { 20 | delete this._customParameterNames; 21 | } 22 | 23 | static getParameterName(parameter) { 24 | return this._customParameterNames && this._customParameterNames.hasOwnProperty(parameter) 25 | ? this._customParameterNames[parameter] 26 | : parameter; 27 | } 28 | 29 | baseUrl(baseUrl) { 30 | this._baseUrl = baseUrl; 31 | return this; 32 | } 33 | 34 | param(...args) { 35 | switch (args.length) { 36 | case 1: 37 | if (gettype(args[0]) !== 'object') { 38 | throw new Error(); 39 | } 40 | Object.entries(args[0]).forEach(entry => { 41 | this.param(...entry); 42 | }); 43 | break; 44 | case 2: 45 | if ( 46 | gettype(args[0]) !== 'string' || 47 | ['string', 'number', 'array'].indexOf(gettype(args[1])) === -1 48 | ) { 49 | throw new Error(); 50 | } 51 | this._params[args[0]] = args[1]; 52 | break; 53 | default: 54 | throw new Error(); 55 | } 56 | return this; 57 | } 58 | 59 | forgetParam(...args) { 60 | if (args.length === 0) { 61 | this._params = {}; 62 | } else { 63 | args.forEach(arg => { 64 | switch (gettype(arg)) { 65 | case 'array': 66 | this.forgetParam(...arg); 67 | break; 68 | case 'string': 69 | delete this._params[arg]; 70 | break; 71 | default: 72 | throw new Error(); 73 | } 74 | }); 75 | } 76 | 77 | return this; 78 | } 79 | 80 | include(...args) { 81 | args.forEach(arg => { 82 | switch (gettype(arg)) { 83 | case 'array': 84 | this.include(...arg); 85 | break; 86 | case 'string': 87 | this._includes.push(arg); 88 | break; 89 | default: 90 | throw new Error(); 91 | } 92 | }); 93 | return this; 94 | } 95 | 96 | forgetInclude(...args) { 97 | if (args.length === 0) { 98 | this._includes = []; 99 | } else { 100 | args.forEach(arg => { 101 | switch (gettype(arg)) { 102 | case 'array': 103 | this.forgetInclude(...arg); 104 | break; 105 | case 'string': 106 | this._includes = this._includes.filter(v => v !== arg); 107 | break; 108 | default: 109 | throw new Error(); 110 | } 111 | }); 112 | } 113 | 114 | return this; 115 | } 116 | 117 | append(...args) { 118 | args.forEach(arg => { 119 | switch (gettype(arg)) { 120 | case 'array': 121 | this.append(...arg); 122 | break; 123 | case 'string': 124 | this._appends.push(arg); 125 | break; 126 | default: 127 | throw new Error(); 128 | } 129 | }); 130 | return this; 131 | } 132 | 133 | forgetAppend(...args) { 134 | if (args.length === 0) { 135 | this._appends = []; 136 | } else { 137 | args.forEach(arg => { 138 | switch (gettype(arg)) { 139 | case 'array': 140 | this.forgetAppend(...arg); 141 | break; 142 | case 'string': 143 | this._appends = this._appends.filter(v => v !== arg); 144 | break; 145 | default: 146 | throw new Error(); 147 | } 148 | }); 149 | } 150 | 151 | return this; 152 | } 153 | 154 | filter(...args) { 155 | switch (args.length) { 156 | case 1: 157 | if (gettype(args[0]) !== 'object') { 158 | throw new Error(); 159 | } 160 | Object.entries(args[0]).forEach(entry => { 161 | this.filter(...entry); 162 | }); 163 | break; 164 | case 2: 165 | if ( 166 | gettype(args[0]) !== 'string' || 167 | ['string', 'number', 'array'].indexOf(gettype(args[1])) === -1 168 | ) { 169 | throw new Error(); 170 | } 171 | this._filters[args[0]] = args[1]; 172 | break; 173 | default: 174 | throw new Error(); 175 | } 176 | return this; 177 | } 178 | 179 | forgetFilter(...args) { 180 | if (args.length === 0) { 181 | this._filters = {}; 182 | } else { 183 | args.forEach(arg => { 184 | switch (gettype(arg)) { 185 | case 'array': 186 | this.forgetFilter(...arg); 187 | break; 188 | case 'string': 189 | delete this._filters[arg]; 190 | break; 191 | default: 192 | throw new Error(); 193 | } 194 | }); 195 | } 196 | 197 | return this; 198 | } 199 | 200 | sort(...args) { 201 | args.forEach(arg => { 202 | switch (gettype(arg)) { 203 | case 'array': 204 | this.sort(...arg); 205 | break; 206 | case 'string': 207 | this._sorts.push(arg); 208 | break; 209 | default: 210 | throw new Error(); 211 | } 212 | }); 213 | return this; 214 | } 215 | 216 | forgetSort(...args) { 217 | if (args.length === 0) { 218 | this._sorts = []; 219 | } else { 220 | args.forEach(arg => { 221 | switch (gettype(arg)) { 222 | case 'array': 223 | this.forgetSort(...arg); 224 | break; 225 | case 'string': 226 | this._sorts = this._sorts.filter(v => v !== arg); 227 | break; 228 | default: 229 | throw new Error(); 230 | } 231 | }); 232 | } 233 | 234 | return this; 235 | } 236 | 237 | fields(...args) { 238 | switch (args.length) { 239 | case 1: 240 | if (gettype(args[0]) !== 'object') { 241 | throw new Error(); 242 | } 243 | Object.entries(args[0]).forEach(entry => { 244 | this.fields(...entry); 245 | }); 246 | break; 247 | case 2: 248 | if (gettype(args[0]) !== 'string' || gettype(args[1]) !== 'array') { 249 | throw new Error(); 250 | } 251 | this._fields[args[0]] = args[1]; 252 | break; 253 | default: 254 | throw new Error(); 255 | } 256 | return this; 257 | } 258 | 259 | forgetFields(...args) { 260 | if (args.length === 0) { 261 | this._fields = {}; 262 | } else { 263 | args.forEach(arg => { 264 | switch (gettype(arg)) { 265 | case 'array': 266 | this.forgetFields(...arg); 267 | break; 268 | case 'string': 269 | delete this._fields[arg]; 270 | break; 271 | default: 272 | throw new Error(); 273 | } 274 | }); 275 | } 276 | 277 | return this; 278 | } 279 | 280 | page(page) { 281 | if (gettype(page) !== 'number' && gettype(page) !== 'string') { 282 | throw new Error(); 283 | } 284 | this._page = page; 285 | return this; 286 | } 287 | 288 | forgetPage() { 289 | this._page = null; 290 | return this; 291 | } 292 | 293 | tap(callback) { 294 | if (typeof callback !== 'function') { 295 | throw new Error(); 296 | } 297 | callback(this); 298 | return this; 299 | } 300 | 301 | when(condition, callback) { 302 | if (gettype(callback) !== 'function') { 303 | throw new Error(); 304 | } 305 | condition = gettype(condition) === 'function' ? condition() : condition; 306 | if (condition) { 307 | callback(this); 308 | } 309 | return this; 310 | } 311 | 312 | build() { 313 | const params = []; 314 | 315 | Object.entries(this._filters).forEach(entry => { 316 | params.push([`${QueryBuilder.getParameterName('filter')}[${entry[0]}]`, entry[1]]); 317 | }); 318 | 319 | this._sorts.length && 320 | params.push([QueryBuilder.getParameterName('sort'), this._sorts.join(',')]); 321 | 322 | this._includes.length && 323 | params.push([QueryBuilder.getParameterName('include'), this._includes.join(',')]); 324 | 325 | this._appends.length && 326 | params.push([QueryBuilder.getParameterName('append'), this._appends.join(',')]); 327 | 328 | Object.entries(this._fields).forEach(entry => { 329 | params.push([ 330 | `${QueryBuilder.getParameterName('fields')}[${entry[0]}]`, 331 | entry[1].join(','), 332 | ]); 333 | }); 334 | 335 | if (this._page) { 336 | params.push([QueryBuilder.getParameterName('page'), this._page]); 337 | } 338 | 339 | Object.entries(this._params).forEach(entry => { 340 | params.push(entry); 341 | }); 342 | 343 | const paramsString = params 344 | .sort((a, b) => (a[0] < b[0] ? -1 : 1)) 345 | .map(entry => `${encodeURIComponent(entry[0])}=${encodeURIComponent(entry[1])}`) 346 | .join('&'); 347 | 348 | return `${this._baseUrl}?${paramsString}`; 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import query from './query'; 2 | import QueryBuilder from './QueryBuilder'; 3 | 4 | export { query, QueryBuilder }; 5 | -------------------------------------------------------------------------------- /src/query.js: -------------------------------------------------------------------------------- 1 | import QueryBuilder from './QueryBuilder'; 2 | 3 | export default function query(...args) { 4 | return new QueryBuilder(...args); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/gettype.js: -------------------------------------------------------------------------------- 1 | function gettype(value) { 2 | return {}.toString 3 | .call(value) 4 | .match(/\s([a-zA-Z]+)/)[1] 5 | .toLowerCase(); 6 | } 7 | export default gettype; 8 | -------------------------------------------------------------------------------- /tests/QueryBuilder.test.js: -------------------------------------------------------------------------------- 1 | import QueryBuilder from '../src/QueryBuilder'; 2 | 3 | function qb() { 4 | return new QueryBuilder('/'); 5 | } 6 | 7 | describe('QueryBuilder', () => { 8 | beforeEach(() => { 9 | QueryBuilder.forgetCustomParameterNames(); 10 | }); 11 | 12 | it('should set base url', () => { 13 | expect( 14 | qb() 15 | .baseUrl('/b') 16 | .build() 17 | ).toStrictEqual('/b?'); 18 | }); 19 | 20 | it('should apply custom parameter names', () => { 21 | QueryBuilder.defineCustomParameterNames({ 22 | filter: 'FILTER', 23 | sort: 'SORT', 24 | include: 'INCLUDE', 25 | fields: 'FIELDS', 26 | page: 'PAGE', 27 | }); 28 | 29 | expect( 30 | qb() 31 | .filter('a', 'b') 32 | .sort('c', 'd', 'e') 33 | .include('f', 'a') 34 | .fields('h', ['i', 'v']) 35 | .page(3) 36 | .build() 37 | ).toStrictEqual( 38 | '/?FIELDS%5Bh%5D=i%2Cv&FILTER%5Ba%5D=b&INCLUDE=f%2Ca&PAGE=3&SORT=c%2Cd%2Ce' 39 | ); 40 | }); 41 | 42 | it('should forget custom parameter names', () => { 43 | QueryBuilder.defineCustomParameterNames({ 44 | page: 'PAGE', 45 | }); 46 | 47 | QueryBuilder.forgetCustomParameterNames(); 48 | 49 | expect( 50 | qb() 51 | .page(5) 52 | .build() 53 | ).toStrictEqual('/?page=5'); 54 | }); 55 | 56 | it('should return parameter name', () => { 57 | QueryBuilder.defineCustomParameterNames({ 58 | page: 'PAGE', 59 | }); 60 | 61 | expect(QueryBuilder.getParameterName('page')).toStrictEqual('PAGE'); 62 | expect(QueryBuilder.getParameterName('filter')).toStrictEqual('filter'); 63 | }); 64 | 65 | it('should add params', () => { 66 | expect( 67 | qb() 68 | .param('a', 'b') 69 | .param('c', 'd') 70 | .build() 71 | ).toStrictEqual('/?a=b&c=d'); 72 | 73 | expect( 74 | qb() 75 | .param({ a: 'b', c: 'd' }) 76 | .build() 77 | ).toStrictEqual('/?a=b&c=d'); 78 | }); 79 | 80 | it('should not add params with invalid arguments', () => { 81 | expect(() => 82 | qb() 83 | .param('a') 84 | .build() 85 | ).toThrowError(); 86 | 87 | expect(() => 88 | qb() 89 | .param('a', {}) 90 | .build() 91 | ).toThrowError(); 92 | 93 | expect(() => 94 | qb() 95 | .param({ 96 | b: null, 97 | }) 98 | .build() 99 | ).toThrowError(); 100 | 101 | expect(() => 102 | qb() 103 | .param('a', 'b', 'c') 104 | .build() 105 | ).toThrowError(); 106 | }); 107 | 108 | it('should forget params', () => { 109 | expect( 110 | qb() 111 | .param({ a: 'b', c: 'd', e: 'f' }) 112 | .forgetParam(['a', 'e']) 113 | .build() 114 | ).toStrictEqual('/?c=d'); 115 | 116 | expect( 117 | qb() 118 | .param({ a: 'b', c: 'd', e: 'f' }) 119 | .forgetParam('c', 'e') 120 | .build() 121 | ).toStrictEqual('/?a=b'); 122 | 123 | expect( 124 | qb() 125 | .param({ a: 'b', c: 'd' }) 126 | .forgetParam() 127 | .build() 128 | ).toStrictEqual('/?'); 129 | }); 130 | 131 | it('should not forget params with invalid arguments', () => { 132 | expect(() => 133 | qb() 134 | .forgetParam('a', {}) 135 | .build() 136 | ).toThrowError(); 137 | 138 | expect(() => 139 | qb() 140 | .forgetParam(['b', null]) 141 | .build() 142 | ).toThrowError(); 143 | }); 144 | 145 | it('should add includes', () => { 146 | expect( 147 | qb() 148 | .include('a', 'b') 149 | .build() 150 | ).toStrictEqual('/?include=a%2Cb'); 151 | 152 | expect( 153 | qb() 154 | .include(['c', 'd']) 155 | .build() 156 | ).toStrictEqual('/?include=c%2Cd'); 157 | }); 158 | 159 | it('should not add includes with invalid arguments', () => { 160 | expect(() => 161 | qb() 162 | .include('a', {}) 163 | .build() 164 | ).toThrowError(); 165 | 166 | expect(() => 167 | qb() 168 | .include(['b', null]) 169 | .build() 170 | ).toThrowError(); 171 | }); 172 | 173 | it('should forget includes', () => { 174 | expect( 175 | qb() 176 | .include('a', 'd', 'f', 'b') 177 | .forgetInclude(['d', 'f']) 178 | .build() 179 | ).toStrictEqual('/?include=a%2Cb'); 180 | 181 | expect( 182 | qb() 183 | .include(['a', 'd', 'f', 'b']) 184 | .forgetInclude('a', 'f') 185 | .build() 186 | ).toStrictEqual('/?include=d%2Cb'); 187 | 188 | expect( 189 | qb() 190 | .include(['a', 'd', 'f', 'b']) 191 | .forgetInclude() 192 | .build() 193 | ).toStrictEqual('/?'); 194 | }); 195 | 196 | it('should not forget includes with invalid arguments', () => { 197 | expect(() => 198 | qb() 199 | .forgetInclude(null) 200 | .build() 201 | ).toThrowError(); 202 | 203 | expect(() => 204 | qb() 205 | .forgetInclude({}) 206 | .build() 207 | ).toThrowError(); 208 | 209 | expect(() => 210 | qb() 211 | .forgetInclude([2, undefined]) 212 | .build() 213 | ).toThrowError(); 214 | }); 215 | 216 | it('should add appends', () => { 217 | expect( 218 | qb() 219 | .append('a', 'b') 220 | .build() 221 | ).toStrictEqual('/?append=a%2Cb'); 222 | 223 | expect( 224 | qb() 225 | .append(['c', 'd']) 226 | .build() 227 | ).toStrictEqual('/?append=c%2Cd'); 228 | }); 229 | 230 | it('should not add appends with invalid arguments', () => { 231 | expect(() => 232 | qb() 233 | .append('a', {}) 234 | .build() 235 | ).toThrowError(); 236 | 237 | expect(() => 238 | qb() 239 | .append(['b', null]) 240 | .build() 241 | ).toThrowError(); 242 | }); 243 | 244 | it('should forget appends', () => { 245 | expect( 246 | qb() 247 | .append('a', 'd', 'f', 'b') 248 | .forgetAppend(['d', 'f']) 249 | .build() 250 | ).toStrictEqual('/?append=a%2Cb'); 251 | 252 | expect( 253 | qb() 254 | .append(['a', 'd', 'f', 'b']) 255 | .forgetAppend('a', 'f') 256 | .build() 257 | ).toStrictEqual('/?append=d%2Cb'); 258 | 259 | expect( 260 | qb() 261 | .append(['a', 'd', 'f', 'b']) 262 | .forgetAppend() 263 | .build() 264 | ).toStrictEqual('/?'); 265 | }); 266 | 267 | it('should not forget appends with invalid arguments', () => { 268 | expect(() => 269 | qb() 270 | .forgetAppend(null) 271 | .build() 272 | ).toThrowError(); 273 | 274 | expect(() => 275 | qb() 276 | .forgetAppend({}) 277 | .build() 278 | ).toThrowError(); 279 | 280 | expect(() => 281 | qb() 282 | .forgetAppend([2, undefined]) 283 | .build() 284 | ).toThrowError(); 285 | }); 286 | 287 | it('should apply filters', () => { 288 | expect( 289 | qb() 290 | .filter('a', 'b') 291 | .build() 292 | ).toStrictEqual('/?filter%5Ba%5D=b'); 293 | 294 | expect( 295 | qb() 296 | .filter({ a: 'b', c: 'd' }) 297 | .build() 298 | ).toStrictEqual('/?filter%5Ba%5D=b&filter%5Bc%5D=d'); 299 | 300 | expect( 301 | qb() 302 | .filter({ a: 'b', c: ['f', 'd'] }) 303 | .build() 304 | ).toStrictEqual('/?filter%5Ba%5D=b&filter%5Bc%5D=f%2Cd'); 305 | }); 306 | 307 | it('should not apply filters with invalid arguments', () => { 308 | expect(() => 309 | qb() 310 | .filter('a') 311 | .build() 312 | ).toThrowError(); 313 | 314 | expect(() => 315 | qb() 316 | .filter('a', null) 317 | .build() 318 | ).toThrowError(); 319 | 320 | expect(() => 321 | qb() 322 | .filter({ 323 | a: {}, 324 | b: 'c', 325 | }) 326 | .build() 327 | ).toThrowError(); 328 | 329 | expect(() => 330 | qb() 331 | .filter('a', 'b', 'c') 332 | .build() 333 | ).toThrowError(); 334 | }); 335 | 336 | it('should forget filters', () => { 337 | expect( 338 | qb() 339 | .filter({ a: 'b', c: 'd', e: 'f' }) 340 | .forgetFilter(['a', 'e']) 341 | .build() 342 | ).toStrictEqual('/?filter%5Bc%5D=d'); 343 | 344 | expect( 345 | qb() 346 | .filter({ a: 'b', c: 'd', e: 'f' }) 347 | .forgetFilter('a', 'e') 348 | .build() 349 | ).toStrictEqual('/?filter%5Bc%5D=d'); 350 | 351 | expect( 352 | qb() 353 | .filter({ a: 'b', c: 'd', e: 'f' }) 354 | .forgetFilter() 355 | .build() 356 | ).toStrictEqual('/?'); 357 | }); 358 | 359 | it('should not forget filters with invalid arguments', () => { 360 | expect(() => 361 | qb() 362 | .forgetFilter({ a: 'b' }) 363 | .build() 364 | ).toThrowError(); 365 | 366 | expect(() => 367 | qb() 368 | .forgetFilter([3, {}]) 369 | .build() 370 | ).toThrowError(); 371 | }); 372 | 373 | it('should apply sorts', () => { 374 | expect( 375 | qb() 376 | .sort('a', 'b') 377 | .build() 378 | ).toStrictEqual('/?sort=a%2Cb'); 379 | 380 | expect( 381 | qb() 382 | .sort(['a', 'b']) 383 | .build() 384 | ).toStrictEqual('/?sort=a%2Cb'); 385 | }); 386 | 387 | it('should not apply sorts with invalid arguments', () => { 388 | expect(() => 389 | qb() 390 | .sort(null) 391 | .build() 392 | ).toThrowError(); 393 | 394 | expect(() => 395 | qb() 396 | .sort(['a', {}]) 397 | .build() 398 | ).toThrowError(); 399 | }); 400 | 401 | it('should forget sorts', () => { 402 | expect( 403 | qb() 404 | .sort('a', 'b', 'c', 'd') 405 | .forgetSort(['b', 'c']) 406 | .build() 407 | ).toStrictEqual('/?sort=a%2Cd'); 408 | 409 | expect( 410 | qb() 411 | .sort(['a', 'b', 'c', 'd']) 412 | .forgetSort('a', 'b') 413 | .build() 414 | ).toStrictEqual('/?sort=c%2Cd'); 415 | 416 | expect( 417 | qb() 418 | .sort(['a', 'b', 'c', 'd']) 419 | .forgetSort() 420 | .build() 421 | ).toStrictEqual('/?'); 422 | }); 423 | 424 | it('should not forget sorts with invalid arguments', () => { 425 | expect(() => 426 | qb() 427 | .forgetSort(null) 428 | .build() 429 | ).toThrowError(); 430 | 431 | expect(() => 432 | qb() 433 | .forgetSort({}) 434 | .build() 435 | ).toThrowError(); 436 | 437 | expect(() => 438 | qb() 439 | .forgetSort([2, undefined]) 440 | .build() 441 | ).toThrowError(); 442 | }); 443 | 444 | it('should add fields', () => { 445 | expect( 446 | qb() 447 | .fields('a', ['b', 'c', 'd']) 448 | .build() 449 | ).toStrictEqual('/?fields%5Ba%5D=b%2Cc%2Cd'); 450 | 451 | expect( 452 | qb() 453 | .fields({ a: ['b', 'c'], d: ['e', 'f'] }) 454 | .build() 455 | ).toStrictEqual('/?fields%5Ba%5D=b%2Cc&fields%5Bd%5D=e%2Cf'); 456 | }); 457 | 458 | it('should not add fields with invalid arguments', () => { 459 | expect(() => 460 | qb() 461 | .fields('a') 462 | .build() 463 | ).toThrowError(); 464 | 465 | expect(() => 466 | qb() 467 | .fields('a', 'b') 468 | .build() 469 | ).toThrowError(); 470 | 471 | expect(() => 472 | qb() 473 | .fields({ 474 | a: null, 475 | }) 476 | .build() 477 | ).toThrowError(); 478 | 479 | expect(() => 480 | qb() 481 | .fields('a', 'b', 'c') 482 | .build() 483 | ).toThrowError(); 484 | }); 485 | 486 | it('should forget fields', () => { 487 | expect( 488 | qb() 489 | .fields('a', ['b', 'c']) 490 | .fields('d', ['e', 'f']) 491 | .forgetFields('d') 492 | .build() 493 | ).toStrictEqual('/?fields%5Ba%5D=b%2Cc'); 494 | 495 | expect( 496 | qb() 497 | .fields({ a: ['b', 'c'], d: ['e', 'f'] }) 498 | .forgetFields(['a']) 499 | .build() 500 | ).toStrictEqual('/?fields%5Bd%5D=e%2Cf'); 501 | 502 | expect( 503 | qb() 504 | .fields({ a: ['b', 'c'], d: ['e', 'f'] }) 505 | .forgetFields() 506 | .build() 507 | ).toStrictEqual('/?'); 508 | }); 509 | 510 | it('should not forget fields with invalid arguments', () => { 511 | expect(() => 512 | qb() 513 | .forgetFields({}) 514 | .build() 515 | ).toThrowError(); 516 | 517 | expect(() => 518 | qb() 519 | .forgetFields([null]) 520 | .build() 521 | ).toThrowError(); 522 | }); 523 | 524 | it('should set page', () => { 525 | expect( 526 | qb() 527 | .page(3) 528 | .build() 529 | ).toStrictEqual('/?page=3'); 530 | 531 | expect( 532 | qb() 533 | .page('5') 534 | .build() 535 | ).toStrictEqual('/?page=5'); 536 | }); 537 | 538 | it('should not set page with invalid arguments', () => { 539 | expect(() => 540 | qb() 541 | .page([]) 542 | .build() 543 | ).toThrowError(); 544 | 545 | expect(() => 546 | qb() 547 | .page(null) 548 | .build() 549 | ).toThrowError(); 550 | }); 551 | 552 | it('should forget page', () => { 553 | expect( 554 | qb() 555 | .page(3) 556 | .forgetPage() 557 | .build() 558 | ).toStrictEqual('/?'); 559 | }); 560 | 561 | it('should tap builder', () => { 562 | expect( 563 | qb() 564 | .tap(b => b.param('a', 'b')) 565 | .build() 566 | ).toStrictEqual('/?a=b'); 567 | }); 568 | 569 | it('should not tap builder with invalid arguments', () => { 570 | expect(() => 571 | qb() 572 | .tap('not callback') 573 | .build() 574 | ).toThrowError(); 575 | }); 576 | 577 | it('should conditionally tap builder using when', () => { 578 | expect( 579 | qb() 580 | .when(true, b => b.page(2)) 581 | .build() 582 | ).toStrictEqual('/?page=2'); 583 | 584 | expect( 585 | qb() 586 | .when(false, b => b.page(2)) 587 | .build() 588 | ).toStrictEqual('/?'); 589 | 590 | expect( 591 | qb() 592 | .when(() => true, b => b.page(2)) 593 | .build() 594 | ).toStrictEqual('/?page=2'); 595 | 596 | expect( 597 | qb() 598 | .when(() => false, b => b.page(2)) 599 | .build() 600 | ).toStrictEqual('/?'); 601 | 602 | const name = ''; 603 | 604 | expect( 605 | qb() 606 | .when(name, b => b.filter({ name })) 607 | .build() 608 | ).toStrictEqual('/?'); 609 | }); 610 | 611 | it('should not conditionally tap builder using when with invalid arguments', () => { 612 | expect(() => 613 | qb() 614 | .when(true, 'not callback') 615 | .build() 616 | ).toThrowError(); 617 | }); 618 | 619 | it('should build query string correctly', () => { 620 | expect( 621 | qb() 622 | .filter('a', 'b') 623 | .filter({ d: 'e', f: 'c' }) 624 | .sort('c') 625 | .when(true, b => b.forgetFilter('d')) 626 | .sort(['d', 'e', 'a']) 627 | .tap(b => b.forgetSort(['a'])) 628 | .include('y', 'e', 's') 629 | .forgetInclude(['e']) 630 | .fields({ 631 | h: ['d'], 632 | t: ['g', 'a'], 633 | }) 634 | .forgetFields('h') 635 | .append('b', 'd') 636 | .page(3) 637 | .param({ 638 | a: 'b', 639 | u: 'a', 640 | n: 'p', 641 | }) 642 | .forgetParam('u', 'n') 643 | .build() 644 | ).toStrictEqual( 645 | '/?a=b&append=b%2Cd&fields%5Bt%5D=g%2Ca&filter%5Ba%5D=b&filter%5Bf%5D=c&include=y%2Cs&page=3&sort=c%2Cd%2Ce' 646 | ); 647 | }); 648 | 649 | it('should alphabetically sort params in built string', () => { 650 | expect( 651 | qb() 652 | .fields('b', ['a']) 653 | .fields('c', ['d']) 654 | .param('a', 'd') 655 | .fields('a', ['h']) 656 | .param('z', 'd') 657 | .build() 658 | ).toStrictEqual('/?a=d&fields%5Ba%5D=h&fields%5Bb%5D=a&fields%5Bc%5D=d&z=d'); 659 | }); 660 | }); 661 | -------------------------------------------------------------------------------- /tests/gettype.test.js: -------------------------------------------------------------------------------- 1 | import gettype from '../src/utils/gettype'; 2 | 3 | describe('gettype', () => { 4 | it('should detect type correctly', () => { 5 | expect(gettype('hello')).toBe('string'); 6 | expect(gettype(new String('hello'))).toBe('string'); 7 | expect(gettype({})).toBe('object'); 8 | expect(gettype(new Object())).toBe('object'); 9 | expect(gettype([])).toBe('array'); 10 | expect(gettype(new Array())).toBe('array'); 11 | expect(gettype(2)).toBe('number'); 12 | expect(gettype(new Number(4))).toBe('number'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/query.test.js: -------------------------------------------------------------------------------- 1 | import query from '../src/query'; 2 | import QueryBuilder from '../src/QueryBuilder'; 3 | 4 | describe('query', () => { 5 | it('should return QueryBuilder instance', () => { 6 | expect(query()).toBeInstanceOf(QueryBuilder); 7 | }); 8 | }); 9 | --------------------------------------------------------------------------------