├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.json ├── doc └── Intro.md ├── gulpfile.js ├── index.js ├── jest-config.js ├── package-lock.json ├── package.json ├── src └── odata-parser.js └── test ├── jest-pretest.js └── unit ├── odata-parser.spec.js ├── operator.spec.js └── predicate.spec.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true, 5 | "node": true 6 | }, 7 | 8 | "globals": { 9 | "jQuery": false, 10 | "$": false 11 | }, 12 | 13 | "rules": { 14 | "strict": 0, 15 | "no-underscore-dangle": 0, 16 | "quotes": [0, "double", "avoid-escape"], 17 | "indent": [2, 4], 18 | "brace-style": [2, "1tbs"], 19 | // make this one a two when closer to production 20 | "no-alert": 2, 21 | "no-var": 2 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | test/junit 5 | test/coverage 6 | .idea 7 | odata-filter-parser-*.tgz 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | /.ntvs_analysis.dat 4 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # odata-filter-parser 2 | 3 | Library for parsing and building and parsing OData filter strings. Only compatible with a subset of functions defined in OData specification 4 | 5 | * Supports OData specification V2 through V4 (partially) 6 | 7 | ## Using the Library 8 | Full API documentation and examples is available on the [Documentation](https://github.com/jadrake75/odata-filter-parser/blob/master/doc/Intro.md) page. 9 | 10 | To include the library in your application, you can either reference the `.js` files under the `dist` folder or use the JSPM / Node 11 | modular inclusion discussed on the [Documentation](https://github.com/jadrake75/odata-filter-parser/blob/master/doc/Intro.md) page. 12 | 13 | ### Compatibility 14 | It is required to use a supported ES6 level of JavaScript (supported by all current browsers and NodeJS supported versions) with 15 | version 0.4.0 or higher. 16 | 17 | 18 | ## Including the Library with Aurelia CLI 19 | The new Aurelia CLI will have difficulty resolving the dependencies from the require statements and try and resolve odata-filter under src vs the distribution folder. To resolve this, 20 | use the following configuration in the dependencies section of the aurelia.json file: 21 | 22 | ``` 23 | { 24 | "name": "odata-filter-parser", 25 | "path": "../node_modules/odata-filter-parser", 26 | "main": "index" 27 | } 28 | ``` 29 | 30 | ## Using the Library with TypeScript 31 | Currently no types are available for Predicate, Operators and Parser so they may need to be declared locally. Here is the workaround to do so in your project. 32 | 33 | * Add a directory under src called `@custom_types` if it doesn't exist 34 | * Create a `odata-filter-parser.d.ts` file with the following contents in this location 35 | 36 | ``` 37 | import { Predicate, Operators, Parser } from 'odata-filter-parser' 38 | 39 | export { Predicate, Operators } 40 | export default Parser 41 | ``` 42 | 43 | * Modify your compile options block (in a vuejs 3.x project this is likely the `tsconfig.app.json` file) to add the following (only showing the sections modified): 44 | 45 | ``` 46 | "compilerOptions": { 47 | "paths": { 48 | "*": ["src/@custom_types/*"] 49 | } 50 | }, 51 | "exclude": ["src/@custom_types/*"] 52 | ``` 53 | 54 | 55 | ## Dependencies 56 | This library has *no* third-party dependencies (outside of testing and building tools used by source). No additional software is required. 57 | 58 | ## Platform Support 59 | This library should work on all modern browsers that support HTML-5 EcmaScript 5 standard as well as V8 (used by NodeJS). 60 | 61 | ## Building The Library 62 | To build the code, follow these steps. 63 | 64 | * Ensure that [NodeJS](http://nodejs.org) is installed. This provides the platform on which the build tooling is run. 65 | * From the project folder, executue the following command: 66 | 67 | ``` 68 | npm install 69 | ``` 70 | * Ensure that [Gulp](http://gulpjs.com) is installed. If you need to install it, use the following command (however 71 | running `npm install` above should have installed a local copy): 72 | 73 | ``` 74 | npm install -g gulp 75 | ``` 76 | * To build the code, you can not run: 77 | 78 | ``` 79 | gulp 80 | ``` 81 | * You will find the built code under the `dist` folder. 82 | * See `gulpfile.js` for other tasks related to the generating of the library. 83 | 84 | ## Running Tests 85 | 86 | To execute the tests with jest simply run 87 | 88 | ``` 89 | npm test 90 | ``` 91 | 92 | This will generate coverage information automatically. 93 | 94 | 95 | ## Submission Guidelines 96 | Pull-Requests will be used for accepting bug fixes or feature requests, however please contact the owner prior to proposing 97 | a pull-request for non-bug fixes to avoid unnecessary work and effort. All submissions should provide test coverage and 98 | conform with the eslint standards defined in the `.eslintrc` file. 99 | 100 | ## Deployment Information 101 | 102 | Ensure a proper version is designated in the package.json that matches the commit on github. 103 | 104 | Step 1. Update version in package.json 105 | 106 | Step 2. Commit changes to github 107 | 108 | Step 3. Create Tag of the release locally with 109 | 110 | ``` 111 | git tag -a -m "created tag " 112 | ``` 113 | 114 | Push tag to github 115 | 116 | ``` 117 | git push origin --tags 118 | ``` 119 | 120 | Step 4. Pack the solution for publishing 121 | 122 | ``` 123 | npm pack 124 | ``` 125 | 126 | Step 5. To deploy the module to npmjs use the following command (user access will be required) 127 | 128 | ``` 129 | npm publish 130 | ``` 131 | 132 | Step 6. Optionally create a release in github using the tag and attach the .tgz used to publish to npmjs 133 | 134 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env" 5 | ] 6 | ], 7 | "plugins": [ 8 | 9 | ] 10 | } -------------------------------------------------------------------------------- /doc/Intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The odata-filter-parser library is a lightweight library used for the serialization and deserialization of odata `$filter` strings into a 4 | JavasScript object structure. This is used by several projects under the [stamp-web](http://github.com/stamp-web) organization both 5 | with NodeJS server modules (stamp-webservices) and client applications (stamp-web-aurelia). If others find it of use they are more than 6 | welcome to leverage it, but support will be limited. Please see the list of [Operators](#Operators) below that are supported as well as [Functions](#Functions) 7 |

8 | This library is compatible with the $filter syntax for V2 through V4 of OData but only supports as subset of functions and behaviors. 9 | 10 | #### Dependencies 11 | For dependency information see the note in the [README](https://github.com/jadrake75/odata-filter-parser/blob/master/README.md#Dependencies) 12 | 13 | #### Install via JSPM 14 | Assuming your project already supports npm and jspm, you can install the module via JSPM: 15 | 16 | ``` 17 | jpsm install odata-filter-parser 18 | ``` 19 | 20 | this will install the module into your `jspm_packages` folder and create any mappings in the `config.js` 21 | 22 | #### Install via NPM 23 | If you have a NodeJS project you can install the module directly from NPM: 24 | 25 | ``` 26 | npm install --save odata-filter-parser 27 | ``` 28 | 29 | this will install the module into the `node_modules` directory structure. 30 | 31 | # Reference 32 | Using the provided objects can be achieved using the require syntax. By default, the required module `odata-filter-parser` returns three objects: 33 | 34 | * *Operators* (singleton) 35 | * *Predicate* 36 | * *Parser* (singleton) 37 | 38 | Only the `Predicate` provides a constructor that can be used with `new Predicate()`. To include the module use the following syntax: 39 | 40 | ``` 41 | var odataFilter = require('odata-filter-parser'); 42 | ``` 43 | 44 | Or if only one of the objects is needed, you can access it directly: 45 | 46 | ``` 47 | var parser = require('odata-filter-parser').Parser; 48 | ``` 49 | 50 | 51 | 52 | ## Operators 53 | The following list of OData operators are supported by the parser and predicates 54 | 55 |

56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
Operator constant OData operator Comments
EQUALS eq equality evaluation
AND and logical and operation
OR or logical _or_ operation
GREATHER_THAN gt greater than evaluation exclusive
GREATER_THAN_EQUAL ge greater than evaluation inclusive
LESS_THAN lt less than evaluation exclusive
LESS_THAN_EQUAL le less than evaluation inclusive
LIKE like like expression
IS_NULL is null null or empty statement
NOT_EQUAL ne non-equality evaluation
69 | 70 | 71 | There are also a few functions available on Operators: 72 | 73 | ####isUnary() 74 | Whether the operator only involves one reference to return a boolean result. `Operators.IS_NULL` is an example of a unary operation. 75 | 76 | ####isLogical() 77 | Whether the operator is used for logical "gate" operations such as `Operators.AND` and `Operators.OR` 78 | 79 | ## Predicate 80 | The Predicate is a simple object with particular values and provides some useful utility functions for operating on the Predicate. 81 | 82 | ####serialize() 83 | This is the primary use-case of the predicate which is the facilitate the serialization of the Predicate to a value OData filter string. 84 | For compatibility all predicates will be wrapped by `()` to ensure cleaner processing and parsing by this library or other libraries. 85 | 86 | ##### Example serializing a simple predicate 87 | ``` 88 | var p = new Predicate( { 89 | subject: 'name', 90 | operator: Operators.EQUALS, 91 | value: 'Jerry' 92 | }); 93 | var s = p.serialize(); 94 | ``` 95 | will result in `s` being the value `(name eq 'Jerry')` 96 | 97 | ####flatten() 98 | The flatten function will take a Predicate and will return and array of Predicates. If the Predicate represents a logical operator such as `Operators.OR` or 99 | `Operators.AND` it will use the subject and value of these predicates in a recursive fashion. 100 | 101 | ##### Example flattening a simple structure with a logical operator 102 | ``` 103 | { 104 | subject: { 105 | subject: 'name', 106 | operator: 'eq', 107 | value: 'Serena' 108 | }, 109 | operator: 'and', 110 | value: { 111 | subject: 'age', 112 | operator: 'lt', 113 | value: 5 114 | } 115 | } 116 | ``` 117 | will result in the following output (where each object in the array is a Predicate): 118 | ``` 119 | [ 120 | { 121 | subject: 'name', 122 | operator: 'eq', 123 | value: 'Serena' 124 | }, { 125 | subject: 'age', 126 | operator: 'lt', 127 | value: 5 128 | } 129 | ] 130 | ``` 131 | 132 | ## Parser 133 | The parser is used to convert a string value representing a OData $filter string and convert this into a valid [Predicate](#Predicate) object (structure). 134 | 135 | ####parse(filter:string) 136 | Will parse the string and convert it to a `Predicate` object. The object returned will be nested based on the structure of the $filter string. 137 | If the filter string is null or empty, a value of `null` will be returned. When a predicate is created from the filter string, if the subject is not another 138 | predicate it will be encoded as a string. The value will be encoded either as a Predicate (for logical operations), a string, number or boolean. 139 | 140 | ##### Example parsing a simple expression 141 | Given a string `name eq 'Bob` the parse will result in a Predicate that looks like the following: 142 | ``` 143 | { 144 | subject: 'name', 145 | operator: 'eq', 146 | value: 'Bob' 147 | } 148 | ``` 149 | 150 | ##### Example parsing a more complex expression 151 | Given the string `((name eq 'Serena') and (age lt 5))` the resulting Predicate looks like: 152 | ``` 153 | { 154 | subject: { 155 | subject: 'name', 156 | operator: 'eq', 157 | value: 'Serena' 158 | }, 159 | operator: 'and', 160 | value: { 161 | subject: 'age', 162 | operator: 'lt', 163 | value: 5 164 | } 165 | } 166 | ``` -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Jason Drake 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var gulp = require('gulp'); 18 | var eslint = require('gulp-eslint'); 19 | var uglify = require('gulp-uglify'); 20 | var concat = require('gulp-concat'); 21 | var jest = require('@jest/core'); 22 | var packageJson = require('./package.json'); 23 | 24 | var DESTINATION = 'dist'; 25 | 26 | async function testSrc() { 27 | const testResults = await jest.runCLI({json: false, config: 'jest-config.js'},['test']) 28 | const { results } = testResults 29 | const isTestFailed = !results.success; 30 | if (isTestFailed) { 31 | console.log('You have some failed tests') 32 | process.exit() // Breaks Gulp Pipe 33 | } 34 | return; 35 | } 36 | 37 | gulp.task('compress', function() { 38 | return gulp.src('src/*.js') 39 | .pipe(concat('odata-parser-min.js')) 40 | .pipe(uglify()) 41 | .pipe(gulp.dest(DESTINATION)); 42 | }); 43 | 44 | gulp.task('copy', function() { 45 | return gulp.src('src/**') 46 | .pipe(concat('odata-parser.js')) 47 | .pipe(gulp.dest(DESTINATION)); 48 | }); 49 | 50 | gulp.task('eslint', function () { 51 | return gulp.src('src/**') 52 | .pipe(eslint({ 53 | configFile: '.eslintrc.json' 54 | })) 55 | .pipe(eslint.format()); 56 | }); 57 | 58 | gulp.task('test', gulp.series(testSrc)); 59 | 60 | 61 | gulp.task('default', gulp.series('eslint', 'test', 'copy', 'compress')); 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Jason Drake 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | Predicate: require('./dist/odata-parser').Predicate, 19 | Operators: require('./dist/odata-parser').Operators, 20 | Parser: require('./dist/odata-parser').Parser 21 | } -------------------------------------------------------------------------------- /jest-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "modulePaths": [ 3 | "/src", 4 | "/node_modules" 5 | ], 6 | "moduleFileExtensions": [ 7 | "js", 8 | "json" 9 | ], 10 | "transform": { 11 | "^.+\\.(js)$": "babel-jest" 12 | }, 13 | "testRegex": "\\.spec\\.js$", 14 | "setupFiles": [ 15 | "/test/jest-pretest.js" 16 | ], 17 | "testEnvironment": "node", 18 | "collectCoverage": true, 19 | "reporters": [ 20 | "default", 21 | [ 22 | "jest-junit", 23 | { 24 | "outputDirectory": "test/junit", 25 | "outputName": "TESTS.xml" 26 | } 27 | ] 28 | ], 29 | "collectCoverageFrom": [ 30 | "src/**/*.js", 31 | "!**/node_modules/**", 32 | "!**/test/**" 33 | ], 34 | "coverageDirectory": "/coverage", 35 | "coverageReporters": [ 36 | "json", 37 | "lcov", 38 | "html" 39 | ] 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odata-filter-parser", 3 | "version": "0.5.5", 4 | "description": "Library for parsing and building OData filter strings", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "gulp test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jadrake75/odata-filter-parser.git" 12 | }, 13 | "keywords": [ 14 | "odata" 15 | ], 16 | "author": "Jason Drake ", 17 | "license": "Apache-2.0", 18 | "bugs": { 19 | "url": "https://github.com/jadrake75/odata-filter-parser/issues" 20 | }, 21 | "files": [ 22 | "dist/**", 23 | "doc/**", 24 | "README.md", 25 | "package.json", 26 | "LICENSE", 27 | "index.js" 28 | ], 29 | "homepage": "https://github.com/jadrake75/odata-filter-parser#readme", 30 | "devDependencies": { 31 | "@babel/core": "^7.20.12", 32 | "@babel/eslint-parser": "^7.19.1", 33 | "@babel/plugin-proposal-class-properties": "^7.18.6", 34 | "@babel/plugin-proposal-decorators": "^7.20.7", 35 | "@babel/preset-env": "^7.20.2", 36 | "babel-jest": "^29.3.1", 37 | "expect.js": "^0.3.1", 38 | "gulp": "^4.0.2", 39 | "gulp-babel": "^8.0.0", 40 | "gulp-concat": "^2.6.0", 41 | "gulp-eslint": "^6.0.0", 42 | "gulp-uglify": "^3.0.2", 43 | "jest": "^29.3.1", 44 | "jest-cli": "^29.3.1", 45 | "jest-createspyobj": "^2.0.0", 46 | "jest-junit": "^15.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/odata-parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Jason Drake 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const Operators = { 18 | EQUALS: 'eq', 19 | AND: 'and', 20 | OR: 'or', 21 | GREATER_THAN: 'gt', 22 | GREATER_THAN_EQUAL: 'ge', 23 | LESS_THAN: 'lt', 24 | LESS_THAN_EQUAL: 'le', 25 | LIKE: 'like', 26 | IS_NULL: 'is null', 27 | NOT_EQUAL: 'ne', 28 | 29 | /** 30 | * Whether a defined operation is unary or binary. Will return true 31 | * if the operation only supports a subject with no value. 32 | * 33 | * @param {String} op the operation to check. 34 | * @return {Boolean} whether the operation is an unary operation. 35 | */ 36 | isUnary: function (op) { 37 | let value = false; 38 | if (op === Operators.IS_NULL) { 39 | value = true; 40 | } 41 | return value; 42 | }, 43 | /** 44 | * Whether a defined operation is a logical operators or not. 45 | * 46 | * @param {String} op the operation to check. 47 | * @return {Boolean} whether the operation is a logical operation. 48 | */ 49 | isLogical: function (op) { 50 | return (op === Operators.AND || op === Operators.OR); 51 | } 52 | }; 53 | 54 | const Functions = { 55 | CONTAINS: 'contains', 56 | STARTSWITH: 'startswith', 57 | ENDSWIDTH: 'endswith' 58 | }; 59 | 60 | /** 61 | * Predicate is the basic model construct of the odata expression 62 | * 63 | * @param config 64 | * @returns {Predicate} 65 | * @constructor 66 | */ 67 | const Predicate = function (config) { 68 | if (!config) { 69 | config = {}; 70 | } 71 | this.subject = config.subject; 72 | this.value = config.value; 73 | this.operator = (config.operator) ? config.operator : Operators.EQUALS; 74 | return this; 75 | }; 76 | 77 | Predicate.concat = function (operator, p) { 78 | if (arguments.length < 3 && !(p instanceof Array && p.length >= 2)) { 79 | throw { 80 | key: 'INSUFFICIENT_PREDICATES', 81 | msg: 'At least two predicates are required' 82 | }; 83 | } else if (!operator || !Operators.isLogical(operator)) { 84 | throw { 85 | key: 'INVALID_LOGICAL', 86 | msg: 'The operator is not representative of a logical operator.' 87 | }; 88 | } 89 | let result; 90 | let arr = []; 91 | if (p instanceof Array) { 92 | arr = p; 93 | } else { 94 | for (let i = 1; i < arguments.length; i++) { 95 | arr.push(arguments[i]); 96 | } 97 | } 98 | const len = arr.length; 99 | result = new Predicate({ 100 | subject: arr[0], 101 | operator: operator 102 | }); 103 | if (len === 2) { 104 | result.value = arr[len - 1]; 105 | } else { 106 | let a = []; 107 | for (let j = 1; j < len; j++) { 108 | a.push(arr[j]); 109 | } 110 | result.value = Predicate.concat(operator, a); 111 | } 112 | return result; 113 | }; 114 | 115 | Predicate.prototype.flatten = function (result) { 116 | if (!result) { 117 | result = []; 118 | } 119 | if (Operators.isLogical(this.operator)) { 120 | result = result.concat(this.subject.flatten()); 121 | result = result.concat(this.value.flatten()); 122 | } else { 123 | result.push(this); 124 | } 125 | return result; 126 | }; 127 | 128 | /** 129 | * Will serialie the predicate to an ODATA compliant serialized string. 130 | * 131 | * @return {String} The compliant ODATA query string 132 | */ 133 | Predicate.prototype.serialize = function () { 134 | let retValue = ''; 135 | if (this.operator) { 136 | if (this.subject === undefined || this.subject === null) { 137 | throw { 138 | key: 'INVALID_SUBJECT', 139 | msg: 'The subject is required and is not specified.' 140 | }; 141 | } 142 | if (Operators.isLogical(this.operator) && (!(this.subject instanceof Predicate || 143 | this.value instanceof Predicate) || (this.subject instanceof Predicate && this.value === undefined))) { 144 | throw { 145 | key: 'INVALID_LOGICAL', 146 | msg: 'The predicate does not represent a valid logical expression.' 147 | }; 148 | } 149 | retValue = '('; 150 | if (this.operator === Operators.LIKE) { 151 | let op = Functions.CONTAINS; 152 | const lastIndex = this.value.lastIndexOf('*'); 153 | const index = this.value.indexOf('*'); 154 | let v = this.value; 155 | if (index === 0 && lastIndex !== this.value.length - 1) { 156 | op = 'endswith'; 157 | v = v.substring(1); 158 | } else if (lastIndex === this.value.length - 1 && index === lastIndex) { 159 | op = 'startswith'; 160 | v = v.substring(0, lastIndex); 161 | } else if (index === 0 && lastIndex === this.value.length - 1) { 162 | v = v.substring(1, lastIndex); 163 | } 164 | retValue += op + '(' + this.subject + ',\'' + v + '\')'; 165 | } else { 166 | retValue += ((this.subject instanceof Predicate) ? this.subject.serialize() : this.subject) + ' ' + this.operator; 167 | 168 | if (!Operators.isUnary(this.operator)) { 169 | if (this.value === undefined || this.value === null) { 170 | throw { 171 | key: 'INVALID_VALUE', 172 | msg: 'The value was required but was not defined.' 173 | }; 174 | } 175 | retValue += ' '; 176 | const val = typeof this.value; 177 | if (val === 'string') { 178 | retValue += '\'' + this.value + '\''; 179 | } else if (val === 'number' || val === 'boolean') { 180 | retValue += this.value; 181 | } else if (this.value instanceof Predicate) { 182 | retValue += this.value.serialize(); 183 | } else if (this.value instanceof Date) { 184 | retValue += 'datetimeoffset\'' + this.value.toISOString() + '\''; 185 | } else { 186 | throw { 187 | key: 'UNKNOWN_TYPE', 188 | msg: 'Unsupported value type: ' + (typeof this.value), 189 | source: this.value 190 | }; 191 | } 192 | } 193 | } 194 | 195 | retValue += ')'; 196 | } 197 | return retValue; 198 | }; 199 | 200 | const ODataParser = function () { 201 | 202 | "use strict"; 203 | 204 | const KEY_REGEX = /^([$][0-9]+[$])$/g; 205 | 206 | const REGEX = { 207 | parenthesis: /^([(](.*)[)])$/, 208 | andor: /^(.*?) (or|and)+ (.*)$/, 209 | op: /(\w*) (eq|gt|lt|ge|le|ne) (datetimeoffset'(.*)'|'(.*)'|[0-9]*)/, 210 | isnull: /^(.*?) (is null)$/, 211 | startsWith: /^startswith[(](.*),\s*'(.*)'[)]/, 212 | endsWith: /^endswith[(](.*),\s*'(.*)'[)]/, 213 | contains: /^contains[(](.*),\s*'(.*)'[)]/ 214 | 215 | }; 216 | 217 | function buildLike(match, key) { 218 | let right = (key === 'startsWith') ? match[2] + '*' : (key === 'endsWith') ? '*' + match[2] : '*' + match[2] + '*'; 219 | return new Predicate({ 220 | subject: match[1], 221 | operator: Operators.LIKE, 222 | value: right 223 | }); 224 | } 225 | 226 | function parseFragment(filter) { 227 | let found = false; 228 | let obj = null; 229 | for (let key in REGEX) { 230 | const regex = REGEX[key]; 231 | if (found) { 232 | break; 233 | } 234 | let match = filter.match(regex); 235 | if (match) { 236 | switch (regex) { 237 | case REGEX.parenthesis: 238 | return parseNested(filter); 239 | break; 240 | case REGEX.andor: 241 | let subject = /(\$[0-9]+\$)/.test(match[1]) ? match[1] : parseFragment(match[1]); 242 | let value = /(\$[0-9]+\$)/.test(match[3]) ? match[3] : parseFragment(match[3]); 243 | obj = new Predicate({ 244 | subject: subject, 245 | operator: match[2], 246 | value: value 247 | }); 248 | break; 249 | case REGEX.op: 250 | obj = new Predicate({ 251 | subject: match[1], 252 | operator: match[2], 253 | value: (match[3].indexOf('\'') === -1) ? +match[3] : match[3] 254 | }); 255 | if (typeof obj.value === 'string') { 256 | const quoted = obj.value.match(/^'(.*)'$/); 257 | const m = obj.value.match(/^datetimeoffset'(.*)'$/); 258 | if (quoted && quoted.length > 1) { 259 | obj.value = quoted[1]; 260 | } else if (m && m.length > 1) { 261 | obj.value = new Date(m[1]); 262 | } 263 | } 264 | break; 265 | case REGEX.isnull: 266 | obj = new Predicate({ 267 | subject: match[1], 268 | operator: match[2] 269 | }) 270 | break; 271 | case REGEX.startsWith: 272 | case REGEX.endsWith: 273 | case REGEX.contains: 274 | obj = buildLike(match, key); 275 | break; 276 | } 277 | found = true; 278 | } 279 | } 280 | return obj; 281 | } 282 | 283 | function parseNested(filter) { 284 | const expressions = {}; 285 | 286 | const handleSubstitutions = (key, filterSubstring) => { 287 | let subsituted = false; 288 | // the expression by key is a reference another expression 289 | if (!expressions[key] && filterSubstring.match(KEY_REGEX)) { 290 | expressions[key] = expressions[filterSubstring]; 291 | subsituted = true; 292 | } else { 293 | handleExpressionCascade(key, 'subject'); 294 | handleExpressionCascade(key, 'value'); 295 | } 296 | if (!subsituted) { 297 | const match = filterSubstring.match(KEY_REGEX); 298 | if (match && match.length === 2) { 299 | expressions[key].subject = expressions[match[0]]; 300 | expressions[key].value = expressions[match[1]]; 301 | } else if (match && match.length == 1) { 302 | if (filterSubstring.indexOf('$') === 0) { 303 | expressions[key].subject = expressions[match[0]]; 304 | } else { 305 | expressions[key].value = expressions[match[0]]; 306 | } 307 | } 308 | } 309 | }; 310 | 311 | /** 312 | * Cascade a parameter of an expression to a matched expression 313 | * @param key - the key in the cache 314 | * @param param - the parameter key (subject|value) 315 | */ 316 | const handleExpressionCascade = (key, param) => { 317 | let match = (expressions[key] && typeof expressions[key][param] === 'string') ? expressions[key][param].match(KEY_REGEX) : undefined; 318 | if (match && match.length == 1) { 319 | expressions[key][param] = expressions[match[0]]; 320 | } 321 | }; 322 | 323 | /** 324 | * Determine if the opening parathensis belongs to a function such as 325 | * "contains(" or "startswith(" 326 | * 327 | * @param str 328 | * @param i 329 | * @returns {boolean} 330 | */ 331 | const isParenthesisForFunction = (str, i) => { 332 | let retVal = false; 333 | Object.keys(Functions).forEach(fn => { 334 | const fnLen = Functions[fn].length; 335 | retVal = retVal || (i > fnLen && str.substring(i - fnLen, i) === Functions[fn]); 336 | }); 337 | return retVal; 338 | }; 339 | 340 | while (filter.indexOf('(') !== -1) { 341 | let i, leftParenthesisIndex = 0; 342 | let isInsideQuotes = false; 343 | let isInsideFunction = false; 344 | for (i = 0; i < filter.length; i++) { 345 | if (filter[i] === '\'') { 346 | isInsideQuotes = !isInsideQuotes; 347 | continue; 348 | } else if (!isInsideQuotes) { 349 | if (filter[i] === '(') { 350 | if (isParenthesisForFunction(filter, i)) { 351 | isInsideFunction = true; 352 | continue; 353 | } 354 | leftParenthesisIndex = i; 355 | } else if (filter[i] === ')') { 356 | const filterSubstring = filter.substring(leftParenthesisIndex + 1, (isInsideFunction ? i + 1 : i)); 357 | if (isInsideFunction) { 358 | leftParenthesisIndex++; // need to include full parenthesis 359 | isInsideFunction = false; 360 | } 361 | const key = `$${Object.keys(expressions).length}$`; 362 | expressions[key] = parseFragment(filterSubstring); 363 | handleSubstitutions(key, filterSubstring); 364 | filter = `${filter.substring(0, leftParenthesisIndex)}${key}${filter.substring(i + 1)}`; 365 | break; 366 | } 367 | } 368 | if (i === filter.length - 1) { 369 | throw { 370 | key: 'INVALID_FILTER_STRING', 371 | msg: 'The given string has uneven number of parenthesis' 372 | }; 373 | } 374 | } 375 | } 376 | return expressions[`$${Object.keys(expressions).length - 1}$`]; 377 | } 378 | 379 | return { 380 | parse: function (filterStr) { 381 | if (!filterStr || filterStr === '') { 382 | return null; 383 | } 384 | let filter = filterStr.trim(); 385 | let obj = {}; 386 | if (filter.length > 0) { 387 | obj = parseFragment(filter); 388 | } 389 | return obj; 390 | } 391 | }; 392 | }(); 393 | 394 | module.exports = { 395 | Parser: ODataParser, 396 | Operators: Operators, 397 | Predicate: Predicate 398 | }; 399 | -------------------------------------------------------------------------------- /test/jest-pretest.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jadrake75/odata-filter-parser/c555f980b020927788d04aa6a60b7029124b8d31/test/jest-pretest.js -------------------------------------------------------------------------------- /test/unit/odata-parser.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyvalue 2022 Jason Drake 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var parser = require("../../src/odata-parser").Parser; 18 | var Predicate = require("../../src/odata-parser").Predicate; 19 | 20 | describe('ODataParser Tests', done => { 21 | 22 | describe('OData parsing tests', () => { 23 | 24 | it('Null string is not parsed', () => { 25 | expect(parser.parse(null)).toBe(null); 26 | }); 27 | 28 | it('Empty string is not parsed', () => { 29 | expect(parser.parse('')).toBe(null); 30 | }); 31 | 32 | it('Is null expression', () => { 33 | var obj = parser.parse('name is null') 34 | expect(obj.subject).toEqual("name") 35 | expect(obj.operator).toEqual("is null") 36 | expect(obj.value).toBeUndefined() 37 | }) 38 | 39 | it('Simple binary expression test', () => { 40 | var s = "name eq 'test'"; 41 | var obj = parser.parse(s); 42 | expect(obj.subject).toEqual("name"); 43 | expect(obj.operator).toEqual("eq"); 44 | expect(obj.value).toEqual("test"); 45 | }); 46 | 47 | it('Simple binary expression with String with spaces', () => { 48 | var s = "name eq 'test of strings'"; 49 | var obj = parser.parse(s); 50 | expect(obj.subject).toEqual("name"); 51 | expect(obj.operator).toEqual("eq"); 52 | expect(obj.value).toEqual("test of strings"); 53 | }); 54 | 55 | it('Simple binary expression with a number value', () => { 56 | var s = "id gt 5"; 57 | var obj = parser.parse(s); 58 | expect(obj.subject).toEqual("id"); 59 | expect(obj.operator).toEqual("gt"); 60 | expect(new Number(obj.value)).toBeTruthy(); 61 | expect(obj.value).toEqual(5); 62 | }); 63 | 64 | it( 65 | 'Simple binary expression with a number value enclosed with parenthesis', 66 | () => { 67 | var s = "(id lt 5)"; 68 | var obj = parser.parse(s); 69 | expect(obj.subject).toEqual("id"); 70 | expect(obj.operator).toEqual("lt"); 71 | expect(new Number(obj.value)).toBeTruthy(); 72 | expect(obj.value).toEqual(5); 73 | } 74 | ); 75 | 76 | it('Simple binary expression with text containing parenthesis', () => { 77 | var s = "name eq 'ultramarine (R)'"; 78 | var obj = parser.parse(s); 79 | expect(obj.subject).toEqual("name"); 80 | expect(obj.operator).toEqual("eq"); 81 | expect(obj.value).toEqual("ultramarine (R)"); 82 | }); 83 | 84 | it( 85 | 'Simple binary expression with text containing parenthesis and bracketted parenthesis', 86 | () => { 87 | var s = "(name eq 'ultramarine (R)')"; 88 | var obj = parser.parse(s); 89 | expect(obj.subject).toEqual("name"); 90 | expect(obj.operator).toEqual("eq"); 91 | expect(obj.value).toEqual("ultramarine (R)"); 92 | } 93 | ); 94 | 95 | it( 96 | 'Compound binary expression with text containing parenthesis', 97 | () => { 98 | var s = "((name eq 'ultramarine (R)') and (rate eq '1d'))"; 99 | var obj = parser.parse(s); 100 | var subject = obj.subject; 101 | var value = obj.value; 102 | expect( obj.operator).toEqual("and"); 103 | expect(subject.subject).toEqual("name"); 104 | expect(subject.operator).toEqual("eq"); 105 | expect(subject.value).toEqual("ultramarine (R)"); 106 | expect(value.subject).toEqual("rate"); 107 | expect(value.operator).toEqual("eq"); 108 | expect(value.value).toEqual("1d"); 109 | } 110 | ); 111 | 112 | it('Compound binary expression with parenthesis on value', () => { 113 | var s = "((name eq 'Bob') and (id gt 5))"; 114 | var obj = parser.parse(s); 115 | var subject = obj.subject; 116 | var value = obj.value; 117 | expect(obj.operator).toEqual("and"); 118 | expect(subject.subject).toEqual("name"); 119 | expect(subject.operator).toEqual("eq"); 120 | expect(subject.value).toEqual("Bob"); 121 | expect(value.subject).toEqual("id"); 122 | expect(value.operator).toEqual("gt"); 123 | expect(value.value).toEqual(5); 124 | }); 125 | 126 | it('More complex multiple binary expressions', () => { 127 | var s = "(name eq 'Bob' and (lastName eq 'Smiley' and (weather ne 'sunny' or temp ge 54)))"; 128 | var obj = parser.parse(s); 129 | var subject = obj.subject; 130 | var value = obj.value; 131 | expect(obj.operator).toEqual("and"); 132 | expect(subject.subject).toEqual("name"); 133 | expect(subject.operator).toEqual("eq"); 134 | expect(subject.value).toEqual("Bob"); 135 | expect(value.operator).toEqual("and"); 136 | subject = value.subject; 137 | value = value.value; 138 | expect(subject.subject).toEqual("lastName"); 139 | expect(subject.operator).toEqual("eq"); 140 | expect(subject.value).toEqual("Smiley"); 141 | 142 | expect(value.operator).toEqual("or"); 143 | subject = value.subject; 144 | value = value.value; 145 | expect(subject.subject).toEqual("weather"); 146 | expect(subject.operator).toEqual("ne"); 147 | expect(subject.value).toEqual("sunny"); 148 | expect(value.subject).toEqual("temp"); 149 | expect(value.operator).toEqual("ge"); 150 | expect(value.value).toEqual(54); 151 | 152 | }); 153 | 154 | it('Nested binary expressions', () => { 155 | var s = "(((district eq 'all') or (district eq 'palilula')) and ((malfunctions eq 'true') or (maintenance eq 'true')))"; 156 | var obj = parser.parse(s); 157 | var subject = obj.subject; 158 | var value = obj.value; 159 | var subjectNestedSubject = subject.subject; 160 | var subjectNestedValue = subject.value; 161 | var valueNestedSubject = value.subject; 162 | var valueNestedValue = value.value; 163 | 164 | expect(obj.operator).toEqual("and"); 165 | expect(subject.operator).toEqual("or"); 166 | expect(value.operator).toEqual("or"); 167 | 168 | expect(subjectNestedSubject.subject).toEqual("district"); 169 | expect(subjectNestedSubject.value).toEqual("all"); 170 | expect(subjectNestedSubject.operator).toEqual("eq"); 171 | 172 | expect(subjectNestedValue.subject).toEqual("district"); 173 | expect(subjectNestedValue.value).toEqual("palilula"); 174 | expect(subjectNestedValue.operator).toEqual("eq"); 175 | 176 | expect(valueNestedSubject.subject).toEqual("malfunctions"); 177 | expect(valueNestedSubject.value).toEqual("true"); 178 | expect(valueNestedSubject.operator).toEqual("eq"); 179 | 180 | expect(valueNestedValue.subject).toEqual("maintenance"); 181 | expect(valueNestedValue.value).toEqual("true"); 182 | expect(valueNestedValue.operator).toEqual("eq"); 183 | }); 184 | 185 | it('Verify startsWith condition', () => { 186 | var s = "startswith(name,'Ja')"; 187 | var obj = parser.parse(s); 188 | expect( obj.subject).toEqual('name'); 189 | expect( obj.value).toEqual('Ja*'); 190 | expect( obj.operator).toEqual('like'); 191 | }) 192 | 193 | it('Verify endsWith condition', () => { 194 | var s = "endswith(name,'Hole')"; 195 | var obj = parser.parse(s); 196 | expect( obj.subject).toEqual('name'); 197 | expect( obj.value).toEqual('*Hole'); 198 | expect( obj.operator).toEqual('like'); 199 | }); 200 | 201 | it('Verify contains condition', () => { 202 | var s = "contains(name,'Something')"; 203 | var obj = parser.parse(s); 204 | expect( obj.subject).toEqual('name'); 205 | expect( obj.value).toEqual('*Something*'); 206 | expect( obj.operator).toEqual('like'); 207 | }); 208 | 209 | it('Verify function with space after comma in parenthesis', () => { 210 | var s = "contains(name, 'Some value')"; 211 | var obj = parser.parse(s); 212 | expect( obj.subject).toEqual('name'); 213 | expect( obj.value).toEqual('*Some value*'); 214 | expect( obj.operator).toEqual('like'); 215 | }); 216 | 217 | it('Verify function with spaces after comma in parenthesis', () => { 218 | var s = "contains(name, 'many spaces')"; 219 | var obj = parser.parse(s); 220 | expect( obj.subject).toEqual('name'); 221 | expect( obj.value).toEqual('*many spaces*'); 222 | expect( obj.operator).toEqual('like'); 223 | }); 224 | 225 | it('Verify compound expression with contains condition with spaces inside of parenthesis', () => { 226 | let s = "contains(name, 'value with spaces') and (name eq 'test')"; 227 | var obj = parser.parse(s); 228 | expect(obj.operator).toBe('and'); 229 | let containsPredicate = obj.subject; 230 | let eqPredicate = obj.value; 231 | expect(containsPredicate instanceof Predicate).toBe(true); 232 | expect(eqPredicate instanceof Predicate).toBe(true); 233 | expect( containsPredicate.subject).toEqual('name'); 234 | expect( containsPredicate.value).toEqual('*value with spaces*'); 235 | expect( eqPredicate.subject).toEqual('name'); 236 | expect( eqPredicate.value).toEqual('test'); 237 | expect( eqPredicate.operator).toEqual('eq'); 238 | }); 239 | 240 | it('Verify compound expression with contains condition', () => { 241 | var s = "contains(name,'Something') and (name eq 'test')"; 242 | var obj = parser.parse(s); 243 | expect(obj.operator).toBe('and'); 244 | let containsPredicate = obj.subject; 245 | let eqPredicate = obj.value; 246 | expect(containsPredicate instanceof Predicate).toBe(true); 247 | expect(eqPredicate instanceof Predicate).toBe(true); 248 | expect( containsPredicate.subject).toEqual('name'); 249 | expect( containsPredicate.value).toEqual('*Something*'); 250 | expect( eqPredicate.subject).toEqual('name'); 251 | expect( eqPredicate.value).toEqual('test'); 252 | expect( eqPredicate.operator).toEqual('eq'); 253 | }); 254 | 255 | it('Verify compound expression with contains condition wrapped', () => { 256 | var s = "(contains(name,'Something') and (name eq 'test'))"; 257 | var obj = parser.parse(s); 258 | expect(obj.operator).toBe('and'); 259 | let containsPredicate = obj.subject; 260 | let eqPredicate = obj.value; 261 | expect(containsPredicate instanceof Predicate).toBe(true); 262 | expect(eqPredicate instanceof Predicate).toBe(true); 263 | expect( containsPredicate.subject).toEqual('name'); 264 | expect( containsPredicate.value).toEqual('*Something*'); 265 | expect( eqPredicate.subject).toEqual('name'); 266 | expect( eqPredicate.value).toEqual('test'); 267 | expect( eqPredicate.operator).toEqual('eq'); 268 | }); 269 | 270 | it('Verify compound expression with contains condition wrapped twice', () => { 271 | var s = "((contains(name,'Something')) and (name eq 'test'))"; 272 | var obj = parser.parse(s); 273 | expect(obj.operator).toBe('and'); 274 | let containsPredicate = obj.subject; 275 | let eqPredicate = obj.value; 276 | expect(containsPredicate instanceof Predicate).toBe(true); 277 | expect(eqPredicate instanceof Predicate).toBe(true); 278 | expect( containsPredicate.subject).toEqual('name'); 279 | expect( containsPredicate.value).toEqual('*Something*'); 280 | expect( eqPredicate.subject).toEqual('name'); 281 | expect( eqPredicate.value).toEqual('test'); 282 | expect( eqPredicate.operator).toEqual('eq'); 283 | }); 284 | 285 | it('Verify like operations return a Predicate', () => { 286 | var s = "contains(name,'predName')"; 287 | var obj = parser.parse(s); 288 | expect( obj instanceof Predicate).toBe(true); 289 | }); 290 | 291 | it('Parse datetimeoffset value', () => { 292 | var s = "(purchased le datetimeoffset'2015-12-06T05:00:00.000Z')"; 293 | var obj = parser.parse(s); 294 | expect( obj.subject).toEqual('purchased'); 295 | expect( obj.value).toEqual(new Date('2015-12-06T05:00:00.000Z')); 296 | expect( obj.operator).toEqual('le'); 297 | }); 298 | 299 | it('Verify compound wrapped expressions', () => { 300 | let s = "((name eq 'Drew Xiu') and (age gt 5))"; 301 | var obj = parser.parse(s); 302 | expect(obj.operator).toBe('and'); 303 | let namePredicate = obj.subject; 304 | let agePredicate = obj.value; 305 | expect(namePredicate instanceof Predicate).toBe(true); 306 | expect(agePredicate instanceof Predicate).toBe(true); 307 | expect( namePredicate.subject).toEqual('name'); 308 | expect( namePredicate.value).toEqual('Drew Xiu'); 309 | expect( namePredicate.operator).toEqual('eq'); 310 | expect( agePredicate.subject).toEqual('age'); 311 | expect( agePredicate.value).toBe(5); 312 | expect( agePredicate.operator).toEqual('gt'); 313 | }); 314 | 315 | it('Mismatched paranthesis fails at start', () => { 316 | var s = "(((((value ge 5)))"; 317 | try { 318 | var obj = parser.parse(s); 319 | } catch(e) { 320 | expect(e.key).toBe('INVALID_FILTER_STRING'); 321 | } 322 | }); 323 | 324 | it('Mismatched paranthesis fails at end', () => { 325 | var s = "value eq ')')"; 326 | try { 327 | var obj = parser.parse(s); 328 | } catch(e) { 329 | expect(e.key).toBe('INVALID_FILTER_STRING'); 330 | } 331 | }); 332 | }); 333 | 334 | }); 335 | -------------------------------------------------------------------------------- /test/unit/operator.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Jason Drake 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var operators = require("../../src/odata-parser").Operators; 18 | 19 | describe('Operator Tests', done => { 20 | 21 | describe('OData Operator tests', () => { 22 | 23 | it('Logical test for and', () => { 24 | var s = "and"; 25 | expect(s).toEqual(operators.AND); 26 | expect(operators.isLogical(s)).toBe(true); 27 | }); 28 | 29 | it('Logical test for or', () => { 30 | var s = "or"; 31 | expect(s).toEqual(operators.OR); 32 | expect(operators.isLogical(s)).toBe(true); 33 | }); 34 | 35 | it('Logical test not valid for eq', () => { 36 | var s = "eq"; 37 | expect(operators.isLogical(s)).toBe(false); 38 | }); 39 | 40 | it('Unary test for null', () => { 41 | var s = "is null"; 42 | expect(s).toEqual(operators.IS_NULL); 43 | expect(operators.isUnary(s)).toBe(true); 44 | }); 45 | 46 | it('Unary test for binary operation', () => { 47 | var s = "and"; 48 | expect(operators.isUnary(s)).toBe(false); 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /test/unit/predicate.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2015 Jason Drake 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var Predicate = require("../../src/odata-parser").Predicate; 18 | var Operators = require("../../src/odata-parser").Operators; 19 | 20 | describe('Predicate Tests', done => { 21 | 22 | describe('Predicate tests', () => { 23 | it('Empty constructor', () => { 24 | var p = new Predicate(); 25 | expect(p.operator).toBe(Operators.EQUALS); 26 | expect(p.subject).toBe(undefined); 27 | expect(p.value).toBe(undefined); 28 | }); 29 | }); 30 | 31 | describe('Predicate serialization tests', () => { 32 | 33 | it('Serialize a simple object', () => { 34 | var p = new Predicate({ 35 | subject: 'name', 36 | operator: Operators.EQUALS, 37 | value: 'Serena' 38 | }); 39 | expect(p.serialize()).toEqual("(name eq 'Serena')"); 40 | }); 41 | 42 | it('Serialize a simple logical set of objects', () => { 43 | var p = new Predicate({ 44 | subject: new Predicate({ 45 | subject: 'name', 46 | operator: Operators.EQUALS, 47 | value: 'Serena' 48 | }), 49 | operator: Operators.AND, 50 | value: new Predicate({ 51 | subject: 'lastname', 52 | operator: Operators.NOT_EQUAL, 53 | value: 'Martinez' 54 | }) 55 | }); 56 | expect(p.serialize()).toEqual("((name eq 'Serena') and (lastname ne 'Martinez'))"); 57 | }); 58 | 59 | it('Serialize boolean values', () => { 60 | var p = new Predicate({ 61 | subject: 'happy', 62 | operator: Operators.EQUALS, 63 | value: true 64 | }); 65 | expect(p.serialize()).toEqual("(happy eq true)"); 66 | }); 67 | 68 | it('Serialize numeric floating-point values', () => { 69 | var p = new Predicate({ 70 | subject: 'pi', 71 | operator: Operators.GREATER_THAN, 72 | value: 3.14159 73 | }); 74 | expect(p.serialize()).toEqual("(pi gt 3.14159)"); 75 | }); 76 | 77 | it('Serialize numeric integer values', () => { 78 | var p = new Predicate({ 79 | subject: 'age', 80 | operator: Operators.NOT_EQUAL, 81 | value: 30 82 | }); 83 | expect(p.serialize()).toEqual("(age ne 30)"); 84 | }); 85 | 86 | it('Serialize current date to ISO String', () => { 87 | var d = new Date(); 88 | var p = new Predicate({ 89 | subject: 'created', 90 | operator: Operators.GREATER_THAN, 91 | value: d 92 | }); 93 | expect(p.serialize()).toEqual("(created gt datetimeoffset'" + d.toISOString() + "')"); 94 | }); 95 | 96 | it('Serialize object value fails', () => { 97 | var p = new Predicate({ 98 | subject: 'created', 99 | operator: Operators.EQUALS, 100 | value: { a: "nice" } 101 | }); 102 | try { 103 | expect(p.serialize()); 104 | fail("Should have failed to serialize an object value"); 105 | } catch( err ) { 106 | expect(err).not.toBe(null); 107 | expect(err.key).toEqual('UNKNOWN_TYPE'); 108 | } 109 | }); 110 | 111 | it('Serialize null value fails', () => { 112 | var p = new Predicate({ 113 | subject: 'created', 114 | operator: Operators.EQUALS, 115 | value: null 116 | }); 117 | try { 118 | expect(p.serialize()); 119 | fail("Should have failed to serialize a null value"); 120 | } catch( err ) { 121 | expect(err).not.toBe(null); 122 | expect(err.key).toEqual('INVALID_VALUE'); 123 | } 124 | }); 125 | 126 | it('Serialize null subject fails', () => { 127 | var p = new Predicate({ 128 | subject: null, 129 | operator: Operators.EQUALS, 130 | value: 'foo' 131 | }); 132 | try { 133 | expect(p.serialize()); 134 | fail("Should have failed to serialize a null subject"); 135 | } catch( err ) { 136 | expect(err).not.toBe(null); 137 | expect(err.key).toEqual('INVALID_SUBJECT'); 138 | } 139 | }); 140 | 141 | it( 142 | 'Serialize logical expression where one side is a not at least a predicate fails', 143 | () => { 144 | var p = new Predicate({ 145 | subject: 'name', 146 | operator: Operators.AND, 147 | value: 'foo' 148 | }); 149 | try { 150 | expect(p.serialize()); 151 | fail("Should have failed to serialize a non-predicate value"); 152 | } catch( err ) { 153 | expect(err).not.toBe(null); 154 | expect(err.key).toEqual('INVALID_LOGICAL'); 155 | } 156 | } 157 | ); 158 | 159 | it('Serialize LIKE with no wildcard', () => { 160 | var p = new Predicate({ 161 | subject: 'name', 162 | operator: Operators.LIKE, 163 | value: 'Serena' 164 | }); 165 | var s = p.serialize(); 166 | expect(s).toBe('(contains(name,\'Serena\'))'); 167 | }); 168 | 169 | it('Serialize LIKE with wildcards', () => { 170 | var p = new Predicate({ 171 | subject: 'name', 172 | operator: Operators.LIKE, 173 | value: '*Some*' 174 | }); 175 | var s = p.serialize(); 176 | expect(s).toBe('(contains(name,\'Some\'))'); 177 | }); 178 | 179 | it('Serialize LIKE with starting wildcard', () => { 180 | var p = new Predicate({ 181 | subject: 'name', 182 | operator: Operators.LIKE, 183 | value: '*ending' 184 | }); 185 | var s = p.serialize(); 186 | expect(s).toBe('(endswith(name,\'ending\'))'); 187 | }); 188 | 189 | it('Serialize LIKE with ending wildcard', () => { 190 | var p = new Predicate({ 191 | subject: 'name', 192 | operator: Operators.LIKE, 193 | value: 'starting*' 194 | }); 195 | var s = p.serialize(); 196 | expect(s).toBe('(startswith(name,\'starting\'))'); 197 | }); 198 | 199 | it('Serialize LIKE with middle wildcard', () => { 200 | var p = new Predicate({ 201 | subject: 'name', 202 | operator: Operators.LIKE, 203 | value: 'start*end' 204 | }); 205 | var s = p.serialize(); 206 | expect(s).toBe('(contains(name,\'start*end\'))'); 207 | }); 208 | }); 209 | 210 | describe('Predicate concat tests', () => { 211 | 212 | it('Invalid case of a single predicate', () => { 213 | var p = new Predicate({ 214 | subject: 'name', 215 | operator: Operators.EQUALS, 216 | value: 'Serena' 217 | }); 218 | try { 219 | var result = Predicate.concat(Operators.AND, p); 220 | fail("expected error"); 221 | } catch( err ) { 222 | expect(err).not.toBe(null); 223 | expect(err.key).toEqual('INSUFFICIENT_PREDICATES'); 224 | } 225 | }); 226 | 227 | it('Invalid case with non-logical operator', () => { 228 | var p = new Predicate({ 229 | subject: 'name', 230 | operator: Operators.EQUALS, 231 | value: 'Serena' 232 | }); 233 | var p2 = new Predicate({ 234 | subject: 'age', 235 | operator: Operators.LESS_THAN, 236 | value: 5 237 | }); 238 | try { 239 | var result = Predicate.concat(Operators.LESS_THAN, p, p2); 240 | fail("expected error"); 241 | } catch( err ) { 242 | expect(err).not.toBe(null); 243 | expect(err.key).toEqual('INVALID_LOGICAL'); 244 | } 245 | }); 246 | 247 | it('Concatenate two simple predicates with AND', () => { 248 | var p = new Predicate({ 249 | subject: 'name', 250 | operator: Operators.EQUALS, 251 | value: 'Serena' 252 | }); 253 | var p2 = new Predicate({ 254 | subject: 'age', 255 | operator: Operators.LESS_THAN, 256 | value: 5 257 | }); 258 | var result = Predicate.concat(Operators.AND, p, p2); 259 | expect(result.subject).toBe(p); 260 | expect(result.value).toBe(p2); 261 | expect(result.operator).toBe(Operators.AND); 262 | }); 263 | 264 | it('Concatenate more than two predicates with OR', () => { 265 | var p = new Predicate({ 266 | subject: 'name', 267 | operator: Operators.EQUALS, 268 | value: 'Serena' 269 | }); 270 | var p2 = new Predicate({ 271 | subject: 'age', 272 | operator: Operators.LESS_THAN, 273 | value: 5 274 | }); 275 | var p3 = new Predicate({ 276 | subject: 'happiness', 277 | operator: Operators.EQUAL, 278 | value: 'high' 279 | }); 280 | var result = Predicate.concat(Operators.OR, p, p2,p3); 281 | expect(result.subject).toBe(p); 282 | expect(result.value).not.toBe(null); 283 | expect(result.operator).toBe(Operators.OR); 284 | var r = result.value; 285 | expect(r instanceof Predicate).toBe(true); 286 | expect(r.subject).toBe(p2); 287 | expect(r.operator).toBe(Operators.OR); 288 | expect(r.value).toBe(p3); 289 | }); 290 | 291 | it('Concatenate with an array of values', () => { 292 | var arr = []; 293 | for( var i = 0; i < 3; i++ ) { 294 | arr.push( new Predicate({ 295 | subject: 'name', 296 | operator: Operators.EQUALS, 297 | value: 'text-' + i 298 | })); 299 | } 300 | var result = Predicate.concat(Operators.OR, arr); 301 | expect(result.subject).toBe(arr[0]); 302 | expect(result.value).not.toBe(null); 303 | expect(result.operator).toBe(Operators.OR); 304 | var r = result.value; 305 | expect(r instanceof Predicate).toBe(true); 306 | expect(r.subject).toBe(arr[1]); 307 | expect(r.operator).toBe(Operators.OR); 308 | expect(r.value).toBe(arr[2]); 309 | }); 310 | 311 | }); 312 | 313 | describe('Predicate flatten tests', () => { 314 | 315 | it('Flatten single statement', () => { 316 | var s = new Predicate({ 317 | subject: "name", 318 | value: "'Bob'" 319 | }); 320 | var obj = s.flatten(); 321 | expect(obj.length).toEqual(1); 322 | expect(obj[0].subject).toEqual("name"); 323 | expect(obj[0].operator).toEqual("eq"); 324 | expect(obj[0].value).toEqual("'Bob'"); 325 | }); 326 | 327 | it('Flatten single statement into existing array', () => { 328 | var s = new Predicate({ 329 | subject: "name", 330 | value: "'Bob'" 331 | }); 332 | var r = []; 333 | s.flatten(r); 334 | expect(r.length).toEqual(1); 335 | expect(r[0].subject).toEqual("name"); 336 | expect(r[0].operator).toEqual("eq"); 337 | expect(r[0].value).toEqual("'Bob'"); 338 | }); 339 | 340 | it('Flatten two and statements', () => { 341 | var s = new Predicate({ 342 | subject: new Predicate({ 343 | subject: "name", 344 | operator: "eq", 345 | value: "'Bob'" 346 | }), 347 | operator: "and", 348 | value: new Predicate({ 349 | subject: "lastname", 350 | operator: "eq", 351 | value: "'someone'" 352 | }) 353 | }); 354 | 355 | var obj = s.flatten(); 356 | expect(obj.length).toEqual(2); 357 | var subject = obj[0]; 358 | expect(subject.subject).toEqual("name"); 359 | expect(subject.operator).toEqual("eq"); 360 | expect(subject.value).toEqual("'Bob'"); 361 | var value = obj[1]; 362 | expect(value.subject).toEqual("lastname"); 363 | expect(value.operator).toEqual("eq"); 364 | expect(value.value).toEqual("'someone'"); 365 | }); 366 | }); 367 | }); --------------------------------------------------------------------------------