├── .babelrc ├── .eslintignore ├── .eslintrc.yml ├── .gitattributes ├── .gitignore ├── .lintstagedrc.json ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── muto-logo.png ├── build ├── build-parser.js ├── cosmiconfig-noop.js ├── noop.js └── webpack.config.js ├── deploy-docs.sh ├── docs ├── custom-conf.md ├── documentation.yml ├── expr-bldrs.md └── intro.md ├── examples └── custom-config │ ├── index.js │ └── muto.config.js ├── index.d.ts ├── jsconfig.json ├── package.json ├── repl.js ├── src ├── condition.js ├── index.d.ts ├── index.js ├── muto-parser.js ├── muto.pegjs ├── parse.js ├── query-builder-def.js └── where.js ├── test ├── .eslintrc.yml ├── __mocks__ │ └── cosmiconfig.js ├── condition.test.js ├── helpers │ └── condition-factory.js ├── index.test.js ├── parse-mock-config.test.js ├── parse.test.js ├── query-builder-def.test.js └── where.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": "4" 6 | } 7 | }] 8 | ], 9 | "plugins": [ 10 | ["transform-runtime", { 11 | "helpers": true, 12 | "polyfill": false, 13 | "regenerator": false 14 | }] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | browser 2 | docs 3 | lib 4 | src/muto-parser.js 5 | build/*noop.js 6 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: chatur 3 | rules: 4 | node/no-unsupported-features: 5 | - error 6 | # We use babel. So change the version instead of inheriting from 7 | # package.json#engines 8 | - version: 6 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | *.sh text eol=lf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # generated files 61 | lib 62 | 63 | # experiments 64 | experiment.js 65 | 66 | # generated docs 67 | browser/ 68 | docs/assets/ 69 | docs/index.html 70 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.js": [ 3 | "eslint --fix", 4 | "git add" 5 | ], 6 | "build/*.js": [ 7 | "eslint --fix", 8 | "git add" 9 | ], 10 | "test/**/*.js": [ 11 | "eslint --fix", 12 | "git add" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '8' 5 | - '7' 6 | - '6' 7 | - '4' 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | cache: yarn 14 | 15 | script: yarn run check 16 | 17 | after_success: 18 | - yarn global add coveralls travis-deploy-once 19 | - cat ./coverage/lcov.info | coveralls 20 | - travis-deploy-once "yarn global add semantic-release@12 && semantic-release" 21 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "dbaeumer.vscode-eslint", 7 | "eg2.vscode-npm-script", 8 | "dkundel.vscode-npm-source", 9 | "esbenp.prettier-vscode" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "eslint.enable": true, 4 | "eslint.autoFixOnSave": true, 5 | "files.eol": "\n", 6 | "vsicons.presets.angular": false, 7 | "editor.detectIndentation": true, 8 | "[json]": { 9 | "editor.tabSize": 2 10 | }, 11 | "prettier.printWidth": 80, 12 | "prettier.tabWidth": 4, 13 | "prettier.singleQuote": true 14 | } 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sudo.suhas@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for taking the time to contribute to elastic-muto! Your work is truly 4 | appreciated. 5 | 6 | Please follow this guide to make that PR the best that it can be! 7 | 8 | ## Guidelines 9 | 10 | * Write small commits with concise, descriptive messages. 11 | * Include tests for any new feature, and regression tests for any bug fix. 12 | * Write [es2015+ javascript][1]. 13 | * Try to keep the code style consistent. Follow existing patterns. 14 | * Modify or add to the README if your feature needs instructions on how to use it. 15 | 16 | ## Development 17 | 18 | Fork, then clone the repo: 19 | ``` 20 | git clone https://github.com/your-username/elastic-muto.git 21 | ``` 22 | 23 | Install dependencies using npm (install [node.js][2] first if necessary). 24 | ``` 25 | npm install 26 | ``` 27 | 28 | ### Write code 29 | 30 | Typically, your changes will go in the `src` directory (the `lib` directory 31 | contains transpiled babel code) and the `test` directory. 32 | 33 | No need to generate the built files, these will be added when a new version of 34 | elastic-muto is published to npm. 35 | 36 | ### Run tests 37 | 38 | This project uses eslint for javascript linting and ava for testing. Run 39 | linting using `npm run lint` and run tests using `npm test`. Or run both using: 40 | ``` 41 | npm run check 42 | ``` 43 | This should take care of formatting as well thanks to [eslint-plugin-prettier][3]. 44 | 45 | ### (Optional) Add yourself as a contributor 46 | 47 | Thanks for contributing! Go ahead and add yourself to the list of contributors 48 | in the npm package manifest `package.json`. 49 | 50 | ### Submit your PR 51 | 52 | This is the last step! Make sure your PR is aimed to merge with the `master` 53 | branch. 54 | 55 | You should also write a good PR message with information on why this feature or 56 | fix is necesary or a good idea. For features, be sure to include information on 57 | how to use the feature; and for bugs, information on how to reproduce the bug is 58 | helpful! 59 | 60 | ## Need help? 61 | 62 | If you have any questions about the feature or fix you want to make, or if you 63 | have doubts about the approach, or anything else you're not sure about, the best 64 | way to get in touch is to [open an issue][4]. We are happy to help out. 65 | 66 | [1]: https://babeljs.io/docs/learn-es2015/ 67 | [2]: https://nodejs.org/ 68 | [3]: https://github.com/not-an-aardvark/eslint-plugin-prettier 69 | [4]: https://github.com/booleanapp/elastic-muto/issues/new 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Boolean Inc. 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 | [![elastic-muto](assets/muto-logo.png)](https://muto.js.org) 2 | 3 | # elastic-muto 4 | 5 | [![Build Status](https://travis-ci.org/booleanapp/elastic-muto.svg?branch=master)](https://travis-ci.org/booleanapp/elastic-muto) 6 | [![Coverage Status](https://coveralls.io/repos/github/booleanapp/elastic-muto/badge.svg?branch=master)](https://coveralls.io/github/booleanapp/elastic-muto?branch=master) 7 | 8 | Easy expressive search queries for Elasticsearch with customisation! 9 | Build complicated elasticsearch queries without having to use the DSL. 10 | Expressions get compiled into native Elasticsearch queries, 11 | offering the same performance as if it had been hand coded. 12 | 13 | `elastic-muto` is built using [PEG.js](https://github.com/pegjs/pegjs). 14 | If you are curious about how the parsing works, check [this](http://dundalek.com/GrammKit/#https://raw.githubusercontent.com/booleanapp/elastic-muto/master/src/muto.pegjs) out. 15 | The parser was originally developed for parsing filter conditions for the [GET score API](https://www.booleanapp.com/docs/v1_score.html) of [Boolean](https://www.booleanapp.com/). 16 | 17 | **Check out the [API reference documentation](https://muto.js.org/docs).** 18 | 19 | _Note: The library includes TypeScript definitions for a superior development experience._ 20 | 21 | ## Elasticsearch compatibility 22 | `elastic-muto` can be used with elasticsearch v2.x and above. 23 | 24 | ## Install 25 | ``` 26 | npm install elastic-muto --save 27 | ``` 28 | 29 | ## Usage 30 | ```js 31 | // Import the library 32 | const muto = require('elastic-muto'); 33 | 34 | // muto.parse returns an elastic-builder BoolQuery object 35 | const qry = muto.parse('["elasticsearch"] == "awesome" and ["unicorn"] exists'); 36 | qry.toJSON(); 37 | { 38 | "bool": { 39 | "must": [ 40 | { 41 | "term": { "elasticsearch.keyword": "awesome" } 42 | }, 43 | { 44 | "exists": { "field": "unicorn" } 45 | } 46 | ] 47 | } 48 | } 49 | ``` 50 | 51 | Classes have also been provided for building the `where` expressions. Use whatever floats your boat :wink:. 52 | ```js 53 | const qry = muto.parse( 54 | muto.where(muto.cn('elasticsearch').eq('awesome')) 55 | .and(muto.cn('unicorn').exists()) 56 | ); 57 | ``` 58 | 59 | `elastic-muto` uses [debug](https://github.com/visionmedia/debug) with the namespace `elastic-muto`. 60 | To enable debug logs, refer [this](https://github.com/visionmedia/debug#wildcards). 61 | 62 | ## Where Conditions 63 | Where conditions can either be single(ex: `'["key"] == value'`) or multiple. 64 | Multiple conditions can be combined with `and`/`or`. 65 | 66 | Supported data types: 67 | 68 | |Data type|Values|Description| 69 | |---------|------|-----------| 70 | |String|`"unicorns"`, `"dancing monkeys"`|Strings are enclosed in double-quotes. Can contain space, special characters| 71 | |Numbers|`3`, `-9.5`, `"2.5"`|Numbers can be integers or floating point. Double quotes are also okay| 72 | |Date|`"2016-12-01"`, `"2011-10-10T14:48:00"`|Dates, enclosed within double quotes, must be in the [ISO-8601 format](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#ECMAScript_5_ISO-8601_format_support)| 73 | |Boolean|`true`, `false`, `"true"`|Boolean can be `true` or `false`. Double quotes are also okay| 74 | 75 | Condition types: 76 | 77 | |Condition type|Operator|Data types|Example| 78 | |--------------|--------|----------|-------| 79 | |Equals|`==`|String, Number, Date|`["elasticsearch"] == "awesome"`, `["answer"] == 42`, `["launch_date"] == "2017-06-01"`| 80 | |Not Equals|`!=`|String, Number|`["joke_type"] != "knock-knock"`, `["downloads"] != 0`| 81 | |Contains|`contains`|String|`["potion"] contains "fluxweed"`| 82 | |Not Contains|`!contains`|String|`["anime"] !contains "fillers"`| 83 | |Less than|`<`|Number, Date|`["num_idiots"] < 0`, `["birthday"] < "1990-12-01"`| 84 | |Less than or equal to|`<=`|Number, Date|`["issues"] <= 0`, `["speed"] <= 299792458`| 85 | |Greater than|`>`|Number, Date|`["contributos"] > 1`, `["fictional_date"] > "2049-01-01"`| 86 | |Greater than or equal to|`>=`|Number, Date|`["pull_requests"] >= 1`, `["unfreeze_date"] >= "3000-01-01"`| 87 | |Boolean|`is`|Boolean|`["prophecy"] is true`| 88 | |Property Exists|`exists`|Any data type|`["unicorn"] exists`| 89 | |Property Missing|`missing`|Any data type|`["clue"] missing`| 90 | 91 | Both `and`, `or` cannot be used in the same level, because if you do, the desired query is not clear. 92 | ```js 93 | it('throws error if both and, or are called', () => { 94 | expect( 95 | () => muto.where() 96 | .and(muto.cn('anime').notContains('fillers')) 97 | .or(muto.cn('elasticsearch').eq('awesome')) 98 | ).toThrowError('Illegal operation! Join types cannot be mixed!'); 99 | }); 100 | ``` 101 | Expressions can be nested using paranthesis. This allows to use both `and`, `or`: 102 | ```js 103 | const qry = muto.parse( 104 | '["elasticsearch"] == "awesome" and ["language"] == "node.js"' + 105 | 'and (["library"] == "elastic-muto" or ["library"] == "elastic-builder")' 106 | ) 107 | ``` 108 | 109 | ## Elasticsearch Mapping 110 | `elastic-muto` makes some assumptions for the mapping of data types. Following are the recommended mappings: 111 | 112 | * String mapping: 113 | ```json 114 | { 115 | "type": "text", 116 | "fields": { 117 | "keyword": { 118 | "type": "keyword", 119 | "ignore_above": 256 120 | } 121 | } 122 | } 123 | ``` 124 | This is the default since [elasticsearch v5.x](https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking_50_mapping_changes.html#_default_string_mappings) 125 | 126 | * Date mapping 127 | ```json 128 | { 129 | "type": "date", 130 | "format": "strict_date_time_no_millis||strict_date_optional_time||epoch_millis" 131 | } 132 | ``` 133 | 134 | * Number mapping 135 | ```json 136 | { "type" : "double" } 137 | ``` 138 | 139 | * Boolean mapping 140 | ```json 141 | { "type": "boolean" } 142 | ``` 143 | 144 | If your mapping doesn't match, you might need to tweak the elasticsearch query generated with customisation. 145 | 146 | ## Customisation 147 | Elasticsearch queries generated by `elastic-muto` can be customised. 148 | Read more [here](docs/custom-conf.md). Check out a contrived example [here](examples/custom-config). 149 | 150 | ## REPL 151 | Try it out on the command line using the node REPL: 152 | 153 | ``` 154 | # Start the repl 155 | node ./node_modules/elastic-muto/repl.js 156 | # Use the library loaded in context as `muto` 157 | elastic-muto > muto.prettyPrint('["elasticsearch"] == "awesome" and ["unicorn"] exists') 158 | ``` 159 | 160 | ## API Reference 161 | API reference can be accessed here - http://muto.js.org/docs. 162 | 163 | API documentation was generated using [documentation.js](https://github.com/documentationjs/documentation). 164 | It is being hosted with help from this awesome project - https://github.com/js-org/dns.js.org 165 | 166 | ## Tests 167 | Run unit tests: 168 | ``` 169 | npm test 170 | ``` 171 | The parser is tested extensively with upto 5 levels of nested queries! 172 | 173 | ## Related 174 | - [elastic-builder](/sudo-suhas/elastic-builder) - An elasticsearch query body builder for node.js 175 | - [FiltrES.js](/abehaskins/FiltrES.js) - A simple, safe, ElasticSearch Query compiler 176 | 177 | ## License 178 | MIT 179 | -------------------------------------------------------------------------------- /assets/muto-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booleanapp/elastic-muto/cba4a00ecbf3b54dc1a9604e3e53f1e612dbd63a/assets/muto-logo.png -------------------------------------------------------------------------------- /build/build-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | 4 | const path = require('path'); 5 | 6 | const PEG = require('pegjs'); 7 | 8 | const pegjsDefPath = path.resolve(__dirname, '../src/muto.pegjs'); 9 | 10 | const pegjsOutputPath = path.resolve(__dirname, '../src/muto-parser.js'); 11 | 12 | const options = { 13 | dependencies: { 14 | BoolQuery: 'elastic-builder/lib/queries/compound-queries/bool-query' 15 | }, 16 | cache: true, 17 | optimize: 'speed', 18 | format: 'commonjs', 19 | output: 'source', 20 | trace: false, 21 | plugins: [] 22 | }; 23 | 24 | fs.writeFileSync( 25 | pegjsOutputPath, 26 | PEG.generate(fs.readFileSync(pegjsDefPath, 'utf8'), options) 27 | ); 28 | -------------------------------------------------------------------------------- /build/cosmiconfig-noop.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { return { load: function() { return null } }; }; 2 | -------------------------------------------------------------------------------- /build/noop.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { return new Function(); }; 2 | -------------------------------------------------------------------------------- /build/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const webpack = require('webpack'); 6 | const WebpackStrip = require('strip-loader'); 7 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 8 | 9 | module.exports = { 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.js$/, 14 | loader: WebpackStrip.loader('debug') 15 | } 16 | ] 17 | }, 18 | output: { 19 | library: 'muto', 20 | libraryTarget: 'umd' 21 | }, 22 | plugins: [ 23 | new webpack.NormalModuleReplacementPlugin( 24 | /debug/, 25 | path.join(__dirname, 'noop.js') 26 | ), 27 | 28 | new webpack.NormalModuleReplacementPlugin( 29 | /cosmiconfig/, 30 | path.join(__dirname, 'cosmiconfig-noop.js') 31 | ), 32 | new UglifyJSPlugin({ 33 | sourceMap: false, 34 | uglifyOptions: { 35 | beautify: false, 36 | mangle: { 37 | toplevel: true, 38 | keep_fnames: false 39 | }, 40 | compressor: { 41 | warnings: false, 42 | conditionals: true, 43 | unused: true, 44 | comparisons: true, 45 | sequences: true, 46 | dead_code: true, 47 | evaluate: true, 48 | if_return: true, 49 | join_vars: true, 50 | negate_iife: false 51 | }, 52 | comments: false 53 | } 54 | }) 55 | ] 56 | }; 57 | -------------------------------------------------------------------------------- /deploy-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | # See http://tldp.org/LDP/abs/html/options.html 3 | # -x -> Print each command to stdout before executing it, expand commands 4 | # -e -> Abort script at first error, when a command exits with non-zero status 5 | # (except in until or while loops, if-tests, list constructs) 6 | 7 | if ! hash gh-pages 2> /dev/null; then 8 | yarn global add gh-pages 9 | fi 10 | 11 | gh-pages --add \ 12 | --dist . \ 13 | --src "{browser/*,docs/*}" \ 14 | --repo "https://$GH_TOKEN@github.com/booleanapp/elastic-muto.git" \ 15 | --message "docs: Build docs for $(npm run -s print-version)" 16 | 17 | -------------------------------------------------------------------------------- /docs/custom-conf.md: -------------------------------------------------------------------------------- 1 | ### Custom configuration 2 | The queries generated by `elastic-muto` can be completely customised. 3 | [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) is used to load the config. 4 | A js file, with the file name `muto.config.js`, will be loaded and used if present. 5 | 6 | The file should export a plain object with a function which returns an `elastic-builder` [`BoolQuery`](https://elastic-builder.js.org/docs/#boolquery) object. 7 | You can define(and override) any of the functions defined in [`src/query-builder-def.js`](https://github.com/booleanapp/elastic-muto/blob/master/src/query-builder-def.js) 8 | 9 | Example: 10 | ```js 11 | 'use strict'; 12 | 13 | module.exports = { 14 | // Condition builder for property exists 15 | exists(key) { 16 | return bob.existsQuery(`shiny_${key}`); 17 | }, 18 | }; 19 | ``` 20 | 21 | You can also customise the construction of Property Keys. Check out [this example](https://github.com/booleanapp/elastic-muto/blob/master/examples/custom-config) 22 | which defines a custom config. 23 | -------------------------------------------------------------------------------- /docs/documentation.yml: -------------------------------------------------------------------------------- 1 | toc: 2 | - name: elastic-muto 3 | file: intro.md 4 | - parse 5 | # - SyntaxError # Can't dictate ordering cause of documentationjs/documentation#768 6 | - name: Configuration 7 | file: custom-conf.md 8 | - name: Expr Builders 9 | file: expr-bldrs.md 10 | - Where 11 | - Condition 12 | -------------------------------------------------------------------------------- /docs/expr-bldrs.md: -------------------------------------------------------------------------------- 1 | These classes are purely optional helpers for building `Where` expressions. 2 | `muto.parse` can handle strings just fine. 3 | 4 | There are two ways to use the classes for constructing queries: 5 | 6 | ```js 7 | // Import the library 8 | const muto = require('elastic-muto'); 9 | 10 | // Use `new` keyword for constructor instances of class 11 | const cn = new muto.Condition('elasticsearch').eq('awesome'); 12 | const expr = new muto.Where(cn).and('["unicorn"] exists'); 13 | 14 | // Or use helper methods which construct the object without need for the `new` keyword 15 | const cn = muto.cn('elasticsearch').eq('awesome'); // or muto.condition 16 | const expr = muto.where(cn).and('["unicorn"] exists') 17 | 18 | const qry = muto.parse(expr); 19 | qry.toJSON(); 20 | { 21 | "bool": { 22 | "must": [ 23 | { 24 | "term": { "elasticsearch.keyword": "awesome" } 25 | }, 26 | { 27 | "exists": { "field": "unicorn" } 28 | } 29 | ] 30 | } 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | https://github.com/booleanapp/elastic-muto 2 | 3 | `elastic-muto` is a library for easliy building elasticsearch queries with simple expressive expressions. 4 | It also allows full control over query generation if you want different behavior. 5 | 6 | The complete library documentation is present here. 7 | 8 | 9 | ```js 10 | // Import the library 11 | const muto = require('elastic-muto'); 12 | 13 | const qry = muto.parse('["elasticsearch"] == "awesome" and ["unicorn"] exists'); 14 | qry.toJSON(); 15 | { 16 | "bool": { 17 | "must": [ 18 | { 19 | "term": { "elasticsearch.keyword": "awesome" } 20 | }, 21 | { 22 | "exists": { "field": "unicorn" } 23 | } 24 | ] 25 | } 26 | } 27 | ``` 28 | 29 | **Demo** - https://muto.js.org/ 30 | 31 | The parser was originally developed for parsing filter conditions for the [GET score endpoint](https://www.booleanapp.com/docs/v1_score.html) of [Boolean](https://www.booleanapp.com/). 32 | -------------------------------------------------------------------------------- /examples/custom-config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const muto = require('../../'); 6 | 7 | const qry = muto.parse('["abe"] exists'); 8 | 9 | assert.deepEqual(qry.toJSON(), { 10 | bool: { 11 | should: [ 12 | { 13 | term: { $abe: 'dancing_monkey' } 14 | }, 15 | { 16 | exists: { field: '$abe' } 17 | } 18 | ] 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /examples/custom-config/muto.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bob = require('elastic-builder'); 4 | 5 | module.exports = { 6 | // Condition builder for property exists 7 | exists(key) { 8 | // Either the property should exist 9 | // or it should be equal to `dancing_monkey` 10 | // Implicit `minimum_should_match = 1` 11 | return bob 12 | .boolQuery() 13 | .should(bob.termQuery(key, 'dancing_monkey')) 14 | .should(bob.existsQuery(key)); 15 | }, 16 | // Function for building property key 17 | // We can add custom logic for manipulating the field name here 18 | // Append $ to field names 19 | propertyKey: chars => '$' + chars.join('') // eslint-disable-line prefer-template 20 | }; 21 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for elastic-muto 2 | // Project: https://muto.js.org 3 | // Definitions by: Suhas Karanth 4 | 5 | import { BoolQuery } from 'elastic-builder'; 6 | 7 | /** 8 | * Parses given expression and generates an `elastic-builder` query object. 9 | * 10 | * @param {string|Where|Condition} expr 11 | * @param {Array} [notAnalysedFields] 12 | * parsing the expression 13 | * @throws {SyntaxError} If expression is an invalid where condition 14 | */ 15 | export function parse(expr: string | Where | Condition, notAnalysedFields?: string[]): BoolQuery; 16 | 17 | interface Location { 18 | line: number; 19 | column: number; 20 | offset: number; 21 | } 22 | 23 | interface LocationRange { 24 | start: Location, 25 | end: Location 26 | } 27 | 28 | interface ExpectedItem { 29 | type: string; 30 | value?: string; 31 | description: string; 32 | } 33 | 34 | export interface SyntaxError extends Error { 35 | name: string; 36 | message: string; 37 | location: LocationRange; 38 | found?: any; 39 | expected?: ExpectedItem[]; 40 | stack?: any; 41 | } 42 | 43 | /** 44 | * Class for building Property Condition to be used with `Where`. 45 | * 46 | * @param {string} [prop] Name of the property key to crate condition against 47 | * @param {string} [operator] Operator for the condition. One of `is`, `eq`, 48 | * `ne`, `lt`, `lte`, `gt`, `gte`, `exists`, `missing`, `contain`, `notcontain` 49 | * @param {*} [value] Value for the property condition 50 | */ 51 | export class Condition { 52 | constructor(prop?: string, operator?: string, value?: any); 53 | 54 | /** 55 | * Sets the property name for condition. 56 | * 57 | * @param {string} prop Name of the property key to crate condition 58 | */ 59 | prop(prop: string): this; 60 | 61 | /** 62 | * Sets the condition type to boolean with given parameter. 63 | * 64 | * @param {boolean} trueOrFalse `true` or `false` 65 | */ 66 | is(trueOrFalse: boolean): this; 67 | 68 | /** 69 | * Sets the type of condition as equality with given value. 70 | * 71 | * @param {*} value A valid string/number/date to check equality ag 72 | */ 73 | eq(value: any): this; 74 | 75 | /** 76 | * Sets the type of condition as not equal to given value. 77 | * 78 | * @param {*} value A valid string/number to check inequality again 79 | */ 80 | ne(value: any): this; 81 | 82 | /** 83 | * Sets the type of condition as less than given value. 84 | * 85 | * @param {*} value A valid string/number/date. 86 | */ 87 | lt(value: any): this; 88 | 89 | /** 90 | * Sets the type of condition as less than or equal to given value. 91 | * 92 | * @param {*} value A valid string/number/date. 93 | */ 94 | lte(value: any): this; 95 | 96 | /** 97 | * Sets the type of condition as greater than given value 98 | * 99 | * @param {*} value A valid string/number/date. 100 | */ 101 | gt(value: any): this; 102 | 103 | /** 104 | * Sets the type of condition as greater than or equal to given value 105 | * 106 | * @param {*} value A valid string/number/date. 107 | */ 108 | gte(value: any): this; 109 | 110 | /** 111 | * Sets the type of condition to check value exists. 112 | * 113 | */ 114 | exists(): this; 115 | 116 | /** 117 | * Sets the type of condition to check value is missing. 118 | * 119 | */ 120 | missing(): this; 121 | 122 | /** 123 | * Sets the type of condition to check property contains given value. 124 | * 125 | * @param {*} value A valid string 126 | */ 127 | contains(value: any): this; 128 | 129 | /** 130 | * Sets the type of condition to check property does not contain given value. 131 | * 132 | * @param {*} value A valid string 133 | */ 134 | notContains(value: any): this; 135 | 136 | /** 137 | * Builds and returns muto syntax for Property Condition 138 | * 139 | * Property Condition 140 | */ 141 | build(): string; 142 | } 143 | 144 | /** 145 | * Returns an instance of `Condition` for building Property Condition to be used with `Where`. 146 | * 147 | * @param {string} [prop] Name of the property key to crate condition against 148 | * @param {string} [operator] Operator for the condition. One of `is`, `eq`, 149 | * `ne`, `lt`, `lte`, `gt`, `gte`, `exists`, `missing`, `contain`, `notcontain` 150 | * @param {*} [value] Value for the property condition 151 | */ 152 | export function cn(prop?: string, operator?: string, value?: any): Condition; 153 | 154 | /** 155 | * Returns an instance of `Condition` for building Property Condition to be used with `Where`. 156 | * 157 | * @param {string} [prop] Name of the property key to crate condition against 158 | * @param {string} [operator] Operator for the condition. One of `is`, `eq`, 159 | * `ne`, `lt`, `lte`, `gt`, `gte`, `exists`, `missing`, `contain`, `notcontain` 160 | * @param {*} [value] Value for the property condition 161 | */ 162 | export function condition(prop?: string, operator?: string, value?: any): Condition; 163 | 164 | /** 165 | * Class for building `Where` expressions. 166 | * 167 | * @param {Condition|Where|string} [condition] 168 | */ 169 | export class Where { 170 | constructor(condition?: Condition | Where | string); 171 | 172 | /** 173 | * Adds an `and` condition. The condition can be instance of `Condition`, 174 | * `Where`(nested expr) or just a string. 175 | * 176 | * @param {Condition|Where|string} condition The condition to b 177 | * @throws {Error} If `and`, `or` are called on the same instance of `Where` 178 | */ 179 | and(condition: Condition | Where | string): this; 180 | 181 | /** 182 | * Adds an `or` condition. The condition can be instance of `Condition`, 183 | * `Where`(nested expr) or just a string. 184 | * 185 | * @param {Condition|Where|string} condition The condition to b 186 | * @throws {Error} If `and`, `or` are called on the same instance of `Where` 187 | */ 188 | or(condition: Condition | Where | string): this; 189 | 190 | /** 191 | * Build and return muto syntax for Where Expression 192 | * Where Expression 193 | */ 194 | build(): string; 195 | } 196 | 197 | /** 198 | * Returns an instance of `Where` for building expressions. 199 | * 200 | * @param {Condition|Where|string} [condition] 201 | */ 202 | export function where(condition?: Condition | Where | string): Where; 203 | 204 | /** 205 | * Utility function to parse and pretty print objects to console. 206 | * To be used in development. 207 | * 208 | * @param {*} obj 209 | */ 210 | export function prettyPrint(obj: any): void; 211 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=759670 3 | // for the documentation about the jsconfig.json format 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "allowSyntheticDefaultImports": true, 8 | "sourceMap": true 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "bower_components", 13 | "jspm_packages", 14 | "tmp", 15 | "temp" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elastic-muto", 3 | "version": "0.0.0-development", 4 | "description": "Easy expressive search queries for Elasticsearch", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib/", 8 | "src/", 9 | "repl.js", 10 | "index.d.ts" 11 | ], 12 | "scripts": { 13 | "build:parser": "node build/build-parser.js", 14 | "build:babel": "cross-env BABEL_ENV=production babel src --out-dir lib", 15 | "build:umd": "webpack lib --config build/webpack.config.js browser/elastic-muto.min.js", 16 | "build:docs": "documentation build src/index.js --github -o docs -f html -c ./docs/documentation.yml --name elastic-muto", 17 | "build": "npm run build:parser && npm run build:babel && npm run build:umd && npm run build:docs", 18 | "lint": "eslint src test build", 19 | "lint:fix": "npm run lint -- --fix", 20 | "lint-staged": "lint-staged", 21 | "test": "jest", 22 | "check": "npm run lint && npm test -- --coverage", 23 | "report": "npm test -- --coverage --coverageReporters=html --coverageReporters=text", 24 | "print-version": "cross-env-shell echo v$npm_package_version", 25 | "prepublishOnly": "npm run -s build", 26 | "postpublish": "bash deploy-docs.sh" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/booleanapp/elastic-muto.git" 31 | }, 32 | "keywords": [ 33 | "elasticsearch", 34 | "search", 35 | "elasticjs", 36 | "elastic search", 37 | "elastic-builder", 38 | "elasticsearch query", 39 | "elasticsearch query DSL", 40 | "elasticsearch query builder", 41 | "pegjs", 42 | "peg.js" 43 | ], 44 | "author": "Suhas Karanth ", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/booleanapp/elastic-muto/issues" 48 | }, 49 | "homepage": "https://muto.js.org", 50 | "pre-commit": "lint-staged", 51 | "jest": { 52 | "testEnvironment": "node", 53 | "collectCoverageFrom": [ 54 | "src/**/*.js" 55 | ] 56 | }, 57 | "dependencies": { 58 | "babel-runtime": "^6.26.0", 59 | "cosmiconfig": "^3.1.0", 60 | "debug": "^3.1.0", 61 | "elastic-builder": "^1.1.3", 62 | "lodash.hasin": "^4.5.2", 63 | "lodash.isempty": "^4.4.0", 64 | "lodash.isfunction": "^3.0.8", 65 | "lodash.isnil": "^4.0.0", 66 | "lodash.isstring": "^4.0.1" 67 | }, 68 | "devDependencies": { 69 | "babel-cli": "^6.26.0", 70 | "babel-core": "^6.26.0", 71 | "babel-jest": "^21.2.0", 72 | "babel-plugin-transform-runtime": "^6.23.0", 73 | "babel-preset-env": "^1.6.0", 74 | "babel-register": "^6.26.0", 75 | "cross-env": "^5.0.5", 76 | "documentation": "^5.3.2", 77 | "eslint": "^4.14.0", 78 | "eslint-config-chatur": "^2.0.0", 79 | "eslint-config-prettier": "^2.9.0", 80 | "eslint-plugin-jest": "^21.2.0", 81 | "eslint-plugin-prettier": "^2.4.0", 82 | "jest": "^21.2.1", 83 | "js-combinatorics": "^0.5.2", 84 | "lint-staged": "^4.2.3", 85 | "pegjs": "^0.10.0", 86 | "pre-commit": "^1.2.2", 87 | "prettier": "^1.9.2", 88 | "strip-loader": "^0.1.2", 89 | "uglifyjs-webpack-plugin": "^1.0.0-beta.1", 90 | "webpack": "^3.6.0" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /repl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const repl = require('repl'); 4 | 5 | const muto = require('./'); 6 | 7 | repl.start('elastic-muto > ').context.muto = muto; 8 | -------------------------------------------------------------------------------- /src/condition.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isNil = require('lodash.isnil'); 4 | const isFunction = require('lodash.isfunction'); 5 | const hasIn = require('lodash.hasin'); 6 | 7 | /** 8 | * Class for building Property Condition to be used with `Where`. 9 | * 10 | * @example 11 | * const condition = muto.cn('psngr_cnt', 'gt', 81); 12 | * 13 | * condition.build() 14 | * '["psngr_cnt"]" > 81' 15 | * 16 | * @param {string} [prop] Name of the property key to crate condition against 17 | * @param {string} [operator] Operator for the condition. One of `is`, `eq`, 18 | * `ne`, `lt`, `lte`, `gt`, `gte`, `exists`, `missing`, `contain`, `notcontain` 19 | * @param {*} [value] Value for the property condition 20 | */ 21 | class Condition { 22 | // eslint-disable-next-line require-jsdoc 23 | constructor(prop, operator, value) { 24 | if (!isNil(prop)) this._prop = prop; 25 | 26 | if (!isNil(operator)) { 27 | if ( 28 | operator !== 'prop' && 29 | operator !== 'build' && 30 | hasIn(this, operator) && 31 | isFunction(this[operator]) 32 | ) { 33 | this[operator](value); 34 | } else throw new Error(`Invalid operator '${operator}'!`); 35 | } 36 | } 37 | 38 | /** 39 | * Sets the property name for condition. 40 | * 41 | * @param {string} prop Name of the property key to crate condition against 42 | * @returns {Condition} returns `this` so that calls can be chained 43 | */ 44 | prop(prop) { 45 | this._prop = prop; 46 | return this; 47 | } 48 | 49 | /** 50 | * Sets the condition type to boolean with given parameter. 51 | * 52 | * @param {boolean} trueOrFalse `true` or `false` 53 | * @returns {Condition} returns `this` so that calls can be chained 54 | */ 55 | is(trueOrFalse) { 56 | this._operator = 'is'; 57 | this._value = trueOrFalse; 58 | return this; 59 | } 60 | 61 | /** 62 | * Sets the type of condition as equality with given value. 63 | * 64 | * @param {*} value A valid string/number/date to check equality against 65 | * @returns {Condition} returns `this` so that calls can be chained 66 | */ 67 | eq(value) { 68 | this._operator = '=='; 69 | this._value = value; 70 | return this; 71 | } 72 | 73 | /** 74 | * Sets the type of condition as not equal to given value. 75 | * 76 | * @param {*} value A valid string/number to check inequality against 77 | * @returns {Condition} returns `this` so that calls can be chained 78 | */ 79 | ne(value) { 80 | this._operator = '!='; 81 | this._value = value; 82 | return this; 83 | } 84 | 85 | /** 86 | * Sets the type of condition as less than given value. 87 | * 88 | * @param {*} value A valid string/number/date. 89 | * @returns {Condition} returns `this` so that calls can be chained 90 | */ 91 | lt(value) { 92 | this._operator = '<'; 93 | this._value = value; 94 | return this; 95 | } 96 | 97 | /** 98 | * Sets the type of condition as less than or equal to given value. 99 | * 100 | * @param {*} value A valid string/number/date. 101 | * @returns {Condition} returns `this` so that calls can be chained 102 | */ 103 | lte(value) { 104 | this._operator = '<='; 105 | this._value = value; 106 | return this; 107 | } 108 | 109 | /** 110 | * Sets the type of condition as greater than given value 111 | * 112 | * @param {*} value A valid string/number/date. 113 | * @returns {Condition} returns `this` so that calls can be chained 114 | */ 115 | gt(value) { 116 | this._operator = '>'; 117 | this._value = value; 118 | return this; 119 | } 120 | 121 | /** 122 | * Sets the type of condition as greater than or equal to given value 123 | * 124 | * @param {*} value A valid string/number/date. 125 | * @returns {Condition} returns `this` so that calls can be chained 126 | */ 127 | gte(value) { 128 | this._operator = '>='; 129 | this._value = value; 130 | return this; 131 | } 132 | 133 | /** 134 | * Sets the type of condition to check value exists. 135 | * 136 | * @returns {Condition} returns `this` so that calls can be chained 137 | */ 138 | exists() { 139 | this._operator = 'exists'; 140 | return this; 141 | } 142 | 143 | /** 144 | * Sets the type of condition to check value is missing. 145 | * 146 | * @returns {Condition} returns `this` so that calls can be chained 147 | */ 148 | missing() { 149 | this._operator = 'missing'; 150 | return this; 151 | } 152 | 153 | /** 154 | * Sets the type of condition to check property contains given value. 155 | * 156 | * @param {*} value A valid string 157 | * @returns {Condition} returns `this` so that calls can be chained 158 | */ 159 | contains(value) { 160 | this._operator = 'contains'; 161 | this._value = value; 162 | return this; 163 | } 164 | 165 | /** 166 | * 167 | * @private 168 | * @param {*} value A valid string 169 | * @returns {Condition} returns `this` so that calls can be chained 170 | */ 171 | notcontains(value) { 172 | return this.notContains(value); 173 | } 174 | 175 | /** 176 | * Sets the type of condition to check property does not contain given value. 177 | * 178 | * @param {*} value A valid string 179 | * @returns {Condition} returns `this` so that calls can be chained 180 | */ 181 | notContains(value) { 182 | this._operator = '!contains'; 183 | this._value = value; 184 | return this; 185 | } 186 | 187 | /** 188 | * Builds and returns muto syntax for Property Condition 189 | * 190 | * @returns {string} returns a string which maps to the muto syntax for 191 | * Property Condition 192 | */ 193 | build() { 194 | // Not gonna throw error here if expected members are not populated 195 | // For exists and missing, thisn._value _should_ be undefined 196 | // We just check if the operator is one of the 2. 197 | if (this._operator === 'exists' || this._operator === 'missing') { 198 | return `["${this._prop}"] ${this._operator}`; 199 | } 200 | return `["${this._prop}"] ${this._operator} ${JSON.stringify( 201 | this._value 202 | )}`; 203 | } 204 | 205 | /** 206 | * Hotwire to return `this.build()` 207 | * 208 | * @override 209 | * @returns {string} 210 | */ 211 | toString() { 212 | return this.build(); 213 | } 214 | 215 | /** 216 | * Hotwire to return `this.build()` 217 | * 218 | * @override 219 | * @returns {string} 220 | */ 221 | toJSON() { 222 | return this.build(); 223 | } 224 | } 225 | 226 | module.exports = Condition; 227 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../' 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mutoParser = require('./muto-parser'); 4 | const parse = require('./parse'); 5 | const Where = require('./where'); 6 | const Condition = require('./condition'); 7 | 8 | exports.parse = parse; 9 | 10 | /** 11 | * Syntax error thrown by PEG.js on trying to parse an invalid expression 12 | * 13 | * @extends Error 14 | */ 15 | exports.SyntaxError = mutoParser.SyntaxError; 16 | 17 | exports.Where = Where; 18 | exports.where = condition => new Where(condition); 19 | 20 | exports.Condition = Condition; 21 | exports.condition = exports.cn = (...args) => new Condition(...args); 22 | 23 | exports.prettyPrint = function prettyPrint(expr) { 24 | console.log(JSON.stringify(parse(expr), null, 2)); 25 | }; 26 | -------------------------------------------------------------------------------- /src/muto.pegjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Core PEG 3 | */ 4 | ExprWrapper "Expression Wrapper" 5 | = expr:Expression { 6 | return expr instanceof BoolQuery 7 | ? expr 8 | : new BoolQuery().must(expr) 9 | } 10 | 11 | Expression "Where Expression" 12 | = '(' head:Expression tail:(And expr:Expression { return expr; })+ ')' { 13 | var conditions = tail; 14 | conditions.unshift(head); 15 | return new BoolQuery().must(conditions); 16 | } 17 | / '(' head:Expression tail:(Or expr:Expression { return expr; })+ ')' { 18 | var conditions = tail; 19 | conditions.unshift(head); 20 | return new BoolQuery().should(conditions); 21 | } 22 | / '(' expr:Expression ')' { return expr; } 23 | / head:PropertyCondition 24 | torso:(And cond:PropertyCondition { return cond; })+ 25 | tail:(And expr:Expression { return expr; })* { 26 | var conditions = torso.concat(tail); 27 | conditions.unshift(head); 28 | return new BoolQuery().must(conditions); 29 | } 30 | / head:PropertyCondition 31 | torso:(And cond:PropertyCondition { return cond; })* 32 | tail:(And expr:Expression { return expr; })+ { 33 | var conditions = torso.concat(tail); 34 | conditions.unshift(head); 35 | return new BoolQuery().must(conditions); 36 | } 37 | / head:PropertyCondition 38 | torso:(Or cond:PropertyCondition { return cond; })+ 39 | tail:(Or expr:Expression { return expr; })* { 40 | var conditions = torso.concat(tail); 41 | conditions.unshift(head); 42 | return new BoolQuery().should(conditions); 43 | } 44 | / head:PropertyCondition 45 | torso:(Or cond:PropertyCondition { return cond; })* 46 | tail:(Or expr:Expression { return expr; })+ { 47 | var conditions = torso.concat(tail); 48 | conditions.unshift(head); 49 | return new BoolQuery().should(conditions); 50 | } 51 | / PropertyCondition 52 | 53 | PropertyCondition "Property Condition" 54 | = _ '(' _ cond:PropertyCondition _ ')' _ { return cond; } 55 | / NumLtCondition 56 | / NumGtCondition 57 | / NumLteCondition 58 | / NumGteCondition 59 | / NumEqCondition 60 | / NumNeCondition 61 | / DateLtCondition 62 | / DateGtCondition 63 | / DateLteCondition 64 | / DateGteCondition 65 | / DateEqCondition 66 | / StrContainsCondition 67 | / StrNotContainsCondition 68 | / StrEqCondition 69 | / StrNeCondition 70 | / ExistsCondition 71 | / MissingCondition 72 | / BooleanCondition 73 | 74 | /* 75 | * Numeric property conditions 76 | */ 77 | 78 | NumLteCondition "Number property less than or equal to condition" 79 | = key:PropertyKey LteOperator value:NumericValue 80 | { return options.numLte(key, value); } 81 | 82 | NumGteCondition "Number property greater than or equal to condition" 83 | = key:PropertyKey GteOperator value:NumericValue 84 | { return options.numGte(key, value); } 85 | 86 | NumLtCondition "Number property less than condition" 87 | = key:PropertyKey LtOperator value:NumericValue 88 | { return options.numLt(key, value); } 89 | 90 | NumGtCondition "Number property greater than condition" 91 | = key:PropertyKey GtOperator value:NumericValue 92 | { return options.numGt(key, value); } 93 | 94 | NumEqCondition "Number property equality condition" 95 | = key:PropertyKey EqOperator value:NumericValue 96 | { return options.numEq(key, value); } 97 | 98 | NumNeCondition "Number property inequality condition" 99 | = key:PropertyKey NeOperator value:NumericValue 100 | { return options.numNe(key, value); } 101 | 102 | /* 103 | * Date property condition 104 | */ 105 | 106 | DateLteCondition "Date property less than or equal to condition" 107 | = key:PropertyKey LteOperator value:DateValue 108 | { return options.dateLte(key, value); } 109 | 110 | DateGteCondition "Date property greater than or equal to condition" 111 | = key:PropertyKey GteOperator value:DateValue 112 | { return options.dateGte(key, value); } 113 | 114 | DateLtCondition "Date property less than condition" 115 | = key:PropertyKey LtOperator value:DateValue 116 | { return options.dateLt(key, value); } 117 | 118 | DateGtCondition "Date property greater than condition" 119 | = key:PropertyKey GtOperator value:DateValue 120 | { return options.dateGt(key, value); } 121 | 122 | DateEqCondition "Date property equality condition" 123 | = key:PropertyKey EqOperator value:DateValue 124 | { return options.dateEq(key, value); } 125 | 126 | /* 127 | * String property conditions 128 | */ 129 | StrContainsCondition "String property contains condition" 130 | = key:PropertyKey ContainsOperator value:StringValue 131 | { return options.strContains(key, value); } 132 | 133 | StrNotContainsCondition "String property does not contain condition" 134 | = key:PropertyKey NotContainsOperator value:StringValue 135 | { return options.strNotContains(key, value); } 136 | 137 | StrEqCondition "String property equality condition" 138 | = key:PropertyKey EqOperator value:StringValue 139 | { return options.strEq(key, value, options.notAnalysedFields); } 140 | 141 | StrNeCondition "String property inequality condition" 142 | = key:PropertyKey NeOperator value:StringValue 143 | { return options.strNe(key, value, options.notAnalysedFields); } 144 | 145 | ExistsCondition "Property Exists" 146 | = key:PropertyKey ExistsOperator 147 | { return options.exists(key); } 148 | 149 | MissingCondition "Property does not exist" 150 | = key:PropertyKey MissingOperator 151 | { return options.missing(key); } 152 | 153 | /* 154 | * Boolean property condition 155 | */ 156 | BooleanCondition "Boolean condition" 157 | = key:PropertyKey BooleanOperator value:BooleanValue 158 | { return options.bool(key, value); } 159 | 160 | /* 161 | * Property Keys, operators and property values 162 | */ 163 | PropertyKey "Property key" 164 | = begin_property chars:char+ end_property 165 | { return options.propertyKey(chars) } 166 | 167 | EqOperator "Equal operator" 168 | = _ "==" _ 169 | 170 | NeOperator "Not equal operator" 171 | = _ "!=" _ 172 | 173 | LtOperator "Less than operator" 174 | = _ "<" _ 175 | 176 | GtOperator "Greater than operator" 177 | = _ ">" _ 178 | 179 | LteOperator "Less than or equal to operator" 180 | = _ "<=" _ 181 | 182 | GteOperator "Greater than or equal to operator" 183 | = _ ">=" _ 184 | 185 | ContainsOperator "Contains operator" 186 | = _ "contains"i _ 187 | 188 | NotContainsOperator "Contains operator" 189 | = _ "!contains"i _ 190 | 191 | ExistsOperator "Exists operator" 192 | = _ "exists"i _ 193 | 194 | MissingOperator "Missing operator" 195 | = _ "missing"i _ 196 | 197 | BooleanOperator "Boolean(is) operator" 198 | = _ "is"i _ 199 | 200 | NumericValue "Numeric Value" 201 | = quotation_mark val:NumericValue quotation_mark { return val; } 202 | / minus? int frac? exp? { return parseFloat(text()); } 203 | 204 | DateValue "Date Value" 205 | = quotation_mark val:DateValue quotation_mark { return val; } 206 | / _ iso_date_time _ { return new Date(text().trim()); } 207 | 208 | StringValue "String value" 209 | = quotation_mark chars:char* quotation_mark { return chars.join(""); } 210 | 211 | BooleanValue "Boolean Value" 212 | = quotation_mark val:BooleanValue quotation_mark { return val; } 213 | / _ val:("true"/"false") _ { return val === 'true'; } 214 | 215 | /* 216 | * Supporting identifiers 217 | */ 218 | Or "or condition" 219 | = _ "or"i _ 220 | 221 | And "and condition" 222 | = _ "and"i _ 223 | 224 | begin_property = _ '["' 225 | end_property = '"]' _ 226 | 227 | 228 | /* ----- Numbers ----- */ 229 | decimal_point = "." 230 | digit1_9 = [1-9] 231 | e = [eE] 232 | exp = e (minus / plus)? DIGIT+ 233 | frac = decimal_point DIGIT+ 234 | int = zero / (digit1_9 DIGIT*) 235 | minus = "-" 236 | plus = "+" 237 | zero = "0" 238 | 239 | /* ----- Strings ----- */ 240 | char 241 | = unescaped 242 | / escape 243 | sequence:( 244 | '"' 245 | / "\\" 246 | / "/" 247 | / "b" { return "\b"; } 248 | / "f" { return "\f"; } 249 | / "n" { return "\n"; } 250 | / "r" { return "\r"; } 251 | / "t" { return "\t"; } 252 | / "u" digits:$(HEXDIG HEXDIG HEXDIG HEXDIG) { 253 | return String.fromCharCode(parseInt(digits, 16)); 254 | } 255 | ) { return sequence; } 256 | 257 | escape = "\\" 258 | quotation_mark = '"' 259 | unescaped = [^\0-\x1F\x22\x5C] 260 | 261 | DIGIT = [0-9] 262 | HEXDIG = [0-9a-f]i 263 | 264 | /* Date */ 265 | iso_date_time 266 | = date ("T" time)? { return text(); } 267 | 268 | date_century 269 | // 00-99 270 | = $(DIGIT DIGIT) { return text(); } 271 | 272 | date_decade 273 | // 0-9 274 | = DIGIT { return text(); } 275 | 276 | date_subdecade 277 | // 0-9 278 | = DIGIT { return text(); } 279 | 280 | date_year 281 | = date_decade date_subdecade { return text(); } 282 | 283 | date_fullyear 284 | = date_century date_year { return text(); } 285 | 286 | date_month 287 | // 01-12 288 | = $(DIGIT DIGIT) { return text(); } 289 | 290 | date_wday 291 | // 1-7 292 | // 1 is Monday, 7 is Sunday 293 | = DIGIT { return text(); } 294 | 295 | date_mday 296 | // 01-28, 01-29, 01-30, 01-31 based on 297 | // month/year 298 | = $(DIGIT DIGIT) { return text(); } 299 | 300 | date_yday 301 | // 001-365, 001-366 based on year 302 | = $(DIGIT DIGIT DIGIT) { return text(); } 303 | 304 | date_week 305 | // 01-52, 01-53 based on year 306 | = $(DIGIT DIGIT) { return text(); } 307 | 308 | datepart_fullyear 309 | = date_century? date_year "-"? { return text(); } 310 | 311 | datepart_ptyear 312 | = "-" (date_subdecade "-"?)? { return text(); } 313 | 314 | datepart_wkyear 315 | = datepart_ptyear 316 | / datepart_fullyear 317 | 318 | dateopt_century 319 | = "-" 320 | / date_century 321 | 322 | dateopt_fullyear 323 | = "-" 324 | / datepart_fullyear 325 | 326 | dateopt_year 327 | = "-" 328 | / date_year "-"? 329 | 330 | dateopt_month 331 | = "-" 332 | / date_month "-"? 333 | 334 | dateopt_week 335 | = "-" 336 | / date_week "-"? 337 | 338 | datespec_full 339 | = datepart_fullyear date_month "-"? date_mday { return text(); } 340 | 341 | datespec_year 342 | = date_century 343 | / dateopt_century date_year 344 | 345 | datespec_month 346 | = "-" dateopt_year date_month ("-"? date_mday) { return text(); } 347 | 348 | datespec_mday 349 | = "--" dateopt_month date_mday { return text(); } 350 | 351 | datespec_week 352 | = datepart_wkyear "W" (date_week / dateopt_week date_wday) { return text(); } 353 | 354 | datespec_wday 355 | = "---" date_wday { return text(); } 356 | 357 | datespec_yday 358 | = dateopt_fullyear date_yday { return text(); } 359 | 360 | date 361 | = datespec_full 362 | / datespec_year 363 | / datespec_month 364 | / datespec_mday 365 | / datespec_week 366 | / datespec_wday 367 | / datespec_yday 368 | 369 | 370 | /* Time */ 371 | time_hour 372 | // 00-24 373 | = $(DIGIT DIGIT) { return text(); } 374 | 375 | time_minute 376 | // 00-59 377 | = $(DIGIT DIGIT) { return text(); } 378 | 379 | time_second 380 | // 00-58, 00-59, 00-60 based on 381 | // leap-second rules 382 | = $(DIGIT DIGIT) { return text(); } 383 | 384 | time_fraction 385 | = ("," / ".") $(DIGIT+) { return text(); } 386 | 387 | time_numoffset 388 | = ("+" / "-") time_hour (":"? time_minute)? { return text(); } 389 | 390 | time_zone 391 | = "Z" 392 | / time_numoffset 393 | 394 | timeopt_hour 395 | = "-" 396 | / time_hour ":"? 397 | 398 | timeopt_minute 399 | = "-" 400 | / time_minute ":"? 401 | 402 | timespec_hour 403 | = time_hour (":"? time_minute (":"? time_second)?)? { return text(); } 404 | 405 | timespec_minute 406 | = timeopt_hour time_minute (":"? time_second)? { return text(); } 407 | 408 | timespec_second 409 | = "-" timeopt_minute time_second { return text(); } 410 | 411 | timespec_base 412 | = timespec_hour 413 | / timespec_minute 414 | / timespec_second 415 | 416 | time 417 | = timespec_base time_fraction? time_zone? { return text(); } 418 | 419 | _ "whitespace" 420 | = [ \t\n\r]* 421 | -------------------------------------------------------------------------------- /src/parse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('elastic-muto'); 4 | 5 | const isNil = require('lodash.isnil'); 6 | const isEmpty = require('lodash.isempty'); 7 | const isString = require('lodash.isstring'); 8 | const isFunction = require('lodash.isfunction'); 9 | const cosmiconfig = require('cosmiconfig'); 10 | 11 | const mutoParser = require('./muto-parser'); 12 | const qryBuilderDef = require('./query-builder-def'); 13 | 14 | const explorer = cosmiconfig('muto', { 15 | packageProp: false, 16 | rc: false, 17 | js: 'muto.config.js', 18 | argv: false, 19 | sync: true 20 | }); 21 | 22 | const qryBuilder = Object.assign({}, qryBuilderDef); 23 | try { 24 | debug('Loading user config using cosmiconfig(sync)'); 25 | const result = explorer.load('.'); 26 | if (!isNil(result)) { 27 | debug('Successfully parsed config -', result); 28 | Object.assign(qryBuilder, result.config); 29 | } else { 30 | debug('muto.config.js not found'); 31 | } 32 | } catch (err) { 33 | console.error('Failed to parse config!'); 34 | console.error(err); 35 | } 36 | 37 | /** 38 | * Parses given expression and generates an `elastic-builder` query object. 39 | * 40 | * @example 41 | * // Pass expression as string 42 | * const qry = muto.parse( 43 | * '["discount"] is false or (["psngr_cnt"] > 81 and ["booking_mode"] contains "Airport")' 44 | * ) 45 | * 46 | * // OR 47 | * // Pass conditions using helper classes 48 | * const qry = muto.parse( 49 | * muto.where() 50 | * .or(muto.cn('discount').is(false)) 51 | * .or( 52 | * muto.where() 53 | * .and(muto.cn('psngr_cnt', 'gt', 81)) 54 | * .and('["booking_mode"] contains "Airport"') 55 | * ) 56 | * ); 57 | * 58 | * qry.toJSON() 59 | * { 60 | * "bool": { 61 | * "should": [ 62 | * { "term": { "discount": false } }, 63 | * { 64 | * "bool": { 65 | * "must": [ 66 | * { 67 | * "range": { "psngr_cnt": { "gt": 81 } } 68 | * }, 69 | * { 70 | * "match": { "booking_mode": "Airport" } 71 | * } 72 | * ] 73 | * } 74 | * } 75 | * ] 76 | * } 77 | * } 78 | * 79 | * @param {string|Where|Condition} expr 80 | * @param {Array} [notAnalysedFields] 81 | * @returns {Object} `elastic-builder` `BoolQuery` object generated by 82 | * parsing the expression 83 | */ 84 | module.exports = function parse(expr, notAnalysedFields) { 85 | if (isEmpty(expr)) { 86 | throw new Error('Expression cannot be empty!'); 87 | } 88 | 89 | let strExpr; 90 | if (isString(expr)) strExpr = expr; 91 | else if (isFunction(expr.build)) strExpr = expr.build(); 92 | else strExpr = expr.toString(); 93 | 94 | debug("Parsing expression '%s'", strExpr); 95 | /* 96 | Wrap expression in (). 97 | This handles the edge case of 98 | (ConditionA and ConditionB) or (ConditionC and ConditionD) 99 | Was not able to handle in PEG.js due to 100 | GrammarError: Possible infinite loop when parsing (left recursion: Expression -> Expression). 101 | (expr) will be parsed the same as ((expr)) 102 | So even if expression is already wrapped, it is okay. 103 | */ 104 | try { 105 | return mutoParser.parse( 106 | `(${strExpr})`, 107 | Object.assign( 108 | { notAnalysedFields: new Set(notAnalysedFields), debug }, 109 | qryBuilder 110 | ) 111 | ); 112 | } catch (err) { 113 | console.error('Failed to parse expression', strExpr); 114 | err.expression = strExpr; 115 | throw err; 116 | } 117 | }; 118 | -------------------------------------------------------------------------------- /src/query-builder-def.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('elastic-muto'); 4 | 5 | const { 6 | termLevelQueries: { TermQuery, RangeQuery, ExistsQuery }, 7 | fullTextQueries: { MatchQuery }, 8 | compoundQueries: { BoolQuery } 9 | } = require('elastic-builder/lib/queries'); 10 | 11 | module.exports = { 12 | // Condition builder for number less than or equal to 13 | numLte(key, value) { 14 | debug('Number property less than or equal to condition'); 15 | debug('key - %s, value - %s', key, value); 16 | return new RangeQuery(key).lte(value); 17 | }, 18 | 19 | // Condition builder for number greater than or equal to 20 | numGte(key, value) { 21 | debug('Number property greater than or equal to condition'); 22 | debug('key - %s, value - %s', key, value); 23 | return new RangeQuery(key).gte(value); 24 | }, 25 | 26 | // Condition builder for number less than 27 | numLt(key, value) { 28 | debug('Number property less than condition'); 29 | debug('key - %s, value - %s', key, value); 30 | return new RangeQuery(key).lt(value); 31 | }, 32 | 33 | // Condition builder for number greater than or equal to 34 | numGt(key, value) { 35 | debug('Number property greater than condition'); 36 | debug('key - %s, value - %s', key, value); 37 | return new RangeQuery(key).gt(value); 38 | }, 39 | 40 | // Condition builder for number equalality 41 | numEq(key, value) { 42 | debug('Number property equality condition'); 43 | debug('key - %s, value - %s', key, value); 44 | return new TermQuery(key, value); 45 | }, 46 | 47 | // Condition builder for number inequality 48 | numNe(key, value) { 49 | debug('Number property inequality condition'); 50 | debug('key - %s, value - %s', key, value); 51 | return new BoolQuery() 52 | .must(new ExistsQuery(key)) 53 | .mustNot(new TermQuery(key, value)); 54 | }, 55 | 56 | // Condition builder for date less than or equal to 57 | dateLte(key, value) { 58 | debug('Date property less than or equal to condition'); 59 | debug('key - %s, value - %s', key, value); 60 | return new RangeQuery(key).lte(value.getTime()); 61 | }, 62 | 63 | // Condition builder for date greater than or equal to 64 | dateGte(key, value) { 65 | debug('Date property less than or equal to condition'); 66 | debug('key - %s, value - %s', key, value); 67 | return new RangeQuery(key).gte(value.getTime()); 68 | }, 69 | 70 | // Condition builder for date less than 71 | dateLt(key, value) { 72 | debug('Date property less than condition'); 73 | debug('key - %s, value - %s', key, value); 74 | return new RangeQuery(key).lt(value.getTime()); 75 | }, 76 | 77 | // Condition builder for date greater than 78 | dateGt(key, value) { 79 | debug('Date property greater than condition'); 80 | debug('key - %s, value - %s', key, value); 81 | return new RangeQuery(key).gt(value.getTime()); 82 | }, 83 | 84 | // Condition builder for date equality 85 | dateEq(key, value) { 86 | debug('Date property equality condition'); 87 | debug('key - %s, value - %s', key, value); 88 | return new RangeQuery(key) 89 | .gte(`${value.getTime()}||/d`) 90 | .lte(`${value.getTime()}||+1d/d`); 91 | }, 92 | 93 | // Condition builder for string contains 94 | strContains(key, value) { 95 | debug('String property contains condition'); 96 | debug('key - %s, value - %s', key, value); 97 | return new MatchQuery(key, value); 98 | }, 99 | 100 | // Condition builder for string does not contain 101 | strNotContains(key, value) { 102 | debug('String property does not contain condition'); 103 | debug('key - %s, value - %s', key, value); 104 | return new BoolQuery() 105 | .must(new ExistsQuery(key)) 106 | .mustNot(new MatchQuery(key, value)); 107 | }, 108 | 109 | // Condition builder for string equality 110 | strEq(key, value, notAnalysedFields) { 111 | debug('String property equality condition'); 112 | debug('key - %s, value - %s', key, value); 113 | const fieldName = notAnalysedFields.has(key) ? key : `${key}.keyword`; 114 | return new TermQuery(fieldName, value); 115 | }, 116 | 117 | // Condition builder for string inequality 118 | strNe(key, value, notAnalysedFields) { 119 | debug('String property inequality condition'); 120 | debug('key - %s, value - %s', key, value); 121 | const fieldName = notAnalysedFields.has(key) ? key : `${key}.keyword`; 122 | return new BoolQuery() 123 | .must(new ExistsQuery(key)) 124 | .mustNot(new TermQuery(fieldName, value)); 125 | }, 126 | 127 | // Condition for boolean property equality 128 | bool(key, value) { 129 | debug('Boolean condition'); 130 | debug('key - %s, value - %s', key, value); 131 | return new TermQuery(key, value); 132 | }, 133 | 134 | // Condition builder for property exists 135 | exists(key) { 136 | debug('Property Exists condition'); 137 | debug('key - %s', key); 138 | return new ExistsQuery(key); 139 | }, 140 | 141 | // Condition builder for property missing 142 | missing(key) { 143 | debug('Property does not exist condition'); 144 | debug('key - %s', key); 145 | return new BoolQuery().mustNot(new ExistsQuery(key)); 146 | }, 147 | 148 | // Function for building property key 149 | propertyKey: chars => chars.join('') 150 | }; 151 | -------------------------------------------------------------------------------- /src/where.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isEmpty = require('lodash.isempty'); 4 | const isNil = require('lodash.isnil'); 5 | const hasIn = require('lodash.hasin'); 6 | 7 | const AND = 'and'; 8 | const OR = 'or'; 9 | 10 | /** 11 | * Class for building `Where` expressions 12 | * 13 | * @example 14 | * const where = muto.where() 15 | * .or(muto.cn('discount').is(false)) 16 | * .or( 17 | * muto.where() 18 | * // Pass conditions using helper classes 19 | * .and(muto.cn('psngr_cnt', 'gt', 81)) 20 | * // Or a simple string will do 21 | * .and('["booking_mode"] contains "Airport"') 22 | * ) 23 | * .build(); 24 | * '["discount"] is false or (["psngr_cnt"] > 81 and ["booking_mode"] contains "Airport")' 25 | * 26 | * @param {Condition|Where|string} [condition] 27 | */ 28 | class Where { 29 | // eslint-disable-next-line require-jsdoc 30 | constructor(condition) { 31 | this._join = ''; 32 | this._conditions = []; 33 | 34 | if (!isNil(condition)) this._conditions.push(condition); 35 | } 36 | 37 | /** 38 | * Helper method for adding a condition. 39 | * 40 | * @private 41 | * @param {string} type `and`/`or` 42 | * @param {Condition|Where|string} condition 43 | * @returns {Where} returns `this` so that calls can be chained 44 | */ 45 | _addCondition(type, condition) { 46 | if (isEmpty(this._join)) this._join = type; 47 | else if (this._join !== type) { 48 | console.log( 49 | 'Use nested Where class instances for combining `and` with `or`' 50 | ); 51 | throw new Error('Illegal operation! Join types cannot be mixed!'); 52 | } 53 | 54 | this._conditions.push(condition); 55 | return this; 56 | } 57 | 58 | /** 59 | * Adds an `and` condition. The condition can be instance of `Condition`, 60 | * `Where`(nested expr) or just a string. 61 | * 62 | * @param {Condition|Where|string} condition The condition to be added 63 | * @returns {Where} returns `this` so that calls can be chained 64 | * @throws {Error} If `and`, `or` are called on the same instance of `Where` 65 | */ 66 | and(condition) { 67 | return this._addCondition(AND, condition); 68 | } 69 | 70 | /** 71 | * Adds an `or` condition. The condition can be instance of `Condition`, 72 | * `Where`(nested expr) or just a string. 73 | * 74 | * @param {Condition|Where|string} condition The condition to be added 75 | * @returns {Where} returns `this` so that calls can be chained 76 | * @throws {Error} If `and`, `or` are called on the same instance of `Where` 77 | */ 78 | or(condition) { 79 | return this._addCondition(OR, condition); 80 | } 81 | 82 | /** 83 | * Build and return muto syntax for Where Expression 84 | * 85 | * @returns {string} returns a string which maps to the muto syntax for 86 | * Where Expression 87 | */ 88 | build() { 89 | const whereStr = this._conditions 90 | .map(cn => (hasIn(cn, 'build') ? cn.build() : cn)) 91 | .join(` ${this._join} `); 92 | 93 | return `(${whereStr})`; 94 | } 95 | 96 | /** 97 | * Hotwire to return `this.build()` 98 | * 99 | * @override 100 | * @returns {string} 101 | */ 102 | toString() { 103 | return this.build(); 104 | } 105 | 106 | /** 107 | * Hotwire to return `this.build()` 108 | * 109 | * @override 110 | * @returns {string} 111 | */ 112 | toJSON() { 113 | return this.build(); 114 | } 115 | } 116 | 117 | module.exports = Where; 118 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: chatur/test 3 | -------------------------------------------------------------------------------- /test/__mocks__/cosmiconfig.js: -------------------------------------------------------------------------------- 1 | module.exports = jest.fn(() => ({ load: () => null })); 2 | -------------------------------------------------------------------------------- /test/condition.test.js: -------------------------------------------------------------------------------- 1 | import * as muto from '../src'; 2 | 3 | const { Condition } = muto; 4 | 5 | describe('Condition builder', () => { 6 | it('can be instantiated with params', () => { 7 | const cn = new Condition('my_prop', 'eq', 10).build(); 8 | expect(cn).toBe('["my_prop"] == 10'); 9 | }); 10 | 11 | it('aliases work as expected', () => { 12 | const cn = new Condition('my_prop', 'eq', 10); 13 | expect(muto.cn('my_prop', 'eq', 10)).toEqual(cn); 14 | expect(muto.condition('my_prop', 'eq', 10)).toEqual(cn); 15 | }); 16 | 17 | it('does not throw error in constructor for valid operators', () => { 18 | [ 19 | 'is', 20 | 'eq', 21 | 'ne', 22 | 'lt', 23 | 'lte', 24 | 'gt', 25 | 'gte', 26 | 'contains', 27 | 'notcontains' 28 | ].forEach(operator => 29 | expect(() => new Condition('my_prop', operator, 10)).not.toThrow() 30 | ); 31 | 32 | ['exists', 'missing'].forEach(operator => 33 | expect(() => new Condition('my_prop', operator)).not.toThrow() 34 | ); 35 | }); 36 | 37 | it('throws error for invalid operator param to constructor', () => { 38 | expect(() => new Condition('my_prop', 'invld', 10)).toThrowError( 39 | /Invalid operator/ 40 | ); 41 | expect(() => new Condition('my_prop', 'prop', 10)).toThrowError( 42 | /Invalid operator/ 43 | ); 44 | expect(() => new Condition('my_prop', 'build', 10)).toThrowError( 45 | /Invalid operator/ 46 | ); 47 | }); 48 | 49 | it('sets the property key', () => { 50 | const cn = new Condition() 51 | .prop('my_prop') 52 | .eq('dancing monkeys') 53 | .build(); 54 | expect(cn).toBe('["my_prop"] == "dancing monkeys"'); 55 | }); 56 | 57 | it('builds boolean condition', () => { 58 | const cn = new Condition() 59 | .prop('my_prop') 60 | .is(true) 61 | .build(); 62 | expect(cn).toBe('["my_prop"] is true'); 63 | }); 64 | 65 | it('builds equality condition', () => { 66 | const cn = new Condition() 67 | .prop('my_prop') 68 | .eq('dancing monkeys') 69 | .build(); 70 | expect(cn).toBe('["my_prop"] == "dancing monkeys"'); 71 | }); 72 | 73 | it('builds inequality condition', () => { 74 | const cn = new Condition() 75 | .prop('my_prop') 76 | .ne('dancing monkeys') 77 | .build(); 78 | expect(cn).toBe('["my_prop"] != "dancing monkeys"'); 79 | }); 80 | 81 | it('builds less than condition', () => { 82 | const cn = new Condition() 83 | .prop('my_prop') 84 | .lt(299792458) 85 | .build(); 86 | expect(cn).toBe('["my_prop"] < 299792458'); 87 | }); 88 | 89 | it('builds less than or equal to condition', () => { 90 | const cn = new Condition() 91 | .prop('my_prop') 92 | .lte(299792458) 93 | .build(); 94 | expect(cn).toBe('["my_prop"] <= 299792458'); 95 | }); 96 | 97 | it('builds greater than condition', () => { 98 | const cn = new Condition() 99 | .prop('my_prop') 100 | .gt(299792458) 101 | .build(); 102 | expect(cn).toBe('["my_prop"] > 299792458'); 103 | }); 104 | 105 | it('builds greater than or equal to condition', () => { 106 | const cn = new Condition() 107 | .prop('my_prop') 108 | .gte(299792458) 109 | .build(); 110 | expect(cn).toBe('["my_prop"] >= 299792458'); 111 | }); 112 | 113 | it('builds property exists condition', () => { 114 | const cn = new Condition() 115 | .prop('one_piece') 116 | .exists() 117 | .build(); 118 | expect(cn).toBe('["one_piece"] exists'); 119 | }); 120 | 121 | it('builds property missing condition', () => { 122 | const cn = new Condition() 123 | .prop('the_last_airbender') 124 | .missing() 125 | .build(); 126 | expect(cn).toBe('["the_last_airbender"] missing'); 127 | }); 128 | 129 | it('builds contains condition', () => { 130 | const cn = new Condition() 131 | .prop('potion') 132 | .contains('magic') 133 | .build(); 134 | expect(cn).toBe('["potion"] contains "magic"'); 135 | }); 136 | 137 | it('builds notcontains condition', () => { 138 | const cn = new Condition() 139 | .prop('anime') 140 | .notContains('fillers') 141 | .build(); 142 | expect(cn).toBe('["anime"] !contains "fillers"'); 143 | }); 144 | 145 | it('delegates to the build function on calling toJSON', () => { 146 | const cn = new Condition().prop('prophecy').is(true); 147 | const spy = jest.spyOn(cn, 'build'); 148 | cn.toJSON(); 149 | 150 | expect(spy).toHaveBeenCalled(); 151 | 152 | spy.mockReset(); 153 | 154 | JSON.stringify(cn); 155 | expect(spy).toHaveBeenCalled(); 156 | 157 | spy.mockReset(); 158 | spy.mockRestore(); 159 | }); 160 | 161 | it('delegates to the build function on calling toString', () => { 162 | const cn = new Condition().prop('prophecy').is(true); 163 | const spy = jest.spyOn(cn, 'build'); 164 | cn.toString(); 165 | 166 | expect(spy).toHaveBeenCalled(); 167 | 168 | spy.mockReset(); 169 | spy.mockRestore(); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/helpers/condition-factory.js: -------------------------------------------------------------------------------- 1 | import * as bob from 'elastic-builder'; 2 | import * as muto from '../../src'; 3 | 4 | const d1 = new Date('October 13, 2013 11:13:00'); 5 | const d2 = new Date('October 13, 2016 11:13:00'); 6 | 7 | export const conditions = [ 8 | 'numLt', 9 | 'numLte', 10 | 'numGt', 11 | 'numGte', 12 | 'numEq', 13 | 'numNe', 14 | 'dateLt', 15 | 'dateLte', 16 | 'dateGt', 17 | 'dateGte', 18 | 'dateEq', 19 | 'strContains', 20 | 'strNotContains', 21 | 'strEq', 22 | // 'strEqNotAnalyzed', // special case 23 | 'strNe', 24 | // 'strNeNotAnalyzed', 25 | 'bool', 26 | 'exists', 27 | 'missing' 28 | ]; 29 | 30 | /** 31 | * Helper function to get random condition excluding given set 32 | * @private 33 | * @param {Set} exclude 34 | * @returns {function} 35 | */ 36 | export function randCnGen() { 37 | const exclude = new Set(); 38 | return () => { 39 | let randCn; 40 | do { 41 | randCn = conditions[Math.floor(Math.random() * conditions.length)]; 42 | } while (exclude.has(randCn)); 43 | exclude.add(randCn); 44 | return randCn; 45 | }; 46 | } 47 | 48 | export const cnNamesMap = { 49 | numLt: 'number less than', 50 | numLte: 'number less than or equal to', 51 | numGt: 'number greater than', 52 | numGte: 'number greater than or equal to', 53 | numEq: 'number equal to', 54 | numNe: 'number not equal to', 55 | dateLt: 'date less than', 56 | dateLte: 'date less than or equal to', 57 | dateGt: 'date greater than', 58 | dateGte: 'date greater than or equal to', 59 | dateEq: 'date equal to', 60 | strContains: 'string contains', 61 | strNotContains: 'string not contains', 62 | strEq: 'string equals', 63 | // 'strEqNotAnalyzed': 'string equals for not analyzed field', // special case 64 | strNe: 'string not equals', 65 | // 'strNeNotAnalyzed': 'string not equals for not analyzed field', // special case 66 | bool: 'bool', 67 | exists: 'exists', 68 | missing: 'missing' 69 | }; 70 | 71 | export const cnMap = { 72 | numLt: muto.cn('num_idiots').lt(0), 73 | numLte: muto.cn('num_idiots').lte(0), 74 | numGt: muto.cn('contributors').gt(1), 75 | numGte: muto.cn('contributors').gte(1), 76 | numEq: muto.cn('idiots').eq(0), 77 | numNe: muto.cn('idiots').ne(1), 78 | dateLt: muto.cn('date_fld').lt(d2), 79 | dateLte: muto.cn('date_fld').lte(d2), 80 | dateGt: muto.cn('date_fld').gt(d1), 81 | dateGte: muto.cn('date_fld').gte(d1), 82 | dateEq: muto.cn('date_fld').eq(d1), 83 | strContains: muto.cn('life').contains('friends'), 84 | strNotContains: muto.cn('anime').notContains('fillers'), 85 | strEq: muto.cn('elasticsearch').eq('awesome'), 86 | // 'strEqNotAnalyzed', 87 | strNe: muto.cn('foo').ne('bar'), 88 | // 'strNeNotAnalyzed', 89 | bool: muto.cn('prophecy').is(true), 90 | exists: muto.cn('unicorn').exists(), 91 | missing: muto.cn('turds').missing() 92 | }; 93 | 94 | export const cnQryMap = { 95 | numLt: bob.rangeQuery('num_idiots').lt(0), 96 | numLte: bob.rangeQuery('num_idiots').lte(0), 97 | numGt: bob.rangeQuery('contributors').gt(1), 98 | numGte: bob.rangeQuery('contributors').gte(1), 99 | numEq: bob.termQuery('idiots', 0), 100 | numNe: bob 101 | .boolQuery() 102 | .must(bob.existsQuery('idiots')) 103 | .mustNot(bob.termQuery('idiots', 1)), 104 | dateLt: bob.rangeQuery('date_fld').lt(d2.getTime()), 105 | dateLte: bob.rangeQuery('date_fld').lte(d2.getTime()), 106 | dateGt: bob.rangeQuery('date_fld').gt(d1.getTime()), 107 | dateGte: bob.rangeQuery('date_fld').gte(d1.getTime()), 108 | dateEq: bob 109 | .rangeQuery('date_fld') 110 | .gte(`${d1.getTime()}||/d`) 111 | .lte(`${d1.getTime()}||+1d/d`), 112 | strContains: bob.matchQuery('life', 'friends'), 113 | strNotContains: bob 114 | .boolQuery() 115 | .must(bob.existsQuery('anime')) 116 | .mustNot(bob.matchQuery('anime', 'fillers')), 117 | strEq: bob.termQuery('elasticsearch.keyword', 'awesome'), 118 | strEqNotAnalyzed: bob.termQuery('elasticsearch', 'awesome'), 119 | strNe: bob 120 | .boolQuery() 121 | .must(bob.existsQuery('foo')) 122 | .mustNot(bob.termQuery('foo.keyword', 'bar')), 123 | strNeNotAnalyzed: bob 124 | .boolQuery() 125 | .must(bob.existsQuery('foo')) 126 | .mustNot(bob.termQuery('foo', 'bar')), 127 | bool: bob.termQuery('prophecy', true), 128 | exists: bob.existsQuery('unicorn'), 129 | missing: bob.cookMissingQuery('turds') 130 | }; 131 | 132 | export const qryBldrArgs = { 133 | numLt: ['num_idiots', 0], 134 | numLte: ['num_idiots', 0], 135 | numGt: ['contributors', 1], 136 | numGte: ['contributors', 1], 137 | numEq: ['idiots', 0], 138 | numNe: ['idiots', 1], 139 | dateLt: ['date_fld', d2], 140 | dateLte: ['date_fld', d2], 141 | dateGt: ['date_fld', d1], 142 | dateGte: ['date_fld', d1], 143 | dateEq: ['date_fld', d1], 144 | strContains: ['life', 'friends'], 145 | strNotContains: ['anime', 'fillers'], 146 | strEq: ['elasticsearch', 'awesome', new Set()], 147 | strEqNotAnalyzed: ['elasticsearch', 'awesome', new Set(['elasticsearch'])], 148 | strNe: ['foo', 'bar', new Set()], 149 | strNeNotAnalyzed: ['foo', 'bar', new Set(['foo'])], 150 | bool: ['prophecy', true], 151 | exists: ['unicorn'], 152 | missing: ['turds'] 153 | }; 154 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import * as muto from '../src'; 2 | 3 | describe('index.js', () => { 4 | it('exports members', () => { 5 | expect(muto).toBeDefined(); 6 | expect(muto).toBeInstanceOf(Object); 7 | 8 | expect(muto.parse).toBeDefined(); 9 | expect(muto.parse).toBeInstanceOf(Function); 10 | 11 | expect(muto.SyntaxError).toBeDefined(); 12 | expect(muto.SyntaxError).toBeInstanceOf(Function); 13 | 14 | expect(muto.Where).toBeDefined(); 15 | expect(muto.Where).toBeInstanceOf(Function); 16 | 17 | expect(muto.where).toBeDefined(); 18 | expect(muto.where).toBeInstanceOf(Function); 19 | 20 | expect(muto.Condition).toBeDefined(); 21 | expect(muto.Condition).toBeInstanceOf(Function); 22 | 23 | expect(muto.condition).toBeDefined(); 24 | expect(muto.condition).toBeInstanceOf(Function); 25 | 26 | expect(muto.cn).toBeDefined(); 27 | expect(muto.cn).toBeInstanceOf(Function); 28 | 29 | expect(muto.prettyPrint).toBeDefined(); 30 | expect(muto.prettyPrint).toBeInstanceOf(Function); 31 | }); 32 | 33 | describe('prettyPrint', () => { 34 | it('calls parse', () => { 35 | // Doesn't work for some reason 36 | // const spyParse = jest.spyOn(muto, 'parse'); 37 | const spyConsole = jest.spyOn(console, 'log'); 38 | 39 | muto.prettyPrint('["awesome"] is true'); 40 | 41 | // expect(spyParse).toHaveBeenCalled(); 42 | expect(spyConsole).toHaveBeenCalled(); 43 | 44 | // spyParse.mockReset(); 45 | // spyParse.mockRestore(); 46 | spyConsole.mockReset(); 47 | spyConsole.mockRestore(); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/parse-mock-config.test.js: -------------------------------------------------------------------------------- 1 | import * as bob from 'elastic-builder'; 2 | import { parse } from '../src'; 3 | 4 | jest.mock('cosmiconfig', () => () => ({ 5 | load: () => ({ 6 | config: { 7 | propertyKey: chars => `$${chars.join('')}` 8 | } 9 | }) 10 | })); 11 | 12 | describe('parse', () => { 13 | it('reads custom config if applicable', () => { 14 | const qry = parse('["foo"] == "bar"'); 15 | expect(qry).toEqual( 16 | bob.boolQuery().must(bob.termQuery('$foo.keyword', 'bar')) 17 | ); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/parse.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-lines: "off" */ 2 | 3 | import * as bob from 'elastic-builder'; 4 | import Combinatorics from 'js-combinatorics'; 5 | import * as muto from '../src'; 6 | import { 7 | conditions, 8 | cnNamesMap, 9 | cnMap, 10 | cnQryMap, 11 | randCnGen 12 | } from './helpers/condition-factory'; 13 | 14 | const { parse } = muto; 15 | 16 | const existsCnStr = cnMap.exists.build(); 17 | 18 | describe('parse', () => { 19 | it('throws error for empty expression', () => { 20 | expect(() => parse(null)).toThrowError('Expression cannot be empty!'); 21 | }); 22 | 23 | it('can parse strings', () => { 24 | const qry = parse(existsCnStr); 25 | 26 | expect(qry).toBeInstanceOf(bob.BoolQuery); 27 | expect(qry).toEqual(bob.boolQuery().must(cnQryMap.exists)); 28 | }); 29 | 30 | it('can parse objects with build method', () => { 31 | const testObj = { build: () => existsCnStr }; 32 | const spy = jest.spyOn(testObj, 'build'); 33 | 34 | const qry = parse(testObj); 35 | 36 | expect(spy).toHaveBeenCalled(); 37 | expect(qry).toBeInstanceOf(bob.BoolQuery); 38 | expect(qry).toEqual(bob.boolQuery().must(cnQryMap.exists)); 39 | }); 40 | 41 | it('calls toString as last resort', () => { 42 | const testObj = { toString: () => existsCnStr }; 43 | const spy = jest.spyOn(testObj, 'toString'); 44 | 45 | const qry = parse(testObj); 46 | 47 | expect(spy).toHaveBeenCalled(); 48 | expect(qry).toBeInstanceOf(bob.BoolQuery); 49 | expect(qry).toEqual(bob.boolQuery().must(cnQryMap.exists)); 50 | }); 51 | 52 | describe('simple condition', () => { 53 | conditions.forEach(cn => { 54 | test(cnNamesMap[cn], () => { 55 | const qry = parse(cnMap[cn]); 56 | 57 | expect(qry).toBeInstanceOf(bob.BoolQuery); 58 | if (cnQryMap[cn] instanceof bob.BoolQuery) { 59 | expect(qry).toEqual(cnQryMap[cn]); 60 | } else { 61 | expect(qry).toEqual(bob.boolQuery().must(cnQryMap[cn])); 62 | } 63 | }); 64 | }); 65 | 66 | test('string equals for not analyzed field', () => { 67 | const qry = parse(cnMap.strEq, ['elasticsearch']); 68 | expect(qry).toBeInstanceOf(bob.BoolQuery); 69 | expect(qry).toEqual( 70 | bob.boolQuery().must(cnQryMap.strEqNotAnalyzed) 71 | ); 72 | }); 73 | 74 | test('string not equals for not analyzed field', () => { 75 | const qry = parse(cnMap.strNe, ['foo']); 76 | expect(qry).toBeInstanceOf(bob.BoolQuery); 77 | expect(qry).toEqual(cnQryMap.strNeNotAnalyzed); 78 | }); 79 | }); 80 | 81 | describe('condition combinations', () => { 82 | const cmb2 = Combinatorics.combination(conditions, 2); 83 | 84 | let cmb; 85 | while ((cmb = cmb2.next())) { 86 | const [cn1, cn2] = cmb; 87 | test(`${cnNamesMap[cn1]} and ${cnNamesMap[cn2]}`, () => { 88 | const qry = parse(muto.where(cnMap[cn1]).and(cnMap[cn2])); 89 | 90 | expect(qry).toBeInstanceOf(bob.BoolQuery); 91 | expect(qry).toEqual( 92 | bob.boolQuery().must([cnQryMap[cn1], cnQryMap[cn2]]) 93 | ); 94 | }); 95 | 96 | test(`${cnNamesMap[cn1]} or ${cnNamesMap[cn2]}`, () => { 97 | const qry = parse(muto.where(cnMap[cn1]).or(cnMap[cn2])); 98 | 99 | expect(qry).toBeInstanceOf(bob.BoolQuery); 100 | expect(qry).toEqual( 101 | bob.boolQuery().should([cnQryMap[cn1], cnQryMap[cn2]]) 102 | ); 103 | }); 104 | } 105 | for (let idx = 0; idx < 100; idx++) { 106 | const randCn = randCnGen(); 107 | const cn1 = randCn(), 108 | cn2 = randCn(), 109 | cn3 = randCn(); 110 | test(`${cnNamesMap[cn1]} and ${cnNamesMap[cn2]} and ${ 111 | cnNamesMap[cn3] 112 | }`, () => { 113 | const qry = parse( 114 | muto 115 | .where(cnMap[cn1]) 116 | .and(cnMap[cn2]) 117 | .and(cnMap[cn3]) 118 | ); 119 | 120 | expect(qry).toBeInstanceOf(bob.BoolQuery); 121 | expect(qry).toEqual( 122 | bob 123 | .boolQuery() 124 | .must([cnQryMap[cn1], cnQryMap[cn2], cnQryMap[cn3]]) 125 | ); 126 | }); 127 | 128 | test(`${cnNamesMap[cn1]} or ${cnNamesMap[cn2]} or ${ 129 | cnNamesMap[cn3] 130 | }`, () => { 131 | const qry = parse( 132 | muto 133 | .where(cnMap[cn1]) 134 | .or(cnMap[cn2]) 135 | .or(cnMap[cn3]) 136 | ); 137 | 138 | expect(qry).toBeInstanceOf(bob.BoolQuery); 139 | expect(qry).toEqual( 140 | bob 141 | .boolQuery() 142 | .should([cnQryMap[cn1], cnQryMap[cn2], cnQryMap[cn3]]) 143 | ); 144 | }); 145 | 146 | test(`(${cnNamesMap[cn1]} and ${cnNamesMap[cn2]}) or ${ 147 | cnNamesMap[cn3] 148 | }`, () => { 149 | const qry = parse( 150 | muto 151 | .where(muto.where(cnMap[cn1]).and(cnMap[cn2])) 152 | .or(cnMap[cn3]) 153 | ); 154 | 155 | expect(qry).toBeInstanceOf(bob.BoolQuery); 156 | expect(qry).toEqual( 157 | bob 158 | .boolQuery() 159 | .should([ 160 | bob 161 | .boolQuery() 162 | .must([cnQryMap[cn1], cnQryMap[cn2]]), 163 | cnQryMap[cn3] 164 | ]) 165 | ); 166 | }); 167 | 168 | test(`(${cnNamesMap[cn1]} or ${cnNamesMap[cn2]}) and ${ 169 | cnNamesMap[cn3] 170 | }`, () => { 171 | const qry = parse( 172 | muto 173 | .where(muto.where(cnMap[cn1]).or(cnMap[cn2])) 174 | .and(cnMap[cn3]) 175 | ); 176 | 177 | expect(qry).toBeInstanceOf(bob.BoolQuery); 178 | expect(qry).toEqual( 179 | bob 180 | .boolQuery() 181 | .must([ 182 | bob 183 | .boolQuery() 184 | .should([cnQryMap[cn1], cnQryMap[cn2]]), 185 | cnQryMap[cn3] 186 | ]) 187 | ); 188 | }); 189 | 190 | test(`${cnNamesMap[cn1]} and (${cnNamesMap[cn2]} or ${ 191 | cnNamesMap[cn3] 192 | })`, () => { 193 | const qry = parse( 194 | muto 195 | .where(cnMap[cn1]) 196 | .and(muto.where(cnMap[cn2]).or(cnMap[cn3])) 197 | ); 198 | 199 | expect(qry).toBeInstanceOf(bob.BoolQuery); 200 | expect(qry).toEqual( 201 | bob 202 | .boolQuery() 203 | .must([ 204 | cnQryMap[cn1], 205 | bob 206 | .boolQuery() 207 | .should([cnQryMap[cn2], cnQryMap[cn3]]) 208 | ]) 209 | ); 210 | }); 211 | 212 | test(`${cnNamesMap[cn1]} or (${cnNamesMap[cn2]} and ${ 213 | cnNamesMap[cn3] 214 | })`, () => { 215 | const qry = parse( 216 | muto 217 | .where(cnMap[cn1]) 218 | .or(muto.where(cnMap[cn2]).and(cnMap[cn3])) 219 | ); 220 | 221 | expect(qry).toBeInstanceOf(bob.BoolQuery); 222 | expect(qry).toEqual( 223 | bob 224 | .boolQuery() 225 | .should([ 226 | cnQryMap[cn1], 227 | bob.boolQuery().must([cnQryMap[cn2], cnQryMap[cn3]]) 228 | ]) 229 | ); 230 | }); 231 | } 232 | // const cmb3 = Combinatorics.combination(conditions, 3); 233 | // while ((cmb = cmb3.next())) { 234 | // const [cn1, cn2, cn3] = cmb; 235 | // } 236 | }); 237 | 238 | describe('nested expressions', () => { 239 | describe('2 levels', () => { 240 | for (let idx = 0; idx < 100; idx++) { 241 | const randCn = randCnGen(); 242 | const cn1 = randCn(), 243 | cn2 = randCn(), 244 | cn3 = randCn(), 245 | cn4 = randCn(); 246 | test(`(${cnNamesMap[cn1]} and ${cnNamesMap[cn2]}) 247 | or 248 | (${cnNamesMap[cn3]} and ${cnNamesMap[cn4]})`, () => { 249 | const qry = parse( 250 | muto 251 | .where() 252 | .or(muto.where(cnMap[cn1]).and(cnMap[cn2])) 253 | .or(muto.where(cnMap[cn3]).and(cnMap[cn4])) 254 | ); 255 | 256 | expect(qry).toBeInstanceOf(bob.BoolQuery); 257 | expect(qry).toEqual( 258 | bob 259 | .boolQuery() 260 | .should([ 261 | bob 262 | .boolQuery() 263 | .must([cnQryMap[cn1], cnQryMap[cn2]]), 264 | bob 265 | .boolQuery() 266 | .must([cnQryMap[cn3], cnQryMap[cn4]]) 267 | ]) 268 | ); 269 | }); 270 | 271 | test(`(${cnNamesMap[cn1]} or ${cnNamesMap[cn2]}) 272 | and 273 | (${cnNamesMap[cn3]} or ${cnNamesMap[cn4]})`, () => { 274 | const qry = parse( 275 | muto 276 | .where() 277 | .and(muto.where(cnMap[cn1]).or(cnMap[cn2])) 278 | .and(muto.where(cnMap[cn3]).or(cnMap[cn4])) 279 | ); 280 | 281 | expect(qry).toBeInstanceOf(bob.BoolQuery); 282 | expect(qry).toEqual( 283 | bob 284 | .boolQuery() 285 | .must([ 286 | bob 287 | .boolQuery() 288 | .should([cnQryMap[cn1], cnQryMap[cn2]]), 289 | bob 290 | .boolQuery() 291 | .should([cnQryMap[cn3], cnQryMap[cn4]]) 292 | ]) 293 | ); 294 | }); 295 | } 296 | }); 297 | 298 | describe('3 levels', () => { 299 | for (let idx = 0; idx < 50; idx++) { 300 | const randCn = randCnGen(); 301 | const cn1 = randCn(), 302 | cn2 = randCn(), 303 | cn3 = randCn(); 304 | const cn4 = randCn(), 305 | cn5 = randCn(), 306 | cn6 = randCn(); 307 | 308 | test(`(${cnNamesMap[cn1]} and ${cnNamesMap[cn2]}) 309 | or 310 | (${cnNamesMap[cn3]} and ${cnNamesMap[cn4]} 311 | and 312 | (${cnNamesMap[cn5]} or ${cnNamesMap[cn6]}) 313 | )`, () => { 314 | const qry = parse( 315 | muto 316 | .where() 317 | .or(muto.where(cnMap[cn1]).and(cnMap[cn2])) 318 | .or( 319 | muto 320 | .where(cnMap[cn3]) 321 | .and(cnMap[cn4]) 322 | .and(muto.where(cnMap[cn5]).or(cnMap[cn6])) 323 | ) 324 | ); 325 | 326 | expect(qry).toBeInstanceOf(bob.BoolQuery); 327 | expect(qry).toEqual( 328 | bob 329 | .boolQuery() 330 | .should([ 331 | bob 332 | .boolQuery() 333 | .must([cnQryMap[cn1], cnQryMap[cn2]]), 334 | bob 335 | .boolQuery() 336 | .must([ 337 | cnQryMap[cn3], 338 | cnQryMap[cn4], 339 | bob 340 | .boolQuery() 341 | .should([ 342 | cnQryMap[cn5], 343 | cnQryMap[cn6] 344 | ]) 345 | ]) 346 | ]) 347 | ); 348 | }); 349 | 350 | test(`(${cnNamesMap[cn1]} or ${cnNamesMap[cn2]}) 351 | and 352 | (${cnNamesMap[cn3]} or ${cnNamesMap[cn4]} 353 | or 354 | (${cnNamesMap[cn5]} and ${cnNamesMap[cn6]}) 355 | )`, () => { 356 | const qry = parse( 357 | muto 358 | .where() 359 | .and(muto.where(cnMap[cn1]).or(cnMap[cn2])) 360 | .and( 361 | muto 362 | .where(cnMap[cn3]) 363 | .or(cnMap[cn4]) 364 | .or(muto.where(cnMap[cn5]).and(cnMap[cn6])) 365 | ) 366 | ); 367 | 368 | expect(qry).toBeInstanceOf(bob.BoolQuery); 369 | expect(qry).toEqual( 370 | bob 371 | .boolQuery() 372 | .must([ 373 | bob 374 | .boolQuery() 375 | .should([cnQryMap[cn1], cnQryMap[cn2]]), 376 | bob 377 | .boolQuery() 378 | .should([ 379 | cnQryMap[cn3], 380 | cnQryMap[cn4], 381 | bob 382 | .boolQuery() 383 | .must([ 384 | cnQryMap[cn5], 385 | cnQryMap[cn6] 386 | ]) 387 | ]) 388 | ]) 389 | ); 390 | }); 391 | } 392 | }); 393 | 394 | describe('4 levels', () => { 395 | for (let idx = 0; idx < 50; idx++) { 396 | const randCn = randCnGen(); 397 | const cn1 = randCn(), 398 | cn2 = randCn(), 399 | cn3 = randCn(), 400 | cn4 = randCn(); 401 | const cn5 = randCn(), 402 | cn6 = randCn(), 403 | cn7 = randCn(), 404 | cn8 = randCn(); 405 | 406 | test(`(${cnNamesMap[cn1]} and ${cnNamesMap[cn2]}) 407 | or 408 | (${cnNamesMap[cn3]} and ${cnNamesMap[cn4]} 409 | and 410 | (${cnNamesMap[cn5]} or ${cnNamesMap[cn6]} 411 | or 412 | (${cnNamesMap[cn7]} and ${cnNamesMap[cn8]}) 413 | ) 414 | )`, () => { 415 | const qry = parse( 416 | muto 417 | .where() 418 | .or(muto.where(cnMap[cn1]).and(cnMap[cn2])) 419 | .or( 420 | muto 421 | .where(cnMap[cn3]) 422 | .and(cnMap[cn4]) 423 | .and( 424 | muto 425 | .where(cnMap[cn5]) 426 | .or(cnMap[cn6]) 427 | .or( 428 | muto 429 | .where(cnMap[cn7]) 430 | .and(cnMap[cn8]) 431 | ) 432 | ) 433 | ) 434 | ); 435 | 436 | expect(qry).toBeInstanceOf(bob.BoolQuery); 437 | expect(qry).toEqual( 438 | bob 439 | .boolQuery() 440 | .should([ 441 | bob 442 | .boolQuery() 443 | .must([cnQryMap[cn1], cnQryMap[cn2]]), 444 | bob 445 | .boolQuery() 446 | .must([ 447 | cnQryMap[cn3], 448 | cnQryMap[cn4], 449 | bob 450 | .boolQuery() 451 | .should([ 452 | cnQryMap[cn5], 453 | cnQryMap[cn6], 454 | bob 455 | .boolQuery() 456 | .must([ 457 | cnQryMap[cn7], 458 | cnQryMap[cn8] 459 | ]) 460 | ]) 461 | ]) 462 | ]) 463 | ); 464 | }); 465 | 466 | test(`(${cnNamesMap[cn1]} or ${cnNamesMap[cn2]}) 467 | and 468 | (${cnNamesMap[cn3]} or ${cnNamesMap[cn4]} 469 | or 470 | (${cnNamesMap[cn5]} and ${cnNamesMap[cn6]} 471 | and 472 | (${cnNamesMap[cn7]} or ${cnNamesMap[cn8]}) 473 | ) 474 | )`, () => { 475 | const qry = parse( 476 | muto 477 | .where() 478 | .and(muto.where(cnMap[cn1]).or(cnMap[cn2])) 479 | .and( 480 | muto 481 | .where(cnMap[cn3]) 482 | .or(cnMap[cn4]) 483 | .or( 484 | muto 485 | .where(cnMap[cn5]) 486 | .and(cnMap[cn6]) 487 | .and( 488 | muto 489 | .where(cnMap[cn7]) 490 | .or(cnMap[cn8]) 491 | ) 492 | ) 493 | ) 494 | ); 495 | 496 | expect(qry).toBeInstanceOf(bob.BoolQuery); 497 | expect(qry).toEqual( 498 | bob 499 | .boolQuery() 500 | .must([ 501 | bob 502 | .boolQuery() 503 | .should([cnQryMap[cn1], cnQryMap[cn2]]), 504 | bob 505 | .boolQuery() 506 | .should([ 507 | cnQryMap[cn3], 508 | cnQryMap[cn4], 509 | bob 510 | .boolQuery() 511 | .must([ 512 | cnQryMap[cn5], 513 | cnQryMap[cn6], 514 | bob 515 | .boolQuery() 516 | .should([ 517 | cnQryMap[cn7], 518 | cnQryMap[cn8] 519 | ]) 520 | ]) 521 | ]) 522 | ]) 523 | ); 524 | }); 525 | } 526 | }); 527 | 528 | describe('5 levels', () => { 529 | for (let idx = 0; idx < 50; idx++) { 530 | const randCn = randCnGen(); 531 | const cn1 = randCn(), 532 | cn2 = randCn(), 533 | cn3 = randCn(), 534 | cn4 = randCn(); 535 | const cn5 = randCn(), 536 | cn6 = randCn(), 537 | cn7 = randCn(), 538 | cn8 = randCn(); 539 | const cn9 = randCn(), 540 | cn10 = randCn(); 541 | 542 | test(`(${cnNamesMap[cn1]} and ${cnNamesMap[cn2]}) 543 | or 544 | (${cnNamesMap[cn3]} and ${cnNamesMap[cn4]} 545 | and 546 | (${cnNamesMap[cn5]} or ${cnNamesMap[cn6]} 547 | or 548 | (${cnNamesMap[cn7]} and ${cnNamesMap[cn8]} 549 | and 550 | (${cnNamesMap[cn9]} or ${cnNamesMap[cn10]}) 551 | ) 552 | ) 553 | )`, () => { 554 | const qry = parse( 555 | muto 556 | .where() 557 | .or(muto.where(cnMap[cn1]).and(cnMap[cn2])) 558 | .or( 559 | muto 560 | .where(cnMap[cn3]) 561 | .and(cnMap[cn4]) 562 | .and( 563 | muto 564 | .where(cnMap[cn5]) 565 | .or(cnMap[cn6]) 566 | .or( 567 | muto 568 | .where(cnMap[cn7]) 569 | .and(cnMap[cn8]) 570 | .and( 571 | muto 572 | .where(cnMap[cn9]) 573 | .or(cnMap[cn10]) 574 | ) 575 | ) 576 | ) 577 | ) 578 | ); 579 | 580 | expect(qry).toBeInstanceOf(bob.BoolQuery); 581 | expect(qry).toEqual( 582 | bob 583 | .boolQuery() 584 | .should([ 585 | bob 586 | .boolQuery() 587 | .must([cnQryMap[cn1], cnQryMap[cn2]]), 588 | bob 589 | .boolQuery() 590 | .must([ 591 | cnQryMap[cn3], 592 | cnQryMap[cn4], 593 | bob 594 | .boolQuery() 595 | .should([ 596 | cnQryMap[cn5], 597 | cnQryMap[cn6], 598 | bob 599 | .boolQuery() 600 | .must([ 601 | cnQryMap[cn7], 602 | cnQryMap[cn8], 603 | bob 604 | .boolQuery() 605 | .should([ 606 | cnQryMap[cn9], 607 | cnQryMap[cn10] 608 | ]) 609 | ]) 610 | ]) 611 | ]) 612 | ]) 613 | ); 614 | }); 615 | 616 | test(`(${cnNamesMap[cn1]} or ${cnNamesMap[cn2]}) 617 | and 618 | (${cnNamesMap[cn3]} or ${cnNamesMap[cn4]} 619 | or 620 | (${cnNamesMap[cn5]} and ${cnNamesMap[cn6]} 621 | and 622 | (${cnNamesMap[cn7]} or ${cnNamesMap[cn8]} 623 | or 624 | (${cnNamesMap[cn9]} and ${cnNamesMap[cn10]}) 625 | ) 626 | ) 627 | )`, () => { 628 | const qry = parse( 629 | muto 630 | .where() 631 | .and(muto.where(cnMap[cn1]).or(cnMap[cn2])) 632 | .and( 633 | muto 634 | .where(cnMap[cn3]) 635 | .or(cnMap[cn4]) 636 | .or( 637 | muto 638 | .where(cnMap[cn5]) 639 | .and(cnMap[cn6]) 640 | .and( 641 | muto 642 | .where(cnMap[cn7]) 643 | .or(cnMap[cn8]) 644 | .or( 645 | muto 646 | .where(cnMap[cn9]) 647 | .and(cnMap[cn10]) 648 | ) 649 | ) 650 | ) 651 | ) 652 | ); 653 | 654 | expect(qry).toBeInstanceOf(bob.BoolQuery); 655 | expect(qry).toEqual( 656 | bob 657 | .boolQuery() 658 | .must([ 659 | bob 660 | .boolQuery() 661 | .should([cnQryMap[cn1], cnQryMap[cn2]]), 662 | bob 663 | .boolQuery() 664 | .should([ 665 | cnQryMap[cn3], 666 | cnQryMap[cn4], 667 | bob 668 | .boolQuery() 669 | .must([ 670 | cnQryMap[cn5], 671 | cnQryMap[cn6], 672 | bob 673 | .boolQuery() 674 | .should([ 675 | cnQryMap[cn7], 676 | cnQryMap[cn8], 677 | bob 678 | .boolQuery() 679 | .must([ 680 | cnQryMap[cn9], 681 | cnQryMap[cn10] 682 | ]) 683 | ]) 684 | ]) 685 | ]) 686 | ]) 687 | ); 688 | }); 689 | } 690 | }); 691 | }); 692 | 693 | it('throws error for expression with mixed `and`, `or`', () => { 694 | expect(() => 695 | parse( 696 | `${cnMap.numLt} and ${cnMap.strEq} or ${cnMap.bool} or ${ 697 | cnMap.exists 698 | } and ${cnMap.missing}` 699 | ) 700 | ).toThrow(); 701 | }); 702 | }); 703 | -------------------------------------------------------------------------------- /test/query-builder-def.test.js: -------------------------------------------------------------------------------- 1 | import * as bob from 'elastic-builder'; 2 | import qryBldrDef from '../src/query-builder-def'; 3 | import { 4 | conditions, 5 | cnNamesMap, 6 | cnQryMap, 7 | qryBldrArgs 8 | } from './helpers/condition-factory'; 9 | 10 | describe('query builder default', () => { 11 | describe('has definition for', () => { 12 | conditions.forEach(cn => { 13 | test(cnNamesMap[cn], () => { 14 | expect(qryBldrDef[cn]).toBeDefined(); 15 | expect(qryBldrDef[cn]).toBeInstanceOf(Function); 16 | }); 17 | }); 18 | }); 19 | 20 | it('has method for generating property key', () => { 21 | expect(qryBldrDef.propertyKey).toBeDefined(); 22 | expect(qryBldrDef.propertyKey).toBeInstanceOf(Function); 23 | }); 24 | 25 | describe('build condition', () => { 26 | conditions.forEach(cn => { 27 | test(cnNamesMap[cn], () => { 28 | const qry = qryBldrDef[cn](...qryBldrArgs[cn]); 29 | 30 | expect(qry).toBeInstanceOf(cnQryMap[cn].constructor); 31 | expect(qry).toEqual(cnQryMap[cn]); 32 | }); 33 | }); 34 | 35 | test('string equals for not analyzed field', () => { 36 | const qry = qryBldrDef.strEq(...qryBldrArgs.strEqNotAnalyzed); 37 | 38 | expect(qry).toBeInstanceOf(bob.TermQuery); 39 | expect(qry).toEqual(cnQryMap.strEqNotAnalyzed); 40 | }); 41 | 42 | test('string not equals for not analyzed field', () => { 43 | const qry = qryBldrDef.strNe(...qryBldrArgs.strNeNotAnalyzed); 44 | expect(qry).toBeInstanceOf(bob.BoolQuery); 45 | expect(qry).toEqual(cnQryMap.strNeNotAnalyzed); 46 | }); 47 | }); 48 | 49 | it('builds the property key from char array', () => { 50 | expect(qryBldrDef.propertyKey('blistering_barnacles'.split(''))).toBe( 51 | 'blistering_barnacles' 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/where.test.js: -------------------------------------------------------------------------------- 1 | import * as muto from '../src'; 2 | 3 | const { Where, Condition } = muto; 4 | 5 | const cn1 = new Condition('anime').notContains('fillers'); 6 | const cn2 = new Condition('elasticsearch').eq('awesome'); 7 | const strCn1 = '["one_piece"] exists'; 8 | const strCn2 = '["awesome"] is true'; 9 | 10 | describe('Where builder', () => { 11 | it('can be instantiated with a condition', () => { 12 | const where = new Where(strCn1).build(); 13 | expect(where).toBe(`(${strCn1})`); 14 | }); 15 | 16 | it('aliases work as expected', () => { 17 | const where = new Where(strCn1); 18 | expect(muto.where(strCn1)).toEqual(where); 19 | }); 20 | 21 | it('calls build on condition objects', () => { 22 | const where = new Where(cn1); 23 | 24 | const spy = jest.spyOn(cn1, 'build'); 25 | where.build(); 26 | expect(spy).toHaveBeenCalled(); 27 | 28 | spy.mockReset(); 29 | spy.mockRestore(); 30 | }); 31 | 32 | it('can handle both string and object conditions', () => { 33 | const where = new Where(strCn2).and(cn1).build(); 34 | expect(where).toBe(`(${strCn2} and ${cn1.build()})`); 35 | }); 36 | 37 | it('throws error if both and, or are called', () => { 38 | expect(() => new Where().and(cn1).or(cn2)).toThrowError( 39 | 'Illegal operation! Join types cannot be mixed!' 40 | ); 41 | }); 42 | 43 | it('can handle nested conditions', () => { 44 | const where = new Where(cn1) 45 | .and(cn2) 46 | .and(new Where(strCn1).or(strCn2)) 47 | .build(); 48 | expect(where).toBe( 49 | `(${cn1.build()} and ${cn2.build()} and (${strCn1} or ${strCn2}))` 50 | ); 51 | }); 52 | 53 | it('delegates to the build function on calling toJSON', () => { 54 | const where = new Where(cn1); 55 | const spy = jest.spyOn(where, 'build'); 56 | where.toJSON(); 57 | 58 | expect(spy).toHaveBeenCalled(); 59 | 60 | spy.mockReset(); 61 | 62 | JSON.stringify(where); 63 | expect(spy).toHaveBeenCalled(); 64 | 65 | spy.mockReset(); 66 | spy.mockRestore(); 67 | }); 68 | 69 | it('delegates to the build function on calling toString', () => { 70 | const where = new Where(cn1); 71 | const spy = jest.spyOn(where, 'build'); 72 | where.toString(); 73 | 74 | expect(spy).toHaveBeenCalled(); 75 | 76 | spy.mockReset(); 77 | spy.mockRestore(); 78 | }); 79 | }); 80 | --------------------------------------------------------------------------------