├── .editorconfig ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── dist ├── LICENSE ├── README.md ├── builders │ ├── create-expand.d.ts │ ├── create-expand.js │ ├── create-expand.js.map │ ├── create-filter.d.ts │ ├── create-filter.js │ ├── create-filter.js.map │ ├── create-groupby.d.ts │ ├── create-groupby.js │ ├── create-groupby.js.map │ ├── create-orderby.d.ts │ ├── create-orderby.js │ ├── create-orderby.js.map │ ├── create-paginate.d.ts │ ├── create-paginate.js │ ├── create-paginate.js.map │ ├── create-query.d.ts │ ├── create-query.js │ ├── create-query.js.map │ ├── create-select.d.ts │ ├── create-select.js │ ├── create-select.js.map │ ├── index.d.ts │ ├── index.js │ ├── index.js.map │ ├── query-builder.d.ts │ ├── query-builder.js │ └── query-builder.js.map ├── index.d.ts ├── index.js ├── index.js.map ├── models │ ├── index.d.ts │ ├── index.js │ ├── index.js.map │ ├── odata-query.d.ts │ ├── odata-query.js │ ├── odata-query.js.map │ ├── query-descriptor.d.ts │ ├── query-descriptor.js │ ├── query-descriptor.js.map │ ├── query-expand.d.ts │ ├── query-expand.js │ ├── query-expand.js.map │ ├── query-filter.d.ts │ ├── query-filter.js │ ├── query-filter.js.map │ ├── query-groupby.d.ts │ ├── query-groupby.js │ ├── query-groupby.js.map │ ├── query-orderby.d.ts │ ├── query-orderby.js │ ├── query-orderby.js.map │ ├── query-select.d.ts │ ├── query-select.js │ └── query-select.js.map └── package.json ├── jest.config.js ├── package.json ├── src ├── builders │ ├── create-expand.ts │ ├── create-filter.ts │ ├── create-groupby.ts │ ├── create-orderby.ts │ ├── create-paginate.ts │ ├── create-query.ts │ ├── create-select.ts │ ├── index.ts │ └── query-builder.ts ├── index.ts └── models │ ├── index.ts │ ├── odata-query.ts │ ├── query-descriptor.ts │ ├── query-expand.ts │ ├── query-filter.ts │ ├── query-groupby.ts │ ├── query-orderby.ts │ └── query-select.ts ├── tests ├── data │ └── models.ts ├── expand.spec.ts ├── filter-functions.spec.ts ├── filter.spec.ts ├── groupby.spec.ts ├── orderby.spec.ts ├── paginate.spec.ts ├── query.spec.ts └── select.spec.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | branches: 3 | only: 4 | - master 5 | - /^greenkeeper/.*$/ 6 | cache: 7 | yarn: true 8 | directories: 9 | - node_modules 10 | notifications: 11 | email: false 12 | node_js: 13 | - node 14 | script: 15 | - npm run ci && npm run build -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Jest Tests", 11 | "cwd": "${workspaceFolder}", 12 | "args": [ 13 | "--inspect-brk", 14 | "${workspaceRoot}/node_modules/.bin/jest", 15 | "--runInBand", 16 | "--config", 17 | "${workspaceRoot}/jest.config.js" 18 | ], 19 | "windows": { 20 | "args": [ 21 | "--inspect-brk", 22 | "${workspaceRoot}/node_modules/jest/bin/jest.js", 23 | "--runInBand", 24 | "--config", 25 | "${workspaceRoot}/jest.config.js" 26 | ], 27 | }, 28 | "console": "internalConsole", 29 | "internalConsoleOptions": "neverOpen" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # odata-fluent-query 2 | 3 | **Client side queries with extensive filtering and typesafe joins** 4 | 5 | This lib only generates the query string, so you need to use it with your own implementation of http request. There is no need to scaffold any pre build model. 6 | 7 | - [Filtering with `filter`](#filtering-with-filter) 8 | - [Ordering with `orderBy`](#ordering-with-orderby) 9 | - [Selecting with `select`](#selecting-properties-with-select) 10 | - [Expanding with `expand`](#expanding-with-expand) 11 | - [Expanding with `groupBy`](#grouping-with-groupBy) 12 | - [Paginating with `paginate`](#paginating-with-paginate) 13 | - [Development](#development) 14 | 15 | ## Filtering with `filter` 16 | 17 | Every query exposes a method called `filter`. This method accepts a function as parameter that builds an expression. 18 | 19 | ```ts 20 | import { odataQuery } from 'odata-fluent-query' 21 | 22 | odataQuery() 23 | .filter(u => u.id.equals(1)) 24 | .toString() 25 | 26 | // result: $filter=id eq 1 27 | ``` 28 | 29 | Note that the parameter `u` is not a `User` type but `FilterBuider`. The `FilterBuider` will exposes all properties from `T` as `FilterBuilderType` to provide all filter functions based on its property type which can be: 30 | - `FilterCollection` 31 | - `FilterString` 32 | - `FilterNumber` 33 | - `FilterBoolean` 34 | - `FilterDate` 35 | - `FilterBuilder` 36 | 37 | Check out all the available methods [here](https://github.com/rosostolato/odata-fluent-query/blob/master/src/models/query-filter.ts). 38 | 39 | ```ts 40 | export type FilterBuider = { 41 | [P in keyof T]: FilterBuilderType 42 | } 43 | ``` 44 | 45 | You can modify/combine expressions using `not()`, `and()` and `or()`. 46 | 47 | ```ts 48 | .filter(u => u.username.contains('dave').not()) //where the username doest not contain dave 49 | 50 | .filter(u => u.emailActivaed.equals(true).and(u.username.contains('dave'))) 51 | ``` 52 | 53 | Calling `filter` multiple times will merge the expression in a bigger expression using the `and` operator. In this example you will get the users where "the id is not equal to 1 AND the username start with 'harry'". 54 | 55 | ```ts 56 | import { odataQuery } from 'odata-fluent-query' 57 | 58 | odataQuery() 59 | .filter(u => u.id.notEquals(1)) 60 | .filter(u => u.username.startsWith('Harry')) 61 | .toString() 62 | 63 | // result: $filter=id eq 1 and startswith(username, 'Harry') 64 | ``` 65 | 66 | More filter examples: 67 | 68 | ```ts 69 | odataQuery().filter(u => not(u.id.equals(1))) //where the id is not 1 70 | 71 | // result: $filter=id ne 1 72 | 73 | odataQuery().filter(u => u.id.equals(1).and( 74 | u.username.startsWith('Harry') //where the id is 1 AND the username starts with 'harry' 75 | ))) 76 | 77 | // result: $filter=id eq 1 and startswith(username, 'harry') 78 | 79 | odataQuery().filter(u => u.id.equals(1).or( 80 | u.username.startsWith('Harry') //where the id is 1 OR the username starts with 'harry' 81 | ))) 82 | 83 | // result: $filter=id eq 1 or startswith(username, 'harry') 84 | 85 | odataQuery().filter(u => u.email.startswith(u.name)) //You can also use properties of the same type instead of just values 86 | 87 | // result: $filter=startswith(email, name) 88 | ``` 89 | 90 | You can also use "key selector" passing the property key at the first parameter. 91 | 92 | ```ts 93 | odataQuery().filter('id', id => id.equals(1)) 94 | 95 | // result: $filter=id eq 1 96 | ``` 97 | 98 | ## Selecting properties with `select` 99 | 100 | `select` is used to select a set of properties of your model: 101 | 102 | ```ts 103 | import { odataQuery } from 'odata-fluent-query' 104 | 105 | odataQuery().select('id', 'username') 106 | 107 | // result: $select=id,username 108 | ``` 109 | 110 | ## Ordering with `orderBy` 111 | 112 | `orderby` is used to order the result of your query. This method accepts a function that returns the property you want to order by. 113 | 114 | ```ts 115 | odataQuery().orderBy(u => u.id) 116 | 117 | // result: $orderby=id 118 | ``` 119 | 120 | It is possible to order on relations: 121 | 122 | ```ts 123 | odataQuery() 124 | .select('username') 125 | .orderBy(u => u.address.city) 126 | 127 | // result: $select=username;$orderby=address/city 128 | ``` 129 | 130 | You can set the order mode by calling `Desc` or `Asc`. 131 | 132 | ```ts 133 | odataQuery().orderBy(u => u.id.desc()) 134 | 135 | // result: $orderby=id desc 136 | ``` 137 | 138 | You can also `orderBy` with key string. 139 | 140 | ```ts 141 | odataQuery().orderBy('id', 'desc') 142 | 143 | // result: $orderby=id desc 144 | ``` 145 | 146 | ## Expanding with `expand` 147 | 148 | `expand` is used to load the relationships of the model within the current query. This query can be used to filter, expand and select on the relation you are including. 149 | 150 | ```ts 151 | import { odataQuery } from 'odata-fluent-query' 152 | 153 | odataQuery() 154 | .expand('blogs') // or .expand(u => u.blogs) 155 | .toString() 156 | 157 | // result: $expand=blogs 158 | ``` 159 | 160 | All the query methods are available inside an "expand" call. 161 | 162 | ```ts 163 | import { odataQuery } from 'odata-fluent-query' 164 | 165 | odataQuery() 166 | .expand('blogs', q => 167 | q 168 | .select('id', 'title') 169 | .filter(b => b.public.equals(true)) 170 | .orderBy('id') 171 | .paginate(0, 10) 172 | ) 173 | .toString() 174 | 175 | // result: $expand=blogs($skip=0;$top=10;$orderby=id;$select=id,title;$filter=public eq true) 176 | ``` 177 | 178 | It's possible to nest "expand" calls inside each other. 179 | 180 | ```ts 181 | import { odataQuery } from "odata-fluent-query"; 182 | 183 | odataQuery() 184 | .expand('blogs', q => q 185 | .select('id', 'title') 186 | .expand('reactions' q => q.select('id', 'title') 187 | )) 188 | .toString(); 189 | 190 | // result: $expand=blogs($select=id,title;$expand=reactions($select=id,title)) 191 | ``` 192 | 193 | Key getters can easily get to deeper levels. 194 | 195 | ```ts 196 | odataQuery() 197 | .expand( 198 | u => u.blogs.reactions, 199 | q => q.select('id', 'title') 200 | ) 201 | .toString() 202 | 203 | // result: $expand=blogs/reactions($select=id,title) 204 | ``` 205 | 206 | ## Grouping with `groupBy` 207 | 208 | `groupBy` uses odata `$apply` method to group data by property with optional aggregations. 209 | 210 | ```ts 211 | import { odataQuery } from 'odata-fluent-query' 212 | 213 | odataQuery().groupBy(['email']).toString() 214 | 215 | // result: $apply=groupby((email)) 216 | ``` 217 | 218 | It's posible to apply custom aggregations. 219 | 220 | ```ts 221 | import { odataQuery } from 'odata-fluent-query' 222 | 223 | odataQuery() 224 | .groupBy(['email', 'surname'], a => 225 | a.countdistinct('id', 'all').max('phoneNumbers', 'test') 226 | ) 227 | .toString() 228 | 229 | // result: $apply=groupby((email, surname), aggregate(id with countdistinct as all, phoneNumbers with max as test)) 230 | ``` 231 | 232 | ## Paginating with `paginate` 233 | 234 | `paginate` applies `$top`, `$skip` and `$count` automatically. 235 | 236 | ```ts 237 | import { odataQuery } from 'odata-fluent-query' 238 | 239 | odataQuery().paginate(10).toString() 240 | 241 | // result: $top=10&$count=true 242 | ``` 243 | 244 | Skip and top. 245 | 246 | ```ts 247 | import { odataQuery } from 'odata-fluent-query' 248 | 249 | odataQuery().paginate(25, 5).toString() 250 | 251 | // result: $skip=125&$top=25&$count=true 252 | ``` 253 | 254 | Using object and setting `count` to false. 255 | 256 | ```ts 257 | import { odataQuery } from 'odata-fluent-query' 258 | 259 | odataQuery().paginate({ page: 5, pagesize: 25, count: false }).toString() 260 | 261 | // result: $skip=125&$top=25 262 | ``` 263 | 264 | ## Development 265 | 266 | Dependencies are managed by using `npm`. To install all the dependencies run: 267 | 268 | ```sh 269 | npm 270 | ``` 271 | 272 | To build the project run: 273 | 274 | ```sh 275 | npm build 276 | ``` 277 | 278 | The output files will be placed in the `build` directory. This project contains unittest using `jest` and `ts-jest`. They are placed in the `test` directory. To run all the test run: 279 | 280 | ```sh 281 | npm run test 282 | ``` 283 | 284 | After this you can open `coverage/lcov-report/index.html` in your browser to see all the details about you tests. To publish the package you can run: 285 | 286 | ```sh 287 | npm publish 288 | ``` 289 | -------------------------------------------------------------------------------- /dist/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 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. -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # odata-fluent-query 2 | 3 | **Client side queries with extensive filtering and typesafe joins** 4 | 5 | This lib only generates the query string, so you need to use it with your own implementation of http request. There is no need to scaffold any pre build model. 6 | 7 | - [Filtering with `filter`](#filtering-with-filter) 8 | - [Ordering with `orderBy`](#ordering-with-orderby) 9 | - [Selecting with `select`](#selecting-properties-with-select) 10 | - [Expanding with `expand`](#expanding-with-expand) 11 | - [Expanding with `groupBy`](#grouping-with-groupBy) 12 | - [Paginating with `paginate`](#paginating-with-paginate) 13 | - [Development](#development) 14 | 15 | ## Filtering with `filter` 16 | 17 | Every query exposes a method called `filter`. This method accepts a function as parameter that builds an expression. 18 | 19 | ```ts 20 | import { odataQuery } from 'odata-fluent-query' 21 | 22 | odataQuery() 23 | .filter(u => u.id.equals(1)) 24 | .toString() 25 | 26 | // result: $filter=id eq 1 27 | ``` 28 | 29 | Note that the parameter `u` is not a `User` type but `FilterBuider`. The `FilterBuider` will exposes all properties from `T` as `FilterBuilderType` to provide all filter functions based on its property type which can be: 30 | - `FilterCollection` 31 | - `FilterString` 32 | - `FilterNumber` 33 | - `FilterBoolean` 34 | - `FilterDate` 35 | - `FilterBuilder` 36 | 37 | Check out all the available methods [here](https://github.com/rosostolato/odata-fluent-query/blob/master/src/models/query-filter.ts). 38 | 39 | ```ts 40 | export type FilterBuider = { 41 | [P in keyof T]: FilterBuilderType 42 | } 43 | ``` 44 | 45 | You can modify/combine expressions using `not()`, `and()` and `or()`. 46 | 47 | ```ts 48 | .filter(u => u.username.contains('dave').not()) //where the username doest not contain dave 49 | 50 | .filter(u => u.emailActivaed.equals(true).and(u.username.contains('dave'))) 51 | ``` 52 | 53 | Calling `filter` multiple times will merge the expression in a bigger expression using the `and` operator. In this example you will get the users where "the id is not equal to 1 AND the username start with 'harry'". 54 | 55 | ```ts 56 | import { odataQuery } from 'odata-fluent-query' 57 | 58 | odataQuery() 59 | .filter(u => u.id.notEquals(1)) 60 | .filter(u => u.username.startsWith('Harry')) 61 | .toString() 62 | 63 | // result: $filter=id eq 1 and startswith(username, 'Harry') 64 | ``` 65 | 66 | More filter examples: 67 | 68 | ```ts 69 | odataQuery().filter(u => not(u.id.equals(1))) //where the id is not 1 70 | 71 | // result: $filter=id ne 1 72 | 73 | odataQuery().filter(u => u.id.equals(1).and( 74 | u.username.startsWith('Harry') //where the id is 1 AND the username starts with 'harry' 75 | ))) 76 | 77 | // result: $filter=id eq 1 and startswith(username, 'harry') 78 | 79 | odataQuery().filter(u => u.id.equals(1).or( 80 | u.username.startsWith('Harry') //where the id is 1 OR the username starts with 'harry' 81 | ))) 82 | 83 | // result: $filter=id eq 1 or startswith(username, 'harry') 84 | 85 | odataQuery().filter(u => u.email.startswith(u.name)) //You can also use properties of the same type instead of just values 86 | 87 | // result: $filter=startswith(email, name) 88 | ``` 89 | 90 | You can also use "key selector" passing the property key at the first parameter. 91 | 92 | ```ts 93 | odataQuery().filter('id', id => id.equals(1)) 94 | 95 | // result: $filter=id eq 1 96 | ``` 97 | 98 | ## Selecting properties with `select` 99 | 100 | `select` is used to select a set of properties of your model: 101 | 102 | ```ts 103 | import { odataQuery } from 'odata-fluent-query' 104 | 105 | odataQuery().select('id', 'username') 106 | 107 | // result: $select=id,username 108 | ``` 109 | 110 | ## Ordering with `orderBy` 111 | 112 | `orderby` is used to order the result of your query. This method accepts a function that returns the property you want to order by. 113 | 114 | ```ts 115 | odataQuery().orderBy(u => u.id) 116 | 117 | // result: $orderby=id 118 | ``` 119 | 120 | It is possible to order on relations: 121 | 122 | ```ts 123 | odataQuery() 124 | .select('username') 125 | .orderBy(u => u.address.city) 126 | 127 | // result: $select=username;$orderby=address/city 128 | ``` 129 | 130 | You can set the order mode by calling `Desc` or `Asc`. 131 | 132 | ```ts 133 | odataQuery().orderBy(u => u.id.desc()) 134 | 135 | // result: $orderby=id desc 136 | ``` 137 | 138 | You can also `orderBy` with key string. 139 | 140 | ```ts 141 | odataQuery().orderBy('id', 'desc') 142 | 143 | // result: $orderby=id desc 144 | ``` 145 | 146 | ## Expanding with `expand` 147 | 148 | `expand` is used to load the relationships of the model within the current query. This query can be used to filter, expand and select on the relation you are including. 149 | 150 | ```ts 151 | import { odataQuery } from 'odata-fluent-query' 152 | 153 | odataQuery() 154 | .expand('blogs') // or .expand(u => u.blogs) 155 | .toString() 156 | 157 | // result: $expand=blogs 158 | ``` 159 | 160 | All the query methods are available inside an "expand" call. 161 | 162 | ```ts 163 | import { odataQuery } from 'odata-fluent-query' 164 | 165 | odataQuery() 166 | .expand('blogs', q => 167 | q 168 | .select('id', 'title') 169 | .filter(b => b.public.equals(true)) 170 | .orderBy('id') 171 | .paginate(0, 10) 172 | ) 173 | .toString() 174 | 175 | // result: $expand=blogs($skip=0;$top=10;$orderby=id;$select=id,title;$filter=public eq true) 176 | ``` 177 | 178 | It's possible to nest "expand" calls inside each other. 179 | 180 | ```ts 181 | import { odataQuery } from "odata-fluent-query"; 182 | 183 | odataQuery() 184 | .expand('blogs', q => q 185 | .select('id', 'title') 186 | .expand('reactions' q => q.select('id', 'title') 187 | )) 188 | .toString(); 189 | 190 | // result: $expand=blogs($select=id,title;$expand=reactions($select=id,title)) 191 | ``` 192 | 193 | Key getters can easily get to deeper levels. 194 | 195 | ```ts 196 | odataQuery() 197 | .expand( 198 | u => u.blogs.reactions, 199 | q => q.select('id', 'title') 200 | ) 201 | .toString() 202 | 203 | // result: $expand=blogs/reactions($select=id,title) 204 | ``` 205 | 206 | ## Grouping with `groupBy` 207 | 208 | `groupBy` uses odata `$apply` method to group data by property with optional aggregations. 209 | 210 | ```ts 211 | import { odataQuery } from 'odata-fluent-query' 212 | 213 | odataQuery().groupBy(['email']).toString() 214 | 215 | // result: $apply=groupby((email)) 216 | ``` 217 | 218 | It's posible to apply custom aggregations. 219 | 220 | ```ts 221 | import { odataQuery } from 'odata-fluent-query' 222 | 223 | odataQuery() 224 | .groupBy(['email', 'surname'], a => 225 | a.countdistinct('id', 'all').max('phoneNumbers', 'test') 226 | ) 227 | .toString() 228 | 229 | // result: $apply=groupby((email, surname), aggregate(id with countdistinct as all, phoneNumbers with max as test)) 230 | ``` 231 | 232 | ## Paginating with `paginate` 233 | 234 | `paginate` applies `$top`, `$skip` and `$count` automatically. 235 | 236 | ```ts 237 | import { odataQuery } from 'odata-fluent-query' 238 | 239 | odataQuery().paginate(10).toString() 240 | 241 | // result: $top=10&$count=true 242 | ``` 243 | 244 | Skip and top. 245 | 246 | ```ts 247 | import { odataQuery } from 'odata-fluent-query' 248 | 249 | odataQuery().paginate(25, 5).toString() 250 | 251 | // result: $skip=125&$top=25&$count=true 252 | ``` 253 | 254 | Using object and setting `count` to false. 255 | 256 | ```ts 257 | import { odataQuery } from 'odata-fluent-query' 258 | 259 | odataQuery().paginate({ page: 5, pagesize: 25, count: false }).toString() 260 | 261 | // result: $skip=125&$top=25 262 | ``` 263 | 264 | ## Development 265 | 266 | Dependencies are managed by using `npm`. To install all the dependencies run: 267 | 268 | ```sh 269 | npm 270 | ``` 271 | 272 | To build the project run: 273 | 274 | ```sh 275 | npm build 276 | ``` 277 | 278 | The output files will be placed in the `build` directory. This project contains unittest using `jest` and `ts-jest`. They are placed in the `test` directory. To run all the test run: 279 | 280 | ```sh 281 | npm run test 282 | ``` 283 | 284 | After this you can open `coverage/lcov-report/index.html` in your browser to see all the details about you tests. To publish the package you can run: 285 | 286 | ```sh 287 | npm publish 288 | ``` 289 | -------------------------------------------------------------------------------- /dist/builders/create-expand.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models'; 2 | export declare function createExpand(descriptor: QueryDescriptor): (keyOrExp: string | Function, query?: Function) => any; 3 | -------------------------------------------------------------------------------- /dist/builders/create-expand.js: -------------------------------------------------------------------------------- 1 | import { createQuery, createQueryDescriptor } from './create-query'; 2 | function makeExpand(key = '') { 3 | return new Proxy({}, { 4 | get(_, prop) { 5 | if (prop === '_key') 6 | return key.slice(1); 7 | return makeExpand(`${key}/${String(prop)}`); 8 | }, 9 | }); 10 | } 11 | export function createExpand(descriptor) { 12 | return (keyOrExp, query) => { 13 | let key = ''; 14 | if (typeof keyOrExp === 'function') { 15 | const exp = keyOrExp(makeExpand()); 16 | key = exp._key; 17 | } 18 | else { 19 | key = String(keyOrExp); 20 | } 21 | const expand = createQuery(createQueryDescriptor(key)); 22 | const result = query?.(expand) || expand; 23 | const newDescriptor = { 24 | ...descriptor, 25 | expands: descriptor.expands.concat(result._descriptor), 26 | }; 27 | return createQuery(newDescriptor); 28 | }; 29 | } 30 | //# sourceMappingURL=create-expand.js.map -------------------------------------------------------------------------------- /dist/builders/create-expand.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"create-expand.js","sourceRoot":"","sources":["../../src/builders/create-expand.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAEnE,SAAS,UAAU,CAAC,GAAG,GAAG,EAAE;IAC1B,OAAO,IAAI,KAAK,CACd,EAAE,EACF;QACE,GAAG,CAAC,CAAC,EAAE,IAAI;YACT,IAAI,IAAI,KAAK,MAAM;gBAAE,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YACxC,OAAO,UAAU,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC7C,CAAC;KACF,CACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,UAA2B;IACtD,OAAO,CAAC,QAA2B,EAAE,KAAgB,EAAE,EAAE;QACvD,IAAI,GAAG,GAAW,EAAE,CAAA;QACpB,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE;YAClC,MAAM,GAAG,GAAQ,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;YACvC,GAAG,GAAG,GAAG,CAAC,IAAI,CAAA;SACf;aAAM;YACL,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAA;SACvB;QACD,MAAM,MAAM,GAAG,WAAW,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAA;QACtD,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC,MAAM,CAAC,IAAI,MAAM,CAAA;QACxC,MAAM,aAAa,GAAoB;YACrC,GAAG,UAAU;YACb,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC;SACvD,CAAA;QACD,OAAO,WAAW,CAAC,aAAa,CAAC,CAAA;IACnC,CAAC,CAAA;AACH,CAAC"} -------------------------------------------------------------------------------- /dist/builders/create-filter.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models'; 2 | export declare function getFuncArgs(func: Function): string[]; 3 | export declare function dateToObject(d: Date): { 4 | year: number; 5 | month: number; 6 | day: number; 7 | hour: number; 8 | minute: number; 9 | second: number; 10 | }; 11 | export declare function makeExp(exp: string): any; 12 | export declare function createFilter(descriptor: QueryDescriptor): (keyOrExp: any, exp?: any) => any; 13 | -------------------------------------------------------------------------------- /dist/builders/create-filter.js: -------------------------------------------------------------------------------- 1 | import { createQuery } from './create-query'; 2 | import isUUID from 'validator/lib/isUUID'; 3 | export function getFuncArgs(func) { 4 | const [, , paramStr] = /(function)?(.*?)(?=[={])/.exec(func.toString()) ?? []; 5 | return (paramStr ?? '') 6 | .replace('=>', '') 7 | .replace('(', '') 8 | .replace(')', '') 9 | .split(',') 10 | .map(s => s.trim()); 11 | } 12 | export function dateToObject(d) { 13 | if (typeof d === 'string') { 14 | d = new Date(d); 15 | } 16 | return { 17 | year: d.getFullYear(), 18 | month: d.getMonth(), 19 | day: d.getFullYear(), 20 | hour: d.getFullYear(), 21 | minute: d.getFullYear(), 22 | second: d.getFullYear(), 23 | }; 24 | } 25 | export function makeExp(exp) { 26 | const _get = (checkParetheses = false) => { 27 | if (!checkParetheses) { 28 | return exp; 29 | } 30 | else if (exp.indexOf(' or ') > -1 || exp.indexOf(' and ') > -1) { 31 | return `(${exp})`; 32 | } 33 | else { 34 | return exp; 35 | } 36 | }; 37 | return { 38 | _get, 39 | not: () => makeExp(`not (${exp})`), 40 | and: (exp) => makeExp(`${_get()} and ${exp._get(true)}`), 41 | or: (exp) => makeExp(`${_get()} or ${exp._get(true)}`), 42 | }; 43 | } 44 | function filterBuilder(key) { 45 | const arrFuncBuilder = (method) => (exp) => { 46 | const [arg] = getFuncArgs(exp); 47 | const builder = exp(makeFilter(arg)); 48 | const expr = builder._get(); 49 | return makeExp(`${key}/${method}(${arg}: ${expr})`); 50 | }; 51 | const strFuncBuilder = (method) => (s, opt) => { 52 | if (opt?.caseInsensitive) { 53 | return makeExp(`${method}(tolower(${key}), ${typeof s == 'string' 54 | ? `'${s.toLocaleLowerCase()}'` 55 | : `tolower(${s._key})`})`); 56 | } 57 | else if (s.getPropName) { 58 | return makeExp(`${method}(${key}, ${s._key})`); 59 | } 60 | else { 61 | return makeExp(`${method}(${key}, ${typeof s == 'string' ? `'${s}'` : s})`); 62 | } 63 | }; 64 | const equalityBuilder = (t) => (x, opt) => { 65 | switch (typeof x) { 66 | case 'string': 67 | if (isUUID(x) && !opt?.ignoreGuid) { 68 | return makeExp(`${key} ${t} ${x}`); // no quote around ${x} 69 | } 70 | else if (opt?.caseInsensitive) { 71 | return makeExp(`tolower(${key}) ${t} '${x.toLocaleLowerCase()}'`); 72 | } 73 | else { 74 | return makeExp(`${key} ${t} '${x}'`); 75 | } 76 | case 'number': 77 | return makeExp(`${key} ${t} ${x}`); 78 | case 'boolean': 79 | return makeExp(`${key} ${t} ${x}`); 80 | default: 81 | if (x && opt?.caseInsensitive) { 82 | return makeExp(`tolower(${key}) ${t} tolower(${x._key})`); 83 | } 84 | else { 85 | return makeExp(`${key} ${t} ${x?._key || null}`); 86 | } 87 | } 88 | }; 89 | const dateComparison = (compare) => (d) => { 90 | if (typeof d === 'string') 91 | return makeExp(`${key} ${compare} ${d}`); 92 | else if (d instanceof Date) 93 | return makeExp(`${key} ${compare} ${d.toISOString()}`); 94 | else 95 | return makeExp(`${key} ${compare} ${d._key}`); 96 | }; 97 | const numberComparison = (compare) => (n) => makeExp(`${key} ${compare} ${typeof n == 'number' ? n : n._key}`); 98 | return { 99 | _key: key, 100 | ///////////////////// 101 | // FilterBuilderDate 102 | inTimeSpan: (y, m, d, h, mm) => { 103 | let exps = [`year(${key}) eq ${y}`]; 104 | if (m != undefined) 105 | exps.push(`month(${key}) eq ${m}`); 106 | if (d != undefined) 107 | exps.push(`day(${key}) eq ${d}`); 108 | if (h != undefined) 109 | exps.push(`hour(${key}) eq ${h}`); 110 | if (mm != undefined) 111 | exps.push(`minute(${key}) eq ${mm}`); 112 | return makeExp('(' + exps.join(') and (') + ')'); 113 | }, 114 | isSame: (x, g) => { 115 | if (typeof x === 'string') { 116 | return makeExp(`${key} eq ${x}`); 117 | } 118 | else if (typeof x === 'number') { 119 | return makeExp(`${g}(${key}) eq ${x}`); 120 | } 121 | else if (x instanceof Date) { 122 | if (g == null) { 123 | return makeExp(`${key} eq ${x.toISOString()}`); 124 | } 125 | else { 126 | const o = dateToObject(x); 127 | return makeExp(`${g}(${key}) eq ${o[g]}`); 128 | } 129 | } 130 | else { 131 | return makeExp(`${g}(${key}) eq ${g}(${x._key})`); 132 | } 133 | }, 134 | isAfter: dateComparison('gt'), 135 | isBefore: dateComparison('lt'), 136 | isAfterOrEqual: dateComparison('ge'), 137 | isBeforeOrEqual: dateComparison('le'), 138 | ///////////////////// 139 | // FilterBuilderArray 140 | empty: () => makeExp(`not ${key}/any()`), 141 | notEmpty: () => makeExp(`${key}/any()`), 142 | any: arrFuncBuilder('any'), 143 | all: arrFuncBuilder('all'), 144 | ////////////////////// 145 | // FilterBuilderString 146 | isNull: () => makeExp(`${key} eq null`), 147 | notNull: () => makeExp(`${key} ne null`), 148 | contains: strFuncBuilder('contains'), 149 | startsWith: strFuncBuilder('startswith'), 150 | endsWith: strFuncBuilder('endswith'), 151 | tolower: () => makeFilter(`tolower(${key})`), 152 | toupper: () => makeFilter(`toupper(${key})`), 153 | length: () => makeFilter(`length(${key})`), 154 | trim: () => makeFilter(`trim(${key})`), 155 | indexof: (s) => makeFilter(`indexof(${key}, '${s}')`), 156 | substring: (n) => makeFilter(`substring(${key}, ${n})`), 157 | append: (s) => makeFilter(`concat(${key}, '${s}')`), 158 | prepend: (s) => makeFilter(`concat('${s}', ${key})`), 159 | ////////////////////// 160 | // FilterBuilderNumber 161 | biggerThan: numberComparison('gt'), 162 | lessThan: numberComparison('lt'), 163 | biggerThanOrEqual: numberComparison('ge'), 164 | lessThanOrEqual: numberComparison('le'), 165 | //////////////////////////////// 166 | // FilterBuilder Generic Methods 167 | equals: equalityBuilder('eq'), 168 | notEquals: equalityBuilder('ne'), 169 | in(arr) { 170 | const list = arr 171 | .map(x => (typeof x === 'string' ? `'${x}'` : x)) 172 | .join(','); 173 | return makeExp(`${key} in (${list})`); 174 | }, 175 | }; 176 | } 177 | function makeFilter(prefix = '') { 178 | return new Proxy({}, { 179 | get(_, prop) { 180 | const methods = filterBuilder(prefix); 181 | const key = prefix ? `${prefix}/${String(prop)}` : String(prop); 182 | return methods?.[prop] ? methods[prop] : makeFilter(String(key)); 183 | }, 184 | }); 185 | } 186 | export function createFilter(descriptor) { 187 | return (keyOrExp, exp) => { 188 | const expr = typeof keyOrExp === 'string' 189 | ? exp(filterBuilder(keyOrExp)) 190 | : keyOrExp(makeFilter()); 191 | return createQuery({ 192 | ...descriptor, 193 | filters: descriptor.filters.concat(expr._get()), 194 | }); 195 | }; 196 | } 197 | //# sourceMappingURL=create-filter.js.map -------------------------------------------------------------------------------- /dist/builders/create-filter.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"create-filter.js","sourceRoot":"","sources":["../../src/builders/create-filter.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/builders/create-groupby.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models'; 2 | export declare function groupbyBuilder(aggregator?: string[]): { 3 | aggregator: string[]; 4 | sum(prop: string, as: string): any; 5 | min(prop: string, as: string): any; 6 | max(prop: string, as: string): any; 7 | average(prop: string, as: string): any; 8 | countdistinct(prop: string, as: string): any; 9 | custom: (prop: string, aggreg: string, as: string) => any; 10 | }; 11 | export declare function createGroupby(descriptor: QueryDescriptor): (keys: string[], aggregate?: Function) => any; 12 | -------------------------------------------------------------------------------- /dist/builders/create-groupby.js: -------------------------------------------------------------------------------- 1 | import { createQuery } from './create-query'; 2 | export function groupbyBuilder(aggregator = []) { 3 | const custom = (prop, aggreg, as) => groupbyBuilder(aggregator.concat(`${prop} with ${aggreg} as ${as}`)); 4 | return { 5 | aggregator, 6 | sum(prop, as) { 7 | return custom(prop, 'sum', as); 8 | }, 9 | min(prop, as) { 10 | return custom(prop, 'min', as); 11 | }, 12 | max(prop, as) { 13 | return custom(prop, 'max', as); 14 | }, 15 | average(prop, as) { 16 | return custom(prop, 'average', as); 17 | }, 18 | countdistinct(prop, as) { 19 | return custom(prop, 'countdistinct', as); 20 | }, 21 | custom, 22 | }; 23 | } 24 | export function createGroupby(descriptor) { 25 | return (keys, aggregate) => { 26 | const agg = groupbyBuilder(); 27 | const result = aggregate?.(agg) || agg; 28 | return createQuery({ 29 | ...descriptor, 30 | groupby: keys.map(String), 31 | aggregator: result.aggregator.join(', ') || null, 32 | }); 33 | }; 34 | } 35 | //# sourceMappingURL=create-groupby.js.map -------------------------------------------------------------------------------- /dist/builders/create-groupby.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"create-groupby.js","sourceRoot":"","sources":["../../src/builders/create-groupby.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,MAAM,UAAU,cAAc,CAAC,aAAuB,EAAE;IACtD,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,MAAc,EAAE,EAAU,EAAE,EAAE,CAC1D,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,IAAI,SAAS,MAAM,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;IAEtE,OAAO;QACL,UAAU;QACV,GAAG,CAAC,IAAY,EAAE,EAAU;YAC1B,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;QAChC,CAAC;QACD,GAAG,CAAC,IAAY,EAAE,EAAU;YAC1B,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;QAChC,CAAC;QACD,GAAG,CAAC,IAAY,EAAE,EAAU;YAC1B,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;QAChC,CAAC;QACD,OAAO,CAAC,IAAY,EAAE,EAAU;YAC9B,OAAO,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;QACpC,CAAC;QACD,aAAa,CAAC,IAAY,EAAE,EAAU;YACpC,OAAO,MAAM,CAAC,IAAI,EAAE,eAAe,EAAE,EAAE,CAAC,CAAA;QAC1C,CAAC;QACD,MAAM;KACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,UAA2B;IACvD,OAAO,CAAC,IAAc,EAAE,SAAoB,EAAE,EAAE;QAC9C,MAAM,GAAG,GAAG,cAAc,EAAE,CAAA;QAC5B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC,GAAG,CAAC,IAAI,GAAG,CAAA;QAEtC,OAAO,WAAW,CAAC;YACjB,GAAG,UAAU;YACb,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;YACzB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI;SACjD,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC"} -------------------------------------------------------------------------------- /dist/builders/create-orderby.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models'; 2 | export declare function createOrderby(descriptor: QueryDescriptor): (keyOrExp: any, order?: 'asc' | 'desc') => any; 3 | -------------------------------------------------------------------------------- /dist/builders/create-orderby.js: -------------------------------------------------------------------------------- 1 | import { createQuery } from './create-query'; 2 | function makeOrderby(key = '') { 3 | if (key[0] === '/') { 4 | key = key.slice(1); 5 | } 6 | const methods = { 7 | _key: key, 8 | asc: () => makeOrderby(`${key} asc`), 9 | desc: () => makeOrderby(`${key} desc`), 10 | }; 11 | return new Proxy({}, { 12 | get(_, prop) { 13 | return methods[prop] || makeOrderby(`${key}/${String(prop)}`); 14 | }, 15 | }); 16 | } 17 | export function createOrderby(descriptor) { 18 | return (keyOrExp, order) => { 19 | let expr = typeof keyOrExp === 'string' 20 | ? makeOrderby(keyOrExp) 21 | : keyOrExp(makeOrderby()); 22 | if (order) { 23 | expr = expr[order](); 24 | } 25 | return createQuery({ 26 | ...descriptor, 27 | orderby: descriptor.orderby.concat(expr['_key']), 28 | }); 29 | }; 30 | } 31 | //# sourceMappingURL=create-orderby.js.map -------------------------------------------------------------------------------- /dist/builders/create-orderby.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"create-orderby.js","sourceRoot":"","sources":["../../src/builders/create-orderby.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,SAAS,WAAW,CAAC,GAAG,GAAG,EAAE;IAC3B,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;QAClB,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;KACnB;IAED,MAAM,OAAO,GAAQ;QACnB,IAAI,EAAE,GAAG;QACT,GAAG,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,GAAG,MAAM,CAAC;QACpC,IAAI,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,GAAG,OAAO,CAAC;KACvC,CAAA;IAED,OAAO,IAAI,KAAK,CACd,EAAE,EACF;QACE,GAAG,CAAC,CAAC,EAAE,IAAI;YACT,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC/D,CAAC;KACF,CACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,UAA2B;IACvD,OAAO,CAAC,QAAa,EAAE,KAAsB,EAAE,EAAE;QAC/C,IAAI,IAAI,GACN,OAAO,QAAQ,KAAK,QAAQ;YAC1B,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC;YACvB,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAA;QAE7B,IAAI,KAAK,EAAE;YACT,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAA;SACrB;QAED,OAAO,WAAW,CAAC;YACjB,GAAG,UAAU;YACb,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;SACjD,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC"} -------------------------------------------------------------------------------- /dist/builders/create-paginate.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models'; 2 | export declare function createPaginate(descriptor: QueryDescriptor): (sizeOrOptions: any, page: number) => any; 3 | -------------------------------------------------------------------------------- /dist/builders/create-paginate.js: -------------------------------------------------------------------------------- 1 | import { createQuery } from './create-query'; 2 | export function createPaginate(descriptor) { 3 | return (sizeOrOptions, page) => { 4 | let data; 5 | if (typeof sizeOrOptions === 'number') { 6 | data = { 7 | page: page, 8 | count: true, 9 | pagesize: sizeOrOptions, 10 | }; 11 | } 12 | else { 13 | data = sizeOrOptions; 14 | if (data.count === undefined) { 15 | data.count = true; 16 | } 17 | } 18 | const queryDescriptor = { 19 | ...descriptor, 20 | take: data.pagesize, 21 | skip: data.pagesize * data.page, 22 | count: data.count, 23 | }; 24 | if (!queryDescriptor.skip) { 25 | queryDescriptor.skip = undefined; 26 | } 27 | return createQuery(queryDescriptor); 28 | }; 29 | } 30 | //# sourceMappingURL=create-paginate.js.map -------------------------------------------------------------------------------- /dist/builders/create-paginate.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"create-paginate.js","sourceRoot":"","sources":["../../src/builders/create-paginate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,MAAM,UAAU,cAAc,CAAC,UAA2B;IACxD,OAAO,CAAC,aAAkB,EAAE,IAAY,EAAE,EAAE;QAC1C,IAAI,IAIH,CAAA;QACD,IAAI,OAAO,aAAa,KAAK,QAAQ,EAAE;YACrC,IAAI,GAAG;gBACL,IAAI,EAAE,IAAI;gBACV,KAAK,EAAE,IAAI;gBACX,QAAQ,EAAE,aAAa;aACxB,CAAA;SACF;aAAM;YACL,IAAI,GAAG,aAAa,CAAA;YAEpB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE;gBAC5B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;aAClB;SACF;QACD,MAAM,eAAe,GAAoB;YACvC,GAAG,UAAU;YACb,IAAI,EAAE,IAAI,CAAC,QAAQ;YACnB,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI;YAC/B,KAAK,EAAE,IAAI,CAAC,KAAK;SAClB,CAAA;QACD,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE;YACzB,eAAe,CAAC,IAAI,GAAG,SAAS,CAAA;SACjC;QACD,OAAO,WAAW,CAAC,eAAe,CAAC,CAAA;IACrC,CAAC,CAAA;AACH,CAAC"} -------------------------------------------------------------------------------- /dist/builders/create-query.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models'; 2 | export declare function createQueryDescriptor(key?: string): QueryDescriptor; 3 | export declare function createQuery(descriptor: QueryDescriptor): any; 4 | -------------------------------------------------------------------------------- /dist/builders/create-query.js: -------------------------------------------------------------------------------- 1 | import { createExpand } from './create-expand'; 2 | import { createFilter } from './create-filter'; 3 | import { createGroupby } from './create-groupby'; 4 | import { createOrderby } from './create-orderby'; 5 | import { createPaginate } from './create-paginate'; 6 | import { createSelect } from './create-select'; 7 | import { makeQuery } from './query-builder'; 8 | export function createQueryDescriptor(key) { 9 | return { 10 | key: key, 11 | skip: undefined, 12 | take: undefined, 13 | count: false, 14 | aggregator: undefined, 15 | filters: [], 16 | expands: [], 17 | orderby: [], 18 | groupby: [], 19 | select: [], 20 | }; 21 | } 22 | export function createQuery(descriptor) { 23 | return { 24 | _descriptor: descriptor, 25 | expand: createExpand(descriptor), 26 | filter: createFilter(descriptor), 27 | groupBy: createGroupby(descriptor), 28 | orderBy: createOrderby(descriptor), 29 | paginate: createPaginate(descriptor), 30 | select: createSelect(descriptor), 31 | count() { 32 | return createQuery({ 33 | ...descriptor, 34 | count: true, 35 | }); 36 | }, 37 | toObject() { 38 | return makeQuery(descriptor).reduce((obj, x) => { 39 | obj[x.key] = x.value; 40 | return obj; 41 | }, {}); 42 | }, 43 | toString() { 44 | return makeQuery(descriptor) 45 | .map(p => `${p.key}=${p.value}`) 46 | .join('&'); 47 | }, 48 | }; 49 | } 50 | //# sourceMappingURL=create-query.js.map -------------------------------------------------------------------------------- /dist/builders/create-query.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"create-query.js","sourceRoot":"","sources":["../../src/builders/create-query.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,OAAO;QACL,GAAG,EAAE,GAAG;QACR,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,KAAK;QACZ,UAAU,EAAE,SAAS;QACrB,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,EAAE;QACX,MAAM,EAAE,EAAE;KACX,CAAA;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,UAA2B;IACrD,OAAO;QACL,WAAW,EAAE,UAAU;QACvB,MAAM,EAAE,YAAY,CAAC,UAAU,CAAC;QAChC,MAAM,EAAE,YAAY,CAAC,UAAU,CAAC;QAChC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC;QAClC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC;QAClC,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC;QACpC,MAAM,EAAE,YAAY,CAAC,UAAU,CAAC;QAChC,KAAK;YACH,OAAO,WAAW,CAAC;gBACjB,GAAG,UAAU;gBACb,KAAK,EAAE,IAAI;aACZ,CAAC,CAAA;QACJ,CAAC;QACD,QAAQ;YACN,OAAO,SAAS,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;gBAC7C,GAAG,CAAC,CAAC,CAAC,GAAwB,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;gBACzC,OAAO,GAAG,CAAA;YACZ,CAAC,EAAE,EAAiB,CAAC,CAAA;QACvB,CAAC;QACD,QAAQ;YACN,OAAO,SAAS,CAAC,UAAU,CAAC;iBACzB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;iBAC/B,IAAI,CAAC,GAAG,CAAC,CAAA;QACd,CAAC;KACF,CAAA;AACH,CAAC"} -------------------------------------------------------------------------------- /dist/builders/create-select.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models'; 2 | export declare function createSelect(descriptor: QueryDescriptor): (...keys: any[]) => any; 3 | -------------------------------------------------------------------------------- /dist/builders/create-select.js: -------------------------------------------------------------------------------- 1 | import { createQuery } from './create-query'; 2 | function makeSelect(key = '') { 3 | return new Proxy({}, { 4 | get(_, prop) { 5 | if (prop === '_key') 6 | return key.slice(1); 7 | return makeSelect(`${key}/${String(prop)}`); 8 | }, 9 | }); 10 | } 11 | export function createSelect(descriptor) { 12 | return (...keys) => { 13 | const _keys = keys 14 | .map(keyOrExp => { 15 | if (typeof keyOrExp === 'function') { 16 | const exp = keyOrExp(makeSelect()); 17 | return exp._key; 18 | } 19 | else { 20 | return String(keyOrExp); 21 | } 22 | }) 23 | .filter((k, i, arr) => arr.indexOf(k) === i); // unique 24 | return createQuery({ 25 | ...descriptor, 26 | select: _keys, 27 | expands: descriptor.expands.filter(e => _keys.some(k => e.key == String(k))), 28 | }); 29 | }; 30 | } 31 | //# sourceMappingURL=create-select.js.map -------------------------------------------------------------------------------- /dist/builders/create-select.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"create-select.js","sourceRoot":"","sources":["../../src/builders/create-select.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,SAAS,UAAU,CAAC,GAAG,GAAG,EAAE;IAC1B,OAAO,IAAI,KAAK,CACd,EAAE,EACF;QACE,GAAG,CAAC,CAAC,EAAE,IAAI;YACT,IAAI,IAAI,KAAK,MAAM;gBAAE,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YACxC,OAAO,UAAU,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC7C,CAAC;KACF,CACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,UAA2B;IACtD,OAAO,CAAC,GAAG,IAAW,EAAE,EAAE;QACxB,MAAM,KAAK,GAAG,IAAI;aACf,GAAG,CAAC,QAAQ,CAAC,EAAE;YACd,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE;gBAClC,MAAM,GAAG,GAAQ,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;gBACvC,OAAO,GAAG,CAAC,IAAI,CAAA;aAChB;iBAAM;gBACL,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAA;aACxB;QACH,CAAC,CAAC;aACD,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA,CAAC,SAAS;QACxD,OAAO,WAAW,CAAC;YACjB,GAAG,UAAU;YACb,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CACrC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CACpC;SACF,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC"} -------------------------------------------------------------------------------- /dist/builders/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './create-filter'; 2 | export * from './create-groupby'; 3 | export * from './create-orderby'; 4 | export * from './create-query'; 5 | export * from './create-select'; 6 | export * from './query-builder'; 7 | -------------------------------------------------------------------------------- /dist/builders/index.js: -------------------------------------------------------------------------------- 1 | export * from './create-filter'; 2 | export * from './create-groupby'; 3 | export * from './create-orderby'; 4 | export * from './create-query'; 5 | export * from './create-select'; 6 | export * from './query-builder'; 7 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/builders/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/builders/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA"} -------------------------------------------------------------------------------- /dist/builders/query-builder.d.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models'; 2 | export interface KeyValue { 3 | key: string; 4 | value: T; 5 | } 6 | export declare function makeQuery(qd: QueryDescriptor): KeyValue[]; 7 | export declare function makeQueryParentheses(query: string): string; 8 | export declare function makeRelationQuery(rqd: QueryDescriptor): string; 9 | -------------------------------------------------------------------------------- /dist/builders/query-builder.js: -------------------------------------------------------------------------------- 1 | export function makeQuery(qd) { 2 | let params = []; 3 | if (qd.filters.length) { 4 | if (qd.filters.length > 1) { 5 | params.push({ 6 | key: '$filter', 7 | value: `${qd.filters.map(makeQueryParentheses).join(' and ')}`, 8 | }); 9 | } 10 | else { 11 | params.push({ 12 | key: '$filter', 13 | value: `${qd.filters.join()}`, 14 | }); 15 | } 16 | } 17 | if (qd.groupby.length) { 18 | let group = `groupby((${qd.groupby.join(', ')})`; 19 | if (qd.aggregator) { 20 | group += `, aggregate(${qd.aggregator})`; 21 | } 22 | params.push({ 23 | key: '$apply', 24 | value: group + ')', 25 | }); 26 | } 27 | if (qd.expands.length) { 28 | params.push({ 29 | key: '$expand', 30 | value: `${qd.expands.map(makeRelationQuery).join(',')}`, 31 | }); 32 | } 33 | if (qd.select.length) { 34 | params.push({ 35 | key: '$select', 36 | value: `${qd.select.join(',')}`, 37 | }); 38 | } 39 | if (qd.orderby.length) { 40 | params.push({ 41 | key: '$orderby', 42 | value: `${qd.orderby.join(', ')}`, 43 | }); 44 | } 45 | if (qd.skip != null) { 46 | params.push({ 47 | key: '$skip', 48 | value: `${qd.skip}`, 49 | }); 50 | } 51 | if (qd.take != null) { 52 | params.push({ 53 | key: '$top', 54 | value: `${qd.take}`, 55 | }); 56 | } 57 | if (qd.count == true) { 58 | params.push({ 59 | key: '$count', 60 | value: `true`, 61 | }); 62 | } 63 | return params; 64 | } 65 | export function makeQueryParentheses(query) { 66 | if (query.indexOf(' or ') > -1 || query.indexOf(' and ') > -1) { 67 | return `(${query})`; 68 | } 69 | return query; 70 | } 71 | export function makeRelationQuery(rqd) { 72 | let expand = rqd.key || ''; 73 | if (rqd.filters.length || 74 | rqd.orderby.length || 75 | rqd.select.length || 76 | rqd.expands.length || 77 | rqd.skip != null || 78 | rqd.take != null || 79 | rqd.count != false) { 80 | expand += `(`; 81 | let operators = []; 82 | if (rqd.skip != null) { 83 | operators.push(`$skip=${rqd.skip}`); 84 | } 85 | if (rqd.take != null) { 86 | operators.push(`$top=${rqd.take}`); 87 | } 88 | if (rqd.count == true) { 89 | operators.push(`$count=true`); 90 | } 91 | if (rqd.orderby.length) { 92 | operators.push(`$orderby=${rqd.orderby.join(',')}`); 93 | } 94 | if (rqd.select.length) { 95 | operators.push(`$select=${rqd.select.join(',')}`); 96 | } 97 | if (rqd.filters.length) { 98 | if (rqd.filters.length > 1) { 99 | operators.push(`$filter=${rqd.filters.map(makeQueryParentheses).join(' and ')}`); 100 | } 101 | else { 102 | operators.push(`$filter=${rqd.filters.join()}`); 103 | } 104 | } 105 | if (rqd.expands.length) { 106 | operators.push(`$expand=${rqd.expands.map(makeRelationQuery).join(',')}`); 107 | } 108 | expand += operators.join(';') + ')'; 109 | } 110 | return expand; 111 | } 112 | //# sourceMappingURL=query-builder.js.map -------------------------------------------------------------------------------- /dist/builders/query-builder.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query-builder.js","sourceRoot":"","sources":["../../src/builders/query-builder.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ODataQuery } from './models'; 2 | export declare function odataQuery(): ODataQuery; 3 | export * from './models'; 4 | export default odataQuery; 5 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | import { createQuery, createQueryDescriptor } from './builders'; 2 | export function odataQuery() { 3 | const defaultDescriptor = createQueryDescriptor(); 4 | return createQuery(defaultDescriptor); 5 | } 6 | export * from './models'; 7 | export default odataQuery; 8 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAG/D,MAAM,UAAU,UAAU;IACxB,MAAM,iBAAiB,GAAG,qBAAqB,EAAE,CAAA;IACjD,OAAO,WAAW,CAAC,iBAAiB,CAAC,CAAA;AACvC,CAAC;AAED,cAAc,UAAU,CAAA;AACxB,eAAe,UAAU,CAAA"} -------------------------------------------------------------------------------- /dist/models/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './odata-query'; 2 | export * from './query-descriptor'; 3 | export * from './query-filter'; 4 | export * from './query-groupby'; 5 | export * from './query-orderby'; 6 | export * from './query-select'; 7 | -------------------------------------------------------------------------------- /dist/models/index.js: -------------------------------------------------------------------------------- 1 | export * from './odata-query'; 2 | export * from './query-descriptor'; 3 | export * from './query-filter'; 4 | export * from './query-groupby'; 5 | export * from './query-orderby'; 6 | export * from './query-select'; 7 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/models/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/models/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,oBAAoB,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA"} -------------------------------------------------------------------------------- /dist/models/odata-query.d.ts: -------------------------------------------------------------------------------- 1 | import { ExpandParam, ExpandKey, ExpandQueryComplex } from './query-expand'; 2 | import { FilterBuilder, FilterBuilderProp, FilterExpression } from './query-filter'; 3 | import { GroupbyBuilder } from './query-groupby'; 4 | import { OrderBy, OrderByBuilder, OrderByExpression } from './query-orderby'; 5 | import { SelectParams } from './query-select'; 6 | export interface ODataQuery { 7 | /** 8 | * set $count=true 9 | */ 10 | count(): ODataQuery; 11 | /** 12 | * Adds a $expand operator to the OData query. 13 | * Multiple calls to Expand will expand all the relations, e.g.: $expand=rel1(...),rel2(...). 14 | * The lambda in the second parameter allows you to build a complex inner query. 15 | * 16 | * @param key the name of the relation. 17 | * @param query a lambda expression that build the subquery from the querybuilder. 18 | * 19 | * @example 20 | * q.exand('blogs', q => q.select('id', 'title')) 21 | * q.exand(u => u.blogs, q => q.select('id', 'title')) 22 | */ 23 | expand, U = Required[Tkey]>(key: Tkey | ExpandParam, query?: (x: ExpandQueryComplex) => ExpandQueryComplex): ODataQuery; 24 | /** 25 | * Adds a $filter operator to the OData query. 26 | * Multiple calls to Filter will be merged with `and`. 27 | * 28 | * @param exp a lambda expression that builds an expression from the builder. 29 | * 30 | * @example 31 | * q.filter(u => u.id.equals(1)) 32 | */ 33 | filter(exp: (x: FilterBuilder) => FilterExpression): ODataQuery; 34 | /** 35 | * Adds a $filter operator to the OData query. 36 | * Multiple calls to Filter will be merged with `and`. 37 | * 38 | * @param key property key selector. 39 | * @param exp a lambda expression that builds an expression from the builder. 40 | * 41 | * @example 42 | * q.filter('id', id => id.equals(1)) 43 | */ 44 | filter(key: TKey, exp: (x: FilterBuilderProp) => FilterExpression): ODataQuery; 45 | /** 46 | * group by the selected keys 47 | * 48 | * @param keys keys to be grouped by 49 | * @param aggregate aggregate builder [optional] 50 | * 51 | * @example 52 | * q.groupBy(["email", "surname"], agg => agg 53 | * .countdistinct("phoneNumbers", "count") 54 | * .max("id", "id") 55 | * ) 56 | */ 57 | groupBy(keys: key[], aggregate?: (aggregator: GroupbyBuilder) => GroupbyBuilder): ODataQuery; 58 | /** 59 | * Adds a $orderby operator to the OData query. 60 | * Ordering over relations is supported (check OData implementation for details). 61 | * 62 | * @param key key in T. 63 | * @param order the order of the sort. 64 | * 65 | * @example 66 | * q.orderBy('blogs', 'desc') 67 | */ 68 | orderBy(key: TKey, order?: 'asc' | 'desc'): ODataQuery; 69 | /** 70 | * Adds a $orderby operator to the OData query. 71 | * Ordering over relations is supported (check OData implementation for details). 72 | * 73 | * @param exp a lambda expression that builds the orderby expression from the builder. 74 | * 75 | * @example 76 | * q.orderBy(u => u.blogs().id.desc()) 77 | */ 78 | orderBy(exp: (ob: OrderByBuilder) => OrderBy | OrderByExpression): ODataQuery; 79 | /** 80 | * Adds a $skip and $top to the OData query. 81 | * The pageindex in zero-based. 82 | * This method automatically adds $count=true to the query. 83 | * 84 | * @param pagesize page index ($skip). 85 | * @param page page size ($top) 86 | * 87 | * @example 88 | * q.paginate(50, 0) 89 | */ 90 | paginate(pagesize: number, page?: number): ODataQuery; 91 | /** 92 | * Adds a $skip and $top to the OData query. 93 | * The pageindex in zero-based. 94 | * 95 | * @param options paginate query options 96 | * 97 | * @example 98 | * q.paginate({ pagesize: 50, page: 0, count: false }) 99 | */ 100 | paginate(options: { 101 | pagesize: number; 102 | page?: number; 103 | count?: boolean; 104 | }): ODataQuery; 105 | /** 106 | * Adds a $select operator to the OData query. 107 | * There is only one instance of $select, if you call multiple times it will take the last one. 108 | * 109 | * @param keys the names or a expression of the properties you want to select 110 | * 111 | * @example 112 | * q.select('id', 'title') 113 | * q.select(x => x.address.city) 114 | * q.select('id', x => x.title) 115 | */ 116 | select(...keys: SelectParams): ODataQuery; 117 | /** 118 | * exports query to object key/value 119 | * 120 | * @example 121 | * { 122 | * '$filter': 'order gt 5', 123 | * '$select': 'id' 124 | * } 125 | */ 126 | toObject(): QueryObject; 127 | /** 128 | * exports query to string joined with `&` 129 | * 130 | * @example 131 | * '$filter=order gt 5&$select=id' 132 | */ 133 | toString(): string; 134 | } 135 | export type QueryObject = { 136 | $apply?: string; 137 | $count?: string; 138 | $expand?: string; 139 | $filter?: string; 140 | $orderby?: string; 141 | $select?: string; 142 | $skip?: string; 143 | $top?: string; 144 | }; 145 | -------------------------------------------------------------------------------- /dist/models/odata-query.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=odata-query.js.map -------------------------------------------------------------------------------- /dist/models/odata-query.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"odata-query.js","sourceRoot":"","sources":["../../src/models/odata-query.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/models/query-descriptor.d.ts: -------------------------------------------------------------------------------- 1 | export interface QueryDescriptor { 2 | count?: boolean; 3 | key?: string; 4 | skip?: number; 5 | take?: number; 6 | aggregator?: string; 7 | select: string[]; 8 | filters: string[]; 9 | orderby: string[]; 10 | groupby: string[]; 11 | expands: QueryDescriptor[]; 12 | } 13 | -------------------------------------------------------------------------------- /dist/models/query-descriptor.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=query-descriptor.js.map -------------------------------------------------------------------------------- /dist/models/query-descriptor.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query-descriptor.js","sourceRoot":"","sources":["../../src/models/query-descriptor.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/models/query-expand.d.ts: -------------------------------------------------------------------------------- 1 | import { ODataQuery } from './odata-query'; 2 | type Primitive = number | string | Boolean | Date | Uint8Array; 3 | export type ExpandParam = (exp: ExpandBuilder) => ExpandBuilder; 4 | export type ExpandBuilder = { 5 | [K in keyof T as NonNullable extends Primitive ? never : K]-?: NonNullable extends Array ? NonNullable extends Primitive ? ExpandExpression : ExpandBuilder : ExpandBuilder>; 6 | }; 7 | export interface ExpandExpression { 8 | } 9 | export type ExpandKey = Pick[K]> extends number | string | Boolean | Date | Uint8Array ? never : K; 11 | }[keyof T]>; 12 | export type ExpandQueryComplex = T extends Array ? ExpandArrayQuery : T extends Object ? ExpandObjectQuery : never; 13 | export type ExpandObjectQuery = Pick, 'select' | 'expand'>; 14 | export type ExpandArrayQuery = Omit, 'toString' | 'toObject'>; 15 | export {}; 16 | -------------------------------------------------------------------------------- /dist/models/query-expand.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=query-expand.js.map -------------------------------------------------------------------------------- /dist/models/query-expand.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query-expand.js","sourceRoot":"","sources":["../../src/models/query-expand.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/models/query-filter.d.ts: -------------------------------------------------------------------------------- 1 | export type FilterBuilder = { 2 | [P in keyof T]-?: FilterBuilderProp; 3 | }; 4 | export type FilterBuilderProp = null extends T ? FilterBuilderType & FilterNullable : FilterBuilderType; 5 | export type FilterBuilderType = T extends Array ? FilterCollection : T extends string ? FilterString : T extends number ? FilterNumber : T extends boolean ? FilterBoolean : T extends Date ? FilterDate : T extends Object ? FilterBuilder : never; 6 | export interface StringOptions { 7 | /** Applies `tolower` method to the property */ 8 | caseInsensitive?: boolean; 9 | /** Ignores Guid type casting */ 10 | ignoreGuid?: boolean; 11 | } 12 | export interface FilterExpression { 13 | not(): FilterExpression; 14 | and(exp: FilterExpression): FilterExpression; 15 | or(exp: FilterExpression): FilterExpression; 16 | } 17 | export interface FilterDate { 18 | inTimeSpan(y: number, m?: number, d?: number, h?: number, mm?: number): FilterExpression; 19 | isSame(d: string | Date | FilterDate): FilterExpression; 20 | isSame(d: number | Date | FilterDate, g: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'): FilterExpression; 21 | isAfter(d: string | Date | FilterDate): FilterExpression; 22 | isAfterOrEqual(d: string | Date | FilterDate): FilterExpression; 23 | isBefore(d: string | Date | FilterDate): FilterExpression; 24 | isBeforeOrEqual(d: string | Date | FilterDate): FilterExpression; 25 | } 26 | export interface FilterString { 27 | contains(s: string | FilterString, options?: StringOptions): FilterExpression; 28 | equals(s: string | FilterString, options?: StringOptions): FilterExpression; 29 | notEquals(s: string | FilterString, options?: StringOptions): FilterExpression; 30 | startsWith(s: string | FilterString, options?: StringOptions): FilterExpression; 31 | endsWith(s: string | FilterString, options?: StringOptions): FilterExpression; 32 | in(list: string[]): FilterExpression; 33 | length(): FilterNumber; 34 | tolower(): FilterString; 35 | toupper(): FilterString; 36 | trim(): FilterString; 37 | indexof(s: string): FilterNumber; 38 | substring(n: number): FilterString; 39 | append(s: string): FilterString; 40 | prepend(s: string): FilterString; 41 | } 42 | export interface FilterNumber { 43 | equals(n: number | FilterNumber): FilterExpression; 44 | notEquals(n: number | FilterNumber): FilterExpression; 45 | biggerThan(n: number | FilterNumber): FilterExpression; 46 | biggerThanOrEqual(n: number | FilterNumber): FilterExpression; 47 | lessThan(n: number | FilterNumber): FilterExpression; 48 | lessThanOrEqual(n: number | FilterNumber): FilterExpression; 49 | in(list: number[]): FilterExpression; 50 | } 51 | export interface FilterBoolean { 52 | equals(b: boolean | FilterBoolean): FilterExpression; 53 | notEquals(b: boolean | FilterBoolean): FilterExpression; 54 | } 55 | export interface FilterNullable { 56 | isNull(): FilterExpression; 57 | notNull(): FilterExpression; 58 | } 59 | export interface FilterCollection { 60 | empty(): FilterExpression; 61 | notEmpty(): FilterExpression; 62 | any(c: (arg: FilterBuilderProp) => FilterExpression): FilterExpression; 63 | all(c: (arg: FilterBuilderProp) => FilterExpression): FilterExpression; 64 | } 65 | -------------------------------------------------------------------------------- /dist/models/query-filter.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=query-filter.js.map -------------------------------------------------------------------------------- /dist/models/query-filter.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query-filter.js","sourceRoot":"","sources":["../../src/models/query-filter.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/models/query-groupby.d.ts: -------------------------------------------------------------------------------- 1 | export interface GroupbyBuilder { 2 | sum(prop: keyof T, as: string): GroupbyBuilder; 3 | min(prop: keyof T, as: string): GroupbyBuilder; 4 | max(prop: keyof T, as: string): GroupbyBuilder; 5 | average(prop: keyof T, as: string): GroupbyBuilder; 6 | countdistinct(prop: keyof T, as: string): GroupbyBuilder; 7 | custom(prop: keyof T, aggregator: string, as: string): GroupbyBuilder; 8 | } 9 | -------------------------------------------------------------------------------- /dist/models/query-groupby.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=query-groupby.js.map -------------------------------------------------------------------------------- /dist/models/query-groupby.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query-groupby.js","sourceRoot":"","sources":["../../src/models/query-groupby.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/models/query-orderby.d.ts: -------------------------------------------------------------------------------- 1 | export type OrderByBuilder = { 2 | [P in keyof T]-?: OrderByBuilderTyped; 3 | }; 4 | export type OrderByBuilderTyped = T extends Array ? R extends Object ? OrderByBuilder : never : T extends number | string | boolean | Date | Uint8Array ? OrderBy : T extends Object ? OrderByBuilder : never; 5 | export interface OrderBy { 6 | asc(): OrderByExpression; 7 | desc(): OrderByExpression; 8 | } 9 | export interface OrderByExpression { 10 | } 11 | -------------------------------------------------------------------------------- /dist/models/query-orderby.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=query-orderby.js.map -------------------------------------------------------------------------------- /dist/models/query-orderby.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query-orderby.js","sourceRoot":"","sources":["../../src/models/query-orderby.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/models/query-select.d.ts: -------------------------------------------------------------------------------- 1 | export type SelectParams = Array) => SelectExpression)>; 2 | export type SelectBuilder = { 3 | [P in keyof T]-?: SelectBuilderType; 4 | }; 5 | export type SelectBuilderType = T extends Array ? SelectBuilder : T extends string | number | boolean | Date ? SelectExpression : T extends Object ? SelectBuilder : SelectExpression; 6 | export interface SelectExpression { 7 | } 8 | -------------------------------------------------------------------------------- /dist/models/query-select.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=query-select.js.map -------------------------------------------------------------------------------- /dist/models/query-select.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"query-select.js","sourceRoot":"","sources":["../../src/models/query-select.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odata-fluent-query", 3 | "version": "2.7.3", 4 | "description": "A fluent OData query builder", 5 | "author": "Eduardo Rosostolato", 6 | "main": "index.js", 7 | "types": "index.d.ts", 8 | "license": "MIT", 9 | "homepage": "https://github.com/rosostolato/odata-fluent-query#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/rosostolato/odata-fluent-query.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/rosostolato/odata-fluent-query/issues" 16 | }, 17 | "scripts": { 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:debug": "node --nolazy --inspect-brk ./node_modules/.bin/jest --runInBand --no-cache --watch", 21 | "coverage": "jest --coverage", 22 | "ci": "jest --coverage --verbose", 23 | "build": "rimraf dist && tsc && copyfiles package.json README.md LICENSE dist && npm run postBuildMsg", 24 | "postBuildMsg": "echo \"Please update the main entry on dist/package.json\"" 25 | }, 26 | "dependencies": { 27 | "validator": "^13.7.0" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^29.1.1", 31 | "@types/validator": "^13.7.7", 32 | "copyfiles": "^2.4.1", 33 | "jest": "^29.1.2", 34 | "rimraf": "^3.0.2", 35 | "ts-jest": "^29.0.3", 36 | "ts-node": "^10.9.1", 37 | "typescript": "^4.8.4" 38 | } 39 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | testRegex: '(/tests/.*\\.(test|spec))\\.(jsx?|tsx?)$', 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 8 | coverageDirectory: 'coverage', 9 | collectCoverageFrom: ['src/**/*.{ts,js}', '!src/**/*.d.ts'], 10 | verbose: true, 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odata-fluent-query", 3 | "version": "2.7.3", 4 | "description": "A fluent OData query builder", 5 | "author": "Eduardo Rosostolato", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "license": "MIT", 9 | "homepage": "https://github.com/rosostolato/odata-fluent-query#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/rosostolato/odata-fluent-query.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/rosostolato/odata-fluent-query/issues" 16 | }, 17 | "scripts": { 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:debug": "node --nolazy --inspect-brk ./node_modules/.bin/jest --runInBand --no-cache --watch", 21 | "coverage": "jest --coverage", 22 | "ci": "jest --coverage --verbose", 23 | "build": "rimraf dist && tsc && copyfiles package.json README.md LICENSE dist && npm run postBuildMsg", 24 | "postBuildMsg": "echo \"Please update the main entry on dist/package.json\"" 25 | }, 26 | "dependencies": { 27 | "validator": "^13.7.0" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^29.1.1", 31 | "@types/validator": "^13.7.7", 32 | "copyfiles": "^2.4.1", 33 | "jest": "^29.1.2", 34 | "rimraf": "^3.0.2", 35 | "ts-jest": "^29.0.3", 36 | "ts-node": "^10.9.1", 37 | "typescript": "^4.8.4" 38 | } 39 | } -------------------------------------------------------------------------------- /src/builders/create-expand.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models' 2 | import { createQuery, createQueryDescriptor } from './create-query' 3 | 4 | function makeExpand(key = ''): any { 5 | return new Proxy( 6 | {}, 7 | { 8 | get(_, prop) { 9 | if (prop === '_key') return key.slice(1) 10 | return makeExpand(`${key}/${String(prop)}`) 11 | }, 12 | } 13 | ) 14 | } 15 | 16 | export function createExpand(descriptor: QueryDescriptor) { 17 | return (keyOrExp: string | Function, query?: Function) => { 18 | let key: string = '' 19 | if (typeof keyOrExp === 'function') { 20 | const exp: any = keyOrExp(makeExpand()) 21 | key = exp._key 22 | } else { 23 | key = String(keyOrExp) 24 | } 25 | const expand = createQuery(createQueryDescriptor(key)) 26 | const result = query?.(expand) || expand 27 | const newDescriptor: QueryDescriptor = { 28 | ...descriptor, 29 | expands: descriptor.expands.concat(result._descriptor), 30 | } 31 | return createQuery(newDescriptor) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/builders/create-filter.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor, StringOptions } from '../models' 2 | import { createQuery } from './create-query' 3 | import isUUID from 'validator/lib/isUUID' 4 | 5 | export function getFuncArgs(func: Function): string[] { 6 | const [, , paramStr] = /(function)?(.*?)(?=[={])/.exec(func.toString()) ?? [] 7 | return (paramStr ?? '') 8 | .replace('=>', '') 9 | .replace('(', '') 10 | .replace(')', '') 11 | .split(',') 12 | .map(s => s.trim()) 13 | } 14 | 15 | export function dateToObject(d: Date) { 16 | if (typeof d === 'string') { 17 | d = new Date(d) 18 | } 19 | 20 | return { 21 | year: d.getFullYear(), 22 | month: d.getMonth(), 23 | day: d.getFullYear(), 24 | hour: d.getFullYear(), 25 | minute: d.getFullYear(), 26 | second: d.getFullYear(), 27 | } 28 | } 29 | 30 | export function makeExp(exp: string): any { 31 | const _get = (checkParetheses = false) => { 32 | if (!checkParetheses) { 33 | return exp 34 | } else if (exp.indexOf(' or ') > -1 || exp.indexOf(' and ') > -1) { 35 | return `(${exp})` 36 | } else { 37 | return exp 38 | } 39 | } 40 | 41 | return { 42 | _get, 43 | not: () => makeExp(`not (${exp})`), 44 | and: (exp: any) => makeExp(`${_get()} and ${exp._get(true)}`), 45 | or: (exp: any) => makeExp(`${_get()} or ${exp._get(true)}`), 46 | } 47 | } 48 | 49 | function filterBuilder(key: string) { 50 | const arrFuncBuilder = (method: 'any' | 'all') => (exp: Function) => { 51 | const [arg] = getFuncArgs(exp) 52 | const builder = exp(makeFilter(arg)) 53 | const expr = builder._get() 54 | return makeExp(`${key}/${method}(${arg}: ${expr})`) 55 | } 56 | 57 | const strFuncBuilder = 58 | (method: 'contains' | 'startswith' | 'endswith') => 59 | (s: any, opt?: StringOptions) => { 60 | if (opt?.caseInsensitive) { 61 | return makeExp( 62 | `${method}(tolower(${key}), ${ 63 | typeof s == 'string' 64 | ? `'${s.toLocaleLowerCase()}'` 65 | : `tolower(${s._key})` 66 | })` 67 | ) 68 | } else if (s.getPropName) { 69 | return makeExp(`${method}(${key}, ${s._key})`) 70 | } else { 71 | return makeExp( 72 | `${method}(${key}, ${typeof s == 'string' ? `'${s}'` : s})` 73 | ) 74 | } 75 | } 76 | 77 | const equalityBuilder = (t: 'eq' | 'ne') => (x: any, opt?: StringOptions) => { 78 | switch (typeof x) { 79 | case 'string': 80 | if (isUUID(x) && !opt?.ignoreGuid) { 81 | return makeExp(`${key} ${t} ${x}`) // no quote around ${x} 82 | } else if (opt?.caseInsensitive) { 83 | return makeExp(`tolower(${key}) ${t} '${x.toLocaleLowerCase()}'`) 84 | } else { 85 | return makeExp(`${key} ${t} '${x}'`) 86 | } 87 | 88 | case 'number': 89 | return makeExp(`${key} ${t} ${x}`) 90 | 91 | case 'boolean': 92 | return makeExp(`${key} ${t} ${x}`) 93 | 94 | default: 95 | if (x && opt?.caseInsensitive) { 96 | return makeExp(`tolower(${key}) ${t} tolower(${x._key})`) 97 | } else { 98 | return makeExp(`${key} ${t} ${x?._key || null}`) 99 | } 100 | } 101 | } 102 | 103 | const dateComparison = (compare: 'ge' | 'gt' | 'le' | 'lt') => (d: any) => { 104 | if (typeof d === 'string') return makeExp(`${key} ${compare} ${d}`) 105 | else if (d instanceof Date) 106 | return makeExp(`${key} ${compare} ${d.toISOString()}`) 107 | else return makeExp(`${key} ${compare} ${d._key}`) 108 | } 109 | 110 | const numberComparison = (compare: 'ge' | 'gt' | 'le' | 'lt') => (n: any) => 111 | makeExp(`${key} ${compare} ${typeof n == 'number' ? n : n._key}`) 112 | 113 | return { 114 | _key: key, 115 | 116 | ///////////////////// 117 | // FilterBuilderDate 118 | inTimeSpan: ( 119 | y: number, 120 | m?: number, 121 | d?: number, 122 | h?: number, 123 | mm?: number 124 | ) => { 125 | let exps = [`year(${key}) eq ${y}`] 126 | if (m != undefined) exps.push(`month(${key}) eq ${m}`) 127 | if (d != undefined) exps.push(`day(${key}) eq ${d}`) 128 | if (h != undefined) exps.push(`hour(${key}) eq ${h}`) 129 | if (mm != undefined) exps.push(`minute(${key}) eq ${mm}`) 130 | return makeExp('(' + exps.join(') and (') + ')') 131 | }, 132 | 133 | isSame: ( 134 | x: any, 135 | g?: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' 136 | ) => { 137 | if (typeof x === 'string') { 138 | return makeExp(`${key} eq ${x}`) 139 | } else if (typeof x === 'number') { 140 | return makeExp(`${g}(${key}) eq ${x}`) 141 | } else if (x instanceof Date) { 142 | if (g == null) { 143 | return makeExp(`${key} eq ${x.toISOString()}`) 144 | } else { 145 | const o = dateToObject(x) 146 | return makeExp(`${g}(${key}) eq ${o[g]}`) 147 | } 148 | } else { 149 | return makeExp(`${g}(${key}) eq ${g}(${x._key})`) 150 | } 151 | }, 152 | 153 | isAfter: dateComparison('gt'), 154 | isBefore: dateComparison('lt'), 155 | isAfterOrEqual: dateComparison('ge'), 156 | isBeforeOrEqual: dateComparison('le'), 157 | 158 | ///////////////////// 159 | // FilterBuilderArray 160 | empty: () => makeExp(`not ${key}/any()`), 161 | notEmpty: () => makeExp(`${key}/any()`), 162 | any: arrFuncBuilder('any'), 163 | all: arrFuncBuilder('all'), 164 | 165 | ////////////////////// 166 | // FilterBuilderString 167 | isNull: () => makeExp(`${key} eq null`), 168 | notNull: () => makeExp(`${key} ne null`), 169 | contains: strFuncBuilder('contains'), 170 | startsWith: strFuncBuilder('startswith'), 171 | endsWith: strFuncBuilder('endswith'), 172 | tolower: () => makeFilter(`tolower(${key})`), 173 | toupper: () => makeFilter(`toupper(${key})`), 174 | length: () => makeFilter(`length(${key})`), 175 | trim: () => makeFilter(`trim(${key})`), 176 | indexof: (s: string) => makeFilter(`indexof(${key}, '${s}')`), 177 | substring: (n: number) => makeFilter(`substring(${key}, ${n})`), 178 | append: (s: string) => makeFilter(`concat(${key}, '${s}')`), 179 | prepend: (s: string) => makeFilter(`concat('${s}', ${key})`), 180 | 181 | ////////////////////// 182 | // FilterBuilderNumber 183 | biggerThan: numberComparison('gt'), 184 | lessThan: numberComparison('lt'), 185 | biggerThanOrEqual: numberComparison('ge'), 186 | lessThanOrEqual: numberComparison('le'), 187 | 188 | //////////////////////////////// 189 | // FilterBuilder Generic Methods 190 | equals: equalityBuilder('eq'), 191 | notEquals: equalityBuilder('ne'), 192 | 193 | in(arr: (number | string)[]) { 194 | const list = arr 195 | .map(x => (typeof x === 'string' ? `'${x}'` : x)) 196 | .join(',') 197 | return makeExp(`${key} in (${list})`) 198 | }, 199 | } 200 | } 201 | 202 | function makeFilter(prefix = ''): any { 203 | return new Proxy( 204 | {}, 205 | { 206 | get(_, prop) { 207 | const methods: any = filterBuilder(prefix) 208 | const key = prefix ? `${prefix}/${String(prop)}` : String(prop) 209 | return methods?.[prop] ? methods[prop] : makeFilter(String(key)) 210 | }, 211 | } 212 | ) 213 | } 214 | 215 | export function createFilter(descriptor: QueryDescriptor) { 216 | return (keyOrExp: any, exp?: any) => { 217 | const expr = 218 | typeof keyOrExp === 'string' 219 | ? exp(filterBuilder(keyOrExp)) 220 | : keyOrExp(makeFilter()) 221 | 222 | return createQuery({ 223 | ...descriptor, 224 | filters: descriptor.filters.concat(expr._get()), 225 | }) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/builders/create-groupby.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models' 2 | import { createQuery } from './create-query' 3 | 4 | export function groupbyBuilder(aggregator: string[] = []) { 5 | const custom = (prop: string, aggreg: string, as: string) => 6 | groupbyBuilder(aggregator.concat(`${prop} with ${aggreg} as ${as}`)) 7 | 8 | return { 9 | aggregator, 10 | sum(prop: string, as: string) { 11 | return custom(prop, 'sum', as) 12 | }, 13 | min(prop: string, as: string) { 14 | return custom(prop, 'min', as) 15 | }, 16 | max(prop: string, as: string) { 17 | return custom(prop, 'max', as) 18 | }, 19 | average(prop: string, as: string) { 20 | return custom(prop, 'average', as) 21 | }, 22 | countdistinct(prop: string, as: string) { 23 | return custom(prop, 'countdistinct', as) 24 | }, 25 | custom, 26 | } 27 | } 28 | 29 | export function createGroupby(descriptor: QueryDescriptor) { 30 | return (keys: string[], aggregate?: Function) => { 31 | const agg = groupbyBuilder() 32 | const result = aggregate?.(agg) || agg 33 | 34 | return createQuery({ 35 | ...descriptor, 36 | groupby: keys.map(String), 37 | aggregator: result.aggregator.join(', ') || null, 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/builders/create-orderby.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models' 2 | import { createQuery } from './create-query' 3 | 4 | function makeOrderby(key = ''): any { 5 | if (key[0] === '/') { 6 | key = key.slice(1) 7 | } 8 | 9 | const methods: any = { 10 | _key: key, 11 | asc: () => makeOrderby(`${key} asc`), 12 | desc: () => makeOrderby(`${key} desc`), 13 | } 14 | 15 | return new Proxy( 16 | {}, 17 | { 18 | get(_, prop) { 19 | return methods[prop] || makeOrderby(`${key}/${String(prop)}`) 20 | }, 21 | } 22 | ) 23 | } 24 | 25 | export function createOrderby(descriptor: QueryDescriptor) { 26 | return (keyOrExp: any, order?: 'asc' | 'desc') => { 27 | let expr = 28 | typeof keyOrExp === 'string' 29 | ? makeOrderby(keyOrExp) 30 | : keyOrExp(makeOrderby()) 31 | 32 | if (order) { 33 | expr = expr[order]() 34 | } 35 | 36 | return createQuery({ 37 | ...descriptor, 38 | orderby: descriptor.orderby.concat(expr['_key']), 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/builders/create-paginate.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models' 2 | import { createQuery } from './create-query' 3 | 4 | export function createPaginate(descriptor: QueryDescriptor) { 5 | return (sizeOrOptions: any, page: number) => { 6 | let data: { 7 | page: number 8 | count?: boolean 9 | pagesize: number 10 | } 11 | if (typeof sizeOrOptions === 'number') { 12 | data = { 13 | page: page, 14 | count: true, 15 | pagesize: sizeOrOptions, 16 | } 17 | } else { 18 | data = sizeOrOptions 19 | 20 | if (data.count === undefined) { 21 | data.count = true 22 | } 23 | } 24 | const queryDescriptor: QueryDescriptor = { 25 | ...descriptor, 26 | take: data.pagesize, 27 | skip: data.pagesize * data.page, 28 | count: data.count, 29 | } 30 | if (!queryDescriptor.skip) { 31 | queryDescriptor.skip = undefined 32 | } 33 | return createQuery(queryDescriptor) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/builders/create-query.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor, QueryObject } from '../models' 2 | import { createExpand } from './create-expand' 3 | import { createFilter } from './create-filter' 4 | import { createGroupby } from './create-groupby' 5 | import { createOrderby } from './create-orderby' 6 | import { createPaginate } from './create-paginate' 7 | import { createSelect } from './create-select' 8 | import { makeQuery } from './query-builder' 9 | 10 | export function createQueryDescriptor(key?: string): QueryDescriptor { 11 | return { 12 | key: key, 13 | skip: undefined, 14 | take: undefined, 15 | count: false, 16 | aggregator: undefined, 17 | filters: [], 18 | expands: [], 19 | orderby: [], 20 | groupby: [], 21 | select: [], 22 | } 23 | } 24 | 25 | export function createQuery(descriptor: QueryDescriptor): any { 26 | return { 27 | _descriptor: descriptor, 28 | expand: createExpand(descriptor), 29 | filter: createFilter(descriptor), 30 | groupBy: createGroupby(descriptor), 31 | orderBy: createOrderby(descriptor), 32 | paginate: createPaginate(descriptor), 33 | select: createSelect(descriptor), 34 | count() { 35 | return createQuery({ 36 | ...descriptor, 37 | count: true, 38 | }) 39 | }, 40 | toObject(): QueryObject { 41 | return makeQuery(descriptor).reduce((obj, x) => { 42 | obj[x.key as keyof QueryObject] = x.value 43 | return obj 44 | }, {} as QueryObject) 45 | }, 46 | toString(): string { 47 | return makeQuery(descriptor) 48 | .map(p => `${p.key}=${p.value}`) 49 | .join('&') 50 | }, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/builders/create-select.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models' 2 | import { createQuery } from './create-query' 3 | 4 | function makeSelect(key = ''): any { 5 | return new Proxy( 6 | {}, 7 | { 8 | get(_, prop) { 9 | if (prop === '_key') return key.slice(1) 10 | return makeSelect(`${key}/${String(prop)}`) 11 | }, 12 | } 13 | ) 14 | } 15 | 16 | export function createSelect(descriptor: QueryDescriptor) { 17 | return (...keys: any[]) => { 18 | const _keys = keys 19 | .map(keyOrExp => { 20 | if (typeof keyOrExp === 'function') { 21 | const exp: any = keyOrExp(makeSelect()) 22 | return exp._key 23 | } else { 24 | return String(keyOrExp) 25 | } 26 | }) 27 | .filter((k, i, arr) => arr.indexOf(k) === i) // unique 28 | return createQuery({ 29 | ...descriptor, 30 | select: _keys, 31 | expands: descriptor.expands.filter(e => 32 | _keys.some(k => e.key == String(k)) 33 | ), 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/builders/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-filter' 2 | export * from './create-groupby' 3 | export * from './create-orderby' 4 | export * from './create-query' 5 | export * from './create-select' 6 | export * from './query-builder' 7 | -------------------------------------------------------------------------------- /src/builders/query-builder.ts: -------------------------------------------------------------------------------- 1 | import { QueryDescriptor } from '../models' 2 | 3 | export interface KeyValue { 4 | key: string 5 | value: T 6 | } 7 | 8 | export function makeQuery(qd: QueryDescriptor): KeyValue[] { 9 | let params: { 10 | key: string 11 | value: string 12 | }[] = [] 13 | 14 | if (qd.filters.length) { 15 | if (qd.filters.length > 1) { 16 | params.push({ 17 | key: '$filter', 18 | value: `${qd.filters.map(makeQueryParentheses).join(' and ')}`, 19 | }) 20 | } else { 21 | params.push({ 22 | key: '$filter', 23 | value: `${qd.filters.join()}`, 24 | }) 25 | } 26 | } 27 | 28 | if (qd.groupby.length) { 29 | let group = `groupby((${qd.groupby.join(', ')})` 30 | 31 | if (qd.aggregator) { 32 | group += `, aggregate(${qd.aggregator})` 33 | } 34 | 35 | params.push({ 36 | key: '$apply', 37 | value: group + ')', 38 | }) 39 | } 40 | 41 | if (qd.expands.length) { 42 | params.push({ 43 | key: '$expand', 44 | value: `${qd.expands.map(makeRelationQuery).join(',')}`, 45 | }) 46 | } 47 | 48 | if (qd.select.length) { 49 | params.push({ 50 | key: '$select', 51 | value: `${qd.select.join(',')}`, 52 | }) 53 | } 54 | 55 | if (qd.orderby.length) { 56 | params.push({ 57 | key: '$orderby', 58 | value: `${qd.orderby.join(', ')}`, 59 | }) 60 | } 61 | 62 | if (qd.skip != null) { 63 | params.push({ 64 | key: '$skip', 65 | value: `${qd.skip}`, 66 | }) 67 | } 68 | 69 | if (qd.take != null) { 70 | params.push({ 71 | key: '$top', 72 | value: `${qd.take}`, 73 | }) 74 | } 75 | 76 | if (qd.count == true) { 77 | params.push({ 78 | key: '$count', 79 | value: `true`, 80 | }) 81 | } 82 | 83 | return params 84 | } 85 | 86 | export function makeQueryParentheses(query: string): string { 87 | if (query.indexOf(' or ') > -1 || query.indexOf(' and ') > -1) { 88 | return `(${query})` 89 | } 90 | 91 | return query 92 | } 93 | 94 | export function makeRelationQuery(rqd: QueryDescriptor): string { 95 | let expand: string = rqd.key || '' 96 | 97 | if ( 98 | rqd.filters.length || 99 | rqd.orderby.length || 100 | rqd.select.length || 101 | rqd.expands.length || 102 | rqd.skip != null || 103 | rqd.take != null || 104 | rqd.count != false 105 | ) { 106 | expand += `(` 107 | 108 | let operators = [] 109 | 110 | if (rqd.skip != null) { 111 | operators.push(`$skip=${rqd.skip}`) 112 | } 113 | 114 | if (rqd.take != null) { 115 | operators.push(`$top=${rqd.take}`) 116 | } 117 | 118 | if (rqd.count == true) { 119 | operators.push(`$count=true`) 120 | } 121 | 122 | if (rqd.orderby.length) { 123 | operators.push(`$orderby=${rqd.orderby.join(',')}`) 124 | } 125 | 126 | if (rqd.select.length) { 127 | operators.push(`$select=${rqd.select.join(',')}`) 128 | } 129 | 130 | if (rqd.filters.length) { 131 | if (rqd.filters.length > 1) { 132 | operators.push( 133 | `$filter=${rqd.filters.map(makeQueryParentheses).join(' and ')}` 134 | ) 135 | } else { 136 | operators.push(`$filter=${rqd.filters.join()}`) 137 | } 138 | } 139 | 140 | if (rqd.expands.length) { 141 | operators.push(`$expand=${rqd.expands.map(makeRelationQuery).join(',')}`) 142 | } 143 | 144 | expand += operators.join(';') + ')' 145 | } 146 | 147 | return expand 148 | } 149 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createQuery, createQueryDescriptor } from './builders' 2 | import { ODataQuery } from './models' 3 | 4 | export function odataQuery(): ODataQuery { 5 | const defaultDescriptor = createQueryDescriptor() 6 | return createQuery(defaultDescriptor) 7 | } 8 | 9 | export * from './models' 10 | export default odataQuery 11 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './odata-query' 2 | export * from './query-descriptor' 3 | export * from './query-filter' 4 | export * from './query-groupby' 5 | export * from './query-orderby' 6 | export * from './query-select' 7 | -------------------------------------------------------------------------------- /src/models/odata-query.ts: -------------------------------------------------------------------------------- 1 | import { ExpandParam, ExpandKey, ExpandQueryComplex } from './query-expand' 2 | import { 3 | FilterBuilder, 4 | FilterBuilderProp, 5 | FilterExpression, 6 | } from './query-filter' 7 | import { GroupbyBuilder } from './query-groupby' 8 | import { OrderBy, OrderByBuilder, OrderByExpression } from './query-orderby' 9 | import { SelectParams } from './query-select' 10 | 11 | export interface ODataQuery { 12 | /** 13 | * set $count=true 14 | */ 15 | count(): ODataQuery 16 | 17 | /** 18 | * Adds a $expand operator to the OData query. 19 | * Multiple calls to Expand will expand all the relations, e.g.: $expand=rel1(...),rel2(...). 20 | * The lambda in the second parameter allows you to build a complex inner query. 21 | * 22 | * @param key the name of the relation. 23 | * @param query a lambda expression that build the subquery from the querybuilder. 24 | * 25 | * @example 26 | * q.exand('blogs', q => q.select('id', 'title')) 27 | * q.exand(u => u.blogs, q => q.select('id', 'title')) 28 | */ 29 | expand, U = Required[Tkey]>( 30 | key: Tkey | ExpandParam, 31 | query?: (x: ExpandQueryComplex) => ExpandQueryComplex 32 | ): ODataQuery 33 | 34 | /** 35 | * Adds a $filter operator to the OData query. 36 | * Multiple calls to Filter will be merged with `and`. 37 | * 38 | * @param exp a lambda expression that builds an expression from the builder. 39 | * 40 | * @example 41 | * q.filter(u => u.id.equals(1)) 42 | */ 43 | filter(exp: (x: FilterBuilder) => FilterExpression): ODataQuery 44 | /** 45 | * Adds a $filter operator to the OData query. 46 | * Multiple calls to Filter will be merged with `and`. 47 | * 48 | * @param key property key selector. 49 | * @param exp a lambda expression that builds an expression from the builder. 50 | * 51 | * @example 52 | * q.filter('id', id => id.equals(1)) 53 | */ 54 | filter( 55 | key: TKey, 56 | exp: (x: FilterBuilderProp) => FilterExpression 57 | ): ODataQuery 58 | 59 | /** 60 | * group by the selected keys 61 | * 62 | * @param keys keys to be grouped by 63 | * @param aggregate aggregate builder [optional] 64 | * 65 | * @example 66 | * q.groupBy(["email", "surname"], agg => agg 67 | * .countdistinct("phoneNumbers", "count") 68 | * .max("id", "id") 69 | * ) 70 | */ 71 | groupBy( 72 | keys: key[], 73 | aggregate?: (aggregator: GroupbyBuilder) => GroupbyBuilder 74 | ): ODataQuery 75 | 76 | /** 77 | * Adds a $orderby operator to the OData query. 78 | * Ordering over relations is supported (check OData implementation for details). 79 | * 80 | * @param key key in T. 81 | * @param order the order of the sort. 82 | * 83 | * @example 84 | * q.orderBy('blogs', 'desc') 85 | */ 86 | orderBy( 87 | key: TKey, 88 | order?: 'asc' | 'desc' 89 | ): ODataQuery 90 | /** 91 | * Adds a $orderby operator to the OData query. 92 | * Ordering over relations is supported (check OData implementation for details). 93 | * 94 | * @param exp a lambda expression that builds the orderby expression from the builder. 95 | * 96 | * @example 97 | * q.orderBy(u => u.blogs().id.desc()) 98 | */ 99 | orderBy( 100 | exp: (ob: OrderByBuilder) => OrderBy | OrderByExpression 101 | ): ODataQuery 102 | 103 | /** 104 | * Adds a $skip and $top to the OData query. 105 | * The pageindex in zero-based. 106 | * This method automatically adds $count=true to the query. 107 | * 108 | * @param pagesize page index ($skip). 109 | * @param page page size ($top) 110 | * 111 | * @example 112 | * q.paginate(50, 0) 113 | */ 114 | paginate(pagesize: number, page?: number): ODataQuery 115 | /** 116 | * Adds a $skip and $top to the OData query. 117 | * The pageindex in zero-based. 118 | * 119 | * @param options paginate query options 120 | * 121 | * @example 122 | * q.paginate({ pagesize: 50, page: 0, count: false }) 123 | */ 124 | paginate(options: { 125 | pagesize: number 126 | page?: number 127 | count?: boolean 128 | }): ODataQuery 129 | 130 | /** 131 | * Adds a $select operator to the OData query. 132 | * There is only one instance of $select, if you call multiple times it will take the last one. 133 | * 134 | * @param keys the names or a expression of the properties you want to select 135 | * 136 | * @example 137 | * q.select('id', 'title') 138 | * q.select(x => x.address.city) 139 | * q.select('id', x => x.title) 140 | */ 141 | select(...keys: SelectParams): ODataQuery 142 | 143 | /** 144 | * exports query to object key/value 145 | * 146 | * @example 147 | * { 148 | * '$filter': 'order gt 5', 149 | * '$select': 'id' 150 | * } 151 | */ 152 | toObject(): QueryObject 153 | 154 | /** 155 | * exports query to string joined with `&` 156 | * 157 | * @example 158 | * '$filter=order gt 5&$select=id' 159 | */ 160 | toString(): string 161 | } 162 | 163 | export type QueryObject = { 164 | $apply?: string 165 | $count?: string 166 | $expand?: string 167 | $filter?: string 168 | $orderby?: string 169 | $select?: string 170 | $skip?: string 171 | $top?: string 172 | } 173 | -------------------------------------------------------------------------------- /src/models/query-descriptor.ts: -------------------------------------------------------------------------------- 1 | export interface QueryDescriptor { 2 | count?: boolean 3 | key?: string 4 | skip?: number 5 | take?: number 6 | aggregator?: string 7 | select: string[] 8 | filters: string[] 9 | orderby: string[] 10 | groupby: string[] 11 | expands: QueryDescriptor[] 12 | } 13 | -------------------------------------------------------------------------------- /src/models/query-expand.ts: -------------------------------------------------------------------------------- 1 | import { ODataQuery } from './odata-query' 2 | 3 | type Primitive = number | string | Boolean | Date | Uint8Array 4 | 5 | export type ExpandParam = (exp: ExpandBuilder) => ExpandBuilder 6 | 7 | export type ExpandBuilder = { 8 | [K in keyof T as NonNullable extends Primitive 9 | ? never 10 | : K]-?: NonNullable extends Array 11 | ? NonNullable extends Primitive 12 | ? ExpandExpression 13 | : ExpandBuilder 14 | : ExpandBuilder> 15 | } 16 | 17 | export interface ExpandExpression {} 18 | 19 | export type ExpandKey = Pick< 20 | T, 21 | { 22 | [K in keyof T]: NonNullable[K]> extends 23 | | number 24 | | string 25 | | Boolean 26 | | Date 27 | | Uint8Array 28 | ? never 29 | : K 30 | }[keyof T] 31 | > 32 | 33 | export type ExpandQueryComplex = T extends Array 34 | ? ExpandArrayQuery 35 | : T extends Object 36 | ? ExpandObjectQuery 37 | : never 38 | 39 | export type ExpandObjectQuery = Pick, 'select' | 'expand'> 40 | export type ExpandArrayQuery = Omit, 'toString' | 'toObject'> 41 | -------------------------------------------------------------------------------- /src/models/query-filter.ts: -------------------------------------------------------------------------------- 1 | export type FilterBuilder = { 2 | [P in keyof T]-?: FilterBuilderProp 3 | } 4 | 5 | export type FilterBuilderProp = null extends T 6 | ? FilterBuilderType & FilterNullable 7 | : FilterBuilderType 8 | 9 | export type FilterBuilderType = T extends Array 10 | ? FilterCollection 11 | : T extends string 12 | ? FilterString 13 | : T extends number 14 | ? FilterNumber 15 | : T extends boolean 16 | ? FilterBoolean 17 | : T extends Date 18 | ? FilterDate 19 | : T extends Object 20 | ? FilterBuilder 21 | : never 22 | 23 | export interface StringOptions { 24 | /** Applies `tolower` method to the property */ 25 | caseInsensitive?: boolean 26 | /** Ignores Guid type casting */ 27 | ignoreGuid?: boolean 28 | } 29 | 30 | export interface FilterExpression { 31 | not(): FilterExpression 32 | and(exp: FilterExpression): FilterExpression 33 | or(exp: FilterExpression): FilterExpression 34 | } 35 | 36 | export interface FilterDate { 37 | inTimeSpan( 38 | y: number, 39 | m?: number, 40 | d?: number, 41 | h?: number, 42 | mm?: number 43 | ): FilterExpression 44 | isSame(d: string | Date | FilterDate): FilterExpression 45 | isSame( 46 | d: number | Date | FilterDate, 47 | g: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' 48 | ): FilterExpression 49 | isAfter(d: string | Date | FilterDate): FilterExpression 50 | isAfterOrEqual(d: string | Date | FilterDate): FilterExpression 51 | isBefore(d: string | Date | FilterDate): FilterExpression 52 | isBeforeOrEqual(d: string | Date | FilterDate): FilterExpression 53 | } 54 | 55 | export interface FilterString { 56 | contains(s: string | FilterString, options?: StringOptions): FilterExpression 57 | equals(s: string | FilterString, options?: StringOptions): FilterExpression 58 | notEquals(s: string | FilterString, options?: StringOptions): FilterExpression 59 | startsWith( 60 | s: string | FilterString, 61 | options?: StringOptions 62 | ): FilterExpression 63 | endsWith(s: string | FilterString, options?: StringOptions): FilterExpression 64 | in(list: string[]): FilterExpression 65 | length(): FilterNumber 66 | tolower(): FilterString 67 | toupper(): FilterString 68 | trim(): FilterString 69 | indexof(s: string): FilterNumber 70 | substring(n: number): FilterString 71 | append(s: string): FilterString 72 | prepend(s: string): FilterString 73 | } 74 | 75 | export interface FilterNumber { 76 | equals(n: number | FilterNumber): FilterExpression 77 | notEquals(n: number | FilterNumber): FilterExpression 78 | biggerThan(n: number | FilterNumber): FilterExpression 79 | biggerThanOrEqual(n: number | FilterNumber): FilterExpression 80 | lessThan(n: number | FilterNumber): FilterExpression 81 | lessThanOrEqual(n: number | FilterNumber): FilterExpression 82 | in(list: number[]): FilterExpression 83 | } 84 | 85 | export interface FilterBoolean { 86 | equals(b: boolean | FilterBoolean): FilterExpression 87 | notEquals(b: boolean | FilterBoolean): FilterExpression 88 | } 89 | 90 | export interface FilterNullable { 91 | isNull(): FilterExpression 92 | notNull(): FilterExpression 93 | } 94 | 95 | export interface FilterCollection { 96 | empty(): FilterExpression 97 | notEmpty(): FilterExpression 98 | any(c: (arg: FilterBuilderProp) => FilterExpression): FilterExpression 99 | all(c: (arg: FilterBuilderProp) => FilterExpression): FilterExpression 100 | } 101 | -------------------------------------------------------------------------------- /src/models/query-groupby.ts: -------------------------------------------------------------------------------- 1 | export interface GroupbyBuilder { 2 | sum(prop: keyof T, as: string): GroupbyBuilder 3 | min(prop: keyof T, as: string): GroupbyBuilder 4 | max(prop: keyof T, as: string): GroupbyBuilder 5 | average(prop: keyof T, as: string): GroupbyBuilder 6 | countdistinct(prop: keyof T, as: string): GroupbyBuilder 7 | custom(prop: keyof T, aggregator: string, as: string): GroupbyBuilder 8 | } 9 | -------------------------------------------------------------------------------- /src/models/query-orderby.ts: -------------------------------------------------------------------------------- 1 | export type OrderByBuilder = { 2 | [P in keyof T]-?: OrderByBuilderTyped 3 | } 4 | 5 | export type OrderByBuilderTyped = T extends Array 6 | ? R extends Object 7 | ? OrderByBuilder 8 | : never 9 | : T extends number | string | boolean | Date | Uint8Array 10 | ? OrderBy 11 | : T extends Object 12 | ? OrderByBuilder 13 | : never 14 | 15 | export interface OrderBy { 16 | asc(): OrderByExpression 17 | desc(): OrderByExpression 18 | } 19 | 20 | export interface OrderByExpression {} 21 | -------------------------------------------------------------------------------- /src/models/query-select.ts: -------------------------------------------------------------------------------- 1 | export type SelectParams = Array< 2 | Tkey | ((exp: SelectBuilder) => SelectExpression) 3 | > 4 | 5 | export type SelectBuilder = { 6 | [P in keyof T]-?: SelectBuilderType 7 | } 8 | 9 | export type SelectBuilderType = T extends Array 10 | ? SelectBuilder 11 | : T extends string | number | boolean | Date 12 | ? SelectExpression 13 | : T extends Object 14 | ? SelectBuilder 15 | : SelectExpression 16 | 17 | export interface SelectExpression {} 18 | -------------------------------------------------------------------------------- /tests/data/models.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number | null 3 | email: string | null 4 | surname?: string 5 | givenName: string 6 | createDate: Date | null 7 | accountEnabled: boolean 8 | 9 | // simple array 10 | phoneNumbers: string[] 11 | 12 | // one2one 13 | address: Address 14 | address2: Address | null 15 | 16 | // one2many 17 | posts: Post[] | null 18 | } 19 | 20 | export interface Address { 21 | code: number 22 | street: string 23 | user: User 24 | } 25 | 26 | export interface Post { 27 | id: number 28 | date: Date 29 | content: string 30 | comments: (string | null)[] 31 | } 32 | -------------------------------------------------------------------------------- /tests/expand.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from './data/models' 2 | import { odataQuery } from '../src' 3 | 4 | describe('testing ODataQuery expand', () => { 5 | // one2one relation 6 | it('expand', () => { 7 | const query = odataQuery() 8 | const actual = query.expand('address').toString() 9 | const expected = '$expand=address' 10 | expect(actual).toBe(expected) 11 | }) 12 | 13 | it('expand and select', () => { 14 | const query = odataQuery() 15 | const actual = query.expand('address', q => q.select('code')).toString() 16 | const expected = '$expand=address($select=code)' 17 | expect(actual).toBe(expected) 18 | }) 19 | 20 | it('expand and select optional', () => { 21 | const query = odataQuery() 22 | const actual = query.expand('address2', q => q.select('code')).toString() 23 | const expected = '$expand=address2($select=code)' 24 | expect(actual).toBe(expected) 25 | }) 26 | 27 | it('expand twice', () => { 28 | const query = odataQuery() 29 | const actual = query 30 | .expand('address', q => q.expand('user', q => q.select('id'))) 31 | .toString() 32 | const expected = '$expand=address($expand=user($select=id))' 33 | expect(actual).toBe(expected) 34 | }) 35 | 36 | // one2many relation 37 | it('expand and filter', () => { 38 | const query = odataQuery() 39 | const actual = query 40 | .expand('posts', e => e.filter(q => q.content.startsWith('test'))) 41 | .toString() 42 | const expected = "$expand=posts($filter=startswith(content, 'test'))" 43 | expect(actual).toBe(expected) 44 | }) 45 | 46 | it('expand and filter composed', () => { 47 | const query = odataQuery() 48 | const actual = query 49 | .expand('posts', e => 50 | e.filter(q => q.content.startsWith('test').or(q.id.biggerThan(5))) 51 | ) 52 | .toString() 53 | const expected = 54 | "$expand=posts($filter=startswith(content, 'test') or id gt 5)" 55 | expect(actual).toBe(expected) 56 | }) 57 | 58 | it('expand and filter composed multiline', () => { 59 | const query = odataQuery() 60 | const actual = query 61 | .expand('posts', e => 62 | e 63 | .filter(q => q.content.startsWith('test').or(q.id.biggerThan(5))) 64 | .filter(q => q.id.lessThan(10)) 65 | ) 66 | .toString() 67 | const expected = 68 | "$expand=posts($filter=(startswith(content, 'test') or id gt 5) and id lt 10)" 69 | expect(actual).toBe(expected) 70 | }) 71 | 72 | it('expand and orderby', () => { 73 | const query = odataQuery() 74 | const actual = query 75 | .expand('posts', e => e.orderBy(q => q.id.desc())) 76 | .toString() 77 | const expected = '$expand=posts($orderby=id desc)' 78 | expect(actual).toBe(expected) 79 | }) 80 | 81 | it('expand and paginate', () => { 82 | const query = odataQuery() 83 | const actual = query.expand('posts', e => e.paginate(0)).toString() 84 | const expected = '$expand=posts($top=0;$count=true)' 85 | expect(actual).toBe(expected) 86 | }) 87 | 88 | it('expand and paginate object', () => { 89 | const query = odataQuery() 90 | const actual = query 91 | .expand('posts', e => 92 | e.paginate({ 93 | page: 5, 94 | pagesize: 10, 95 | count: false, 96 | }) 97 | ) 98 | .toString() 99 | const expected = '$expand=posts($skip=50;$top=10)' 100 | expect(actual).toBe(expected) 101 | }) 102 | }) 103 | 104 | describe('testing ODataQuery expand with key query', () => { 105 | // one2one relation 106 | it('expand', () => { 107 | const query = odataQuery() 108 | const actual = query.expand(u => u.address).toString() 109 | const expected = '$expand=address' 110 | expect(actual).toBe(expected) 111 | }) 112 | 113 | it('expand and select', () => { 114 | const query = odataQuery() 115 | const actual = query 116 | .expand( 117 | u => u.address, 118 | q => q.select('code') 119 | ) 120 | .toString() 121 | const expected = '$expand=address($select=code)' 122 | expect(actual).toBe(expected) 123 | }) 124 | 125 | it('expand and select optional', () => { 126 | const query = odataQuery() 127 | const actual = query 128 | .expand( 129 | u => u.address2, 130 | q => q.select('code') 131 | ) 132 | .toString() 133 | const expected = '$expand=address2($select=code)' 134 | expect(actual).toBe(expected) 135 | }) 136 | 137 | it('expand twice', () => { 138 | const query = odataQuery() 139 | const actual = query 140 | .expand( 141 | u => u.address.user, 142 | q => q.select('id') 143 | ) 144 | .toString() 145 | const expected = '$expand=address/user($select=id)' 146 | expect(actual).toBe(expected) 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /tests/filter-functions.spec.ts: -------------------------------------------------------------------------------- 1 | import { getFuncArgs, makeExp } from '../src/builders' 2 | 3 | describe('test filter expressions', () => { 4 | it('parentheses', () => { 5 | return expect(makeExp('exp')._get()).toEqual('exp') 6 | }) 7 | 8 | it('not expression', () => { 9 | return expect(makeExp('exp').not()._get()).toEqual('not (exp)') 10 | }) 11 | 12 | it('and expression', () => { 13 | return expect(makeExp('exp').and(makeExp('exp'))._get()).toEqual( 14 | 'exp and exp' 15 | ) 16 | }) 17 | 18 | it('or expression', () => { 19 | return expect(makeExp('exp').or(makeExp('exp'))._get()).toEqual( 20 | 'exp or exp' 21 | ) 22 | }) 23 | 24 | it('and expression inception', () => { 25 | return expect(makeExp('exp').and(makeExp('exp or exp'))._get()).toEqual( 26 | 'exp and (exp or exp)' 27 | ) 28 | }) 29 | 30 | it('or expression inception', () => { 31 | return expect(makeExp('exp').or(makeExp('exp or exp'))._get()).toEqual( 32 | 'exp or (exp or exp)' 33 | ) 34 | }) 35 | }) 36 | 37 | describe('getFuncArgs', () => { 38 | it('should return the arguments of a function', () => { 39 | const fn = function (a: number, b: number) { 40 | return a + b 41 | } 42 | return expect(getFuncArgs(fn)).toEqual(['a', 'b']) 43 | }) 44 | 45 | it('should return the arguments of an arrow function', () => { 46 | const fn: (a: any) => any = a => a 47 | return expect(getFuncArgs(fn)).toEqual(['a']) 48 | }) 49 | 50 | it('should return the arguments of an arrow function 2', () => { 51 | const fn: (p: any) => any = p => p.comments.any((c: any) => c.equals(null)) 52 | return expect(getFuncArgs(fn)).toEqual(['p']) 53 | }) 54 | 55 | it('should return the arguments of an arrow function 3', () => { 56 | const fn = (a: number, b: number) => a + b 57 | return expect(getFuncArgs(fn)).toEqual(['a', 'b']) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /tests/filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from './data/models' 2 | import { odataQuery } from '../src' 3 | 4 | // string 5 | describe('testodataQuery filter by string', () => { 6 | it('contains', () => { 7 | const query = odataQuery() 8 | const actual = query.filter(q => q.email.contains('test')).toString() 9 | const expected = "$filter=contains(email, 'test')" 10 | expect(actual).toBe(expected) 11 | }) 12 | 13 | it('contains caseInsensitive', () => { 14 | const query = odataQuery() 15 | const actual = query 16 | .filter(q => q.email.contains('test', { caseInsensitive: true })) 17 | .toString() 18 | const expected = "$filter=contains(tolower(email), 'test')" 19 | expect(actual).toBe(expected) 20 | }) 21 | 22 | it('endsWith', () => { 23 | const query = odataQuery() 24 | const actual = query.filter(q => q.email.endsWith('test')).toString() 25 | const expected = "$filter=endswith(email, 'test')" 26 | expect(actual).toBe(expected) 27 | }) 28 | 29 | it('endsWith caseInsensitive', () => { 30 | const query = odataQuery() 31 | const actual = query 32 | .filter(q => q.email.endsWith('test', { caseInsensitive: true })) 33 | .toString() 34 | const expected = "$filter=endswith(tolower(email), 'test')" 35 | expect(actual).toBe(expected) 36 | }) 37 | 38 | it('equals', () => { 39 | const query = odataQuery() 40 | const actual = query.filter(q => q.email.equals('test')).toString() 41 | const expected = "$filter=email eq 'test'" 42 | expect(actual).toBe(expected) 43 | }) 44 | 45 | it('equals caseInsensitive', () => { 46 | const query = odataQuery() 47 | const actual = query 48 | .filter(q => q.email.equals('test', { caseInsensitive: true })) 49 | .toString() 50 | const expected = "$filter=tolower(email) eq 'test'" 51 | expect(actual).toBe(expected) 52 | }) 53 | 54 | it('notEquals', () => { 55 | const query = odataQuery() 56 | const actual = query.filter(q => q.email.notEquals('test')).toString() 57 | const expected = "$filter=email ne 'test'" 58 | expect(actual).toBe(expected) 59 | }) 60 | 61 | it('not equals caseInsensitive', () => { 62 | const query = odataQuery() 63 | const actual = query 64 | .filter(q => q.email.notEquals('test', { caseInsensitive: true })) 65 | .toString() 66 | const expected = "$filter=tolower(email) ne 'test'" 67 | expect(actual).toBe(expected) 68 | }) 69 | 70 | it('isNull', () => { 71 | const query = odataQuery() 72 | const actual = query.filter(q => q.email.isNull()).toString() 73 | const expected = '$filter=email eq null' 74 | expect(actual).toBe(expected) 75 | }) 76 | 77 | it('notNull', () => { 78 | const query = odataQuery() 79 | const actual = query.filter(q => q.email.notNull()).toString() 80 | const expected = '$filter=email ne null' 81 | expect(actual).toBe(expected) 82 | }) 83 | 84 | it('startsWith', () => { 85 | const query = odataQuery() 86 | const actual = query.filter(q => q.email.startsWith('test')).toString() 87 | const expected = "$filter=startswith(email, 'test')" 88 | expect(actual).toBe(expected) 89 | }) 90 | 91 | it('startsWith caseInsensitive', () => { 92 | const query = odataQuery() 93 | const actual = query 94 | .filter(q => q.email.startsWith('test', { caseInsensitive: true })) 95 | .toString() 96 | const expected = "$filter=startswith(tolower(email), 'test')" 97 | expect(actual).toBe(expected) 98 | }) 99 | 100 | it('string in array', () => { 101 | const query = odataQuery() 102 | const actual = query.filter(q => q.givenName.in(['foo', 'bar'])).toString() 103 | const expected = "$filter=givenName in ('foo','bar')" 104 | expect(actual).toBe(expected) 105 | }) 106 | 107 | it('length', () => { 108 | const query = odataQuery() 109 | const actual = query.filter(q => q.givenName.length().equals(1)).toString() 110 | const expected = "$filter=length(givenName) eq 1" 111 | expect(actual).toBe(expected) 112 | }) 113 | 114 | it('tolower toupper', () => { 115 | const query = odataQuery() 116 | const actual = query.filter(q => q.givenName.tolower().notEquals(q.givenName.toupper())).toString() 117 | const expected = "$filter=tolower(givenName) ne toupper(givenName)" 118 | expect(actual).toBe(expected) 119 | }) 120 | 121 | it('trim', () => { 122 | const query = odataQuery() 123 | const actual = query.filter(q => q.givenName.trim().equals('bar')).toString() 124 | const expected = "$filter=trim(givenName) eq 'bar'" 125 | expect(actual).toBe(expected) 126 | }) 127 | 128 | it('indexof', () => { 129 | const query = odataQuery() 130 | const actual = query.filter(q => q.givenName.indexof('bar').equals(-1)).toString() 131 | const expected = "$filter=indexof(givenName, 'bar') eq -1" 132 | expect(actual).toBe(expected) 133 | }) 134 | 135 | it('substring', () => { 136 | const query = odataQuery() 137 | const actual = query.filter(q => q.givenName.substring(0).equals('bar')).toString() 138 | const expected = "$filter=substring(givenName, 0) eq 'bar'" 139 | expect(actual).toBe(expected) 140 | }) 141 | 142 | it('concat', () => { 143 | const query = odataQuery() 144 | const actual = query.filter(q => q.givenName.append('foo').prepend('bar').notEquals("bar")).toString() 145 | const expected = "$filter=concat('bar', concat(givenName, 'foo')) ne 'bar'" 146 | expect(actual).toBe(expected) 147 | }) 148 | 149 | it('combined functions', () => { 150 | const query = odataQuery() 151 | const actual = query.filter(q => q.address.street.tolower().append('foo').prepend('bar').contains(q.address.street.tolower())).toString() 152 | const expected = "$filter=contains(concat('bar', concat(tolower(address/street), 'foo')), tolower(address/street))" 153 | expect(actual).toBe(expected) 154 | }) 155 | }) 156 | 157 | // guid 158 | describe('testodataQuery filter by guid', () => { 159 | it('equals', () => { 160 | const query = odataQuery() 161 | const guid = '003b63b4-e0b0-40db-8d5f-fb388bf0eabc' 162 | const actual = query.filter(q => q.email.equals(guid)).toString() 163 | const expected = `$filter=email eq ${guid}` 164 | expect(actual).toBe(expected) 165 | }) 166 | 167 | it('notEquals', () => { 168 | const query = odataQuery() 169 | const guid = '003b63b4-e0b0-40db-8d5f-fb388bf0eabc' 170 | const actual = query.filter(q => q.email.notEquals(guid)).toString() 171 | const expected = `$filter=email ne ${guid}` 172 | expect(actual).toBe(expected) 173 | }) 174 | 175 | it('guid as string', () => { 176 | const query = odataQuery() 177 | const guid = '003b63b4-e0b0-40db-8d5f-fb388bf0eabc' 178 | const actual = query 179 | .filter(q => q.email.equals(guid, { ignoreGuid: true })) 180 | .toString() 181 | const expected = `$filter=email eq '${guid}'` 182 | expect(actual).toBe(expected) 183 | }) 184 | }) 185 | 186 | // number 187 | describe('testodataQuery filter by number', () => { 188 | it('biggerThan', () => { 189 | const query = odataQuery() 190 | const actual = query.filter(q => q.id.biggerThan(5)).toString() 191 | const expected = '$filter=id gt 5' 192 | expect(actual).toBe(expected) 193 | }) 194 | 195 | it('lessThan', () => { 196 | const query = odataQuery() 197 | const actual = query.filter(q => q.id.lessThan(5)).toString() 198 | const expected = '$filter=id lt 5' 199 | expect(actual).toBe(expected) 200 | }) 201 | 202 | it('biggerThanOrEqual', () => { 203 | const query = odataQuery() 204 | const actual = query.filter(q => q.id.biggerThanOrEqual(5)).toString() 205 | const expected = '$filter=id ge 5' 206 | expect(actual).toBe(expected) 207 | }) 208 | 209 | it('lessThanOrEqual', () => { 210 | const query = odataQuery() 211 | const actual = query.filter(q => q.id.lessThanOrEqual(5)).toString() 212 | const expected = '$filter=id le 5' 213 | expect(actual).toBe(expected) 214 | }) 215 | 216 | it('equals', () => { 217 | const query = odataQuery() 218 | const actual = query.filter(q => q.id.equals(5)).toString() 219 | const expected = '$filter=id eq 5' 220 | expect(actual).toBe(expected) 221 | }) 222 | 223 | it('notEquals', () => { 224 | const query = odataQuery() 225 | const actual = query.filter(q => q.id.notEquals(5)).toString() 226 | const expected = '$filter=id ne 5' 227 | expect(actual).toBe(expected) 228 | }) 229 | 230 | it('isNull', () => { 231 | const query = odataQuery() 232 | const actual = query.filter(q => q.id.isNull()).toString() 233 | const expected = '$filter=id eq null' 234 | expect(actual).toBe(expected) 235 | }) 236 | 237 | it('notNull', () => { 238 | const query = odataQuery() 239 | const actual = query.filter(q => q.id.notNull()).toString() 240 | const expected = '$filter=id ne null' 241 | expect(actual).toBe(expected) 242 | }) 243 | 244 | it('number in array', () => { 245 | const query = odataQuery() 246 | const actual = query.filter(q => q.id.in([5, 10])).toString() 247 | const expected = '$filter=id in (5,10)' 248 | expect(actual).toBe(expected) 249 | }) 250 | }) 251 | 252 | // boolean 253 | describe('testodataQuery filter by boolean', () => { 254 | it('equals', () => { 255 | const query = odataQuery() 256 | const actual = query.filter(q => q.accountEnabled.equals(true)).toString() 257 | const expected = '$filter=accountEnabled eq true' 258 | expect(actual).toBe(expected) 259 | }) 260 | 261 | it('notEquals', () => { 262 | const query = odataQuery() 263 | const actual = query 264 | .filter(q => q.accountEnabled.notEquals(true)) 265 | .toString() 266 | const expected = '$filter=accountEnabled ne true' 267 | expect(actual).toBe(expected) 268 | }) 269 | }) 270 | 271 | // Date 272 | describe('testodataQuery filter by Date', () => { 273 | it('inTimeSpan', () => { 274 | const query = odataQuery() 275 | const actual = query.filter(q => q.createDate.inTimeSpan(2020)).toString() 276 | const expected = '$filter=(year(createDate) eq 2020)' 277 | expect(actual).toBe(expected) 278 | }) 279 | 280 | it('inTimeSpan full date', () => { 281 | const query = odataQuery() 282 | const actual = query 283 | .filter(q => q.createDate.inTimeSpan(2020, 10, 14, 6, 30)) 284 | .toString() 285 | const expected = 286 | '$filter=(year(createDate) eq 2020) and (month(createDate) eq 10) and (day(createDate) eq 14) and (hour(createDate) eq 6) and (minute(createDate) eq 30)' 287 | expect(actual).toBe(expected) 288 | }) 289 | 290 | it('isAfter', () => { 291 | const query = odataQuery() 292 | const actual = query 293 | .filter(q => q.createDate.isAfter(new Date(2020, 0))) 294 | .toString() 295 | const expected = '$filter=createDate gt 2020-01-01T' 296 | expect(actual.indexOf(expected)).toBeGreaterThan(-1) 297 | }) 298 | 299 | it('isBefore', () => { 300 | const query = odataQuery() 301 | const actual = query 302 | .filter(q => q.createDate.isBefore(new Date(2020, 0))) 303 | .toString() 304 | const expected = '$filter=createDate lt 2020-01-01T' 305 | expect(actual.indexOf(expected)).toBeGreaterThan(-1) 306 | }) 307 | 308 | it('isAfterOrEqual', () => { 309 | const query = odataQuery() 310 | const actual = query 311 | .filter(q => q.createDate.isAfterOrEqual(new Date(2020, 0))) 312 | .toString() 313 | const expected = '$filter=createDate ge 2020-01-01T' 314 | expect(actual.indexOf(expected)).toBeGreaterThan(-1) 315 | }) 316 | 317 | it('isBeforeOrEqual', () => { 318 | const query = odataQuery() 319 | const actual = query 320 | .filter(q => q.createDate.isBeforeOrEqual(new Date(2020, 0))) 321 | .toString() 322 | const expected = '$filter=createDate le 2020-01-01T' 323 | expect(actual.indexOf(expected)).toBeGreaterThan(-1) 324 | }) 325 | 326 | it('isSame', () => { 327 | const query = odataQuery() 328 | const actual = query 329 | .filter(q => q.createDate.isSame(new Date(2020, 0))) 330 | .toString() 331 | const expected = '$filter=createDate eq 2020-01-01T' 332 | expect(actual.indexOf(expected)).toBeGreaterThan(-1) 333 | }) 334 | 335 | it('isNull', () => { 336 | const query = odataQuery() 337 | const actual = query.filter(q => q.createDate.isNull()).toString() 338 | const expected = '$filter=createDate eq null' 339 | expect(actual).toBe(expected) 340 | }) 341 | 342 | it('notNull', () => { 343 | const query = odataQuery() 344 | const actual = query.filter(q => q.createDate.notNull()).toString() 345 | const expected = '$filter=createDate ne null' 346 | expect(actual).toBe(expected) 347 | }) 348 | }) 349 | 350 | // object 351 | describe('testodataQuery filter by object', () => { 352 | it('filter by nested property', () => { 353 | const query = odataQuery() 354 | const actual = query.filter(q => q.address.code.biggerThan(5)).toString() 355 | const expected = '$filter=address/code gt 5' 356 | expect(actual).toBe(expected) 357 | }) 358 | 359 | it('filter by nested property deep', () => { 360 | const query = odataQuery() 361 | const actual = query.filter(q => q.address.user.id.biggerThan(5)).toString() 362 | const expected = '$filter=address/user/id gt 5' 363 | expect(actual).toBe(expected) 364 | }) 365 | 366 | it('filter by null object property', () => { 367 | const query = odataQuery() 368 | const actual = query.filter(q => q.address2.isNull()).toString() 369 | const expected = '$filter=address2 eq null' 370 | expect(actual).toBe(expected) 371 | }) 372 | 373 | it('filter by not null object property', () => { 374 | const query = odataQuery() 375 | const actual = query.filter(q => q.address2.notNull()).toString() 376 | const expected = '$filter=address2 ne null' 377 | expect(actual).toBe(expected) 378 | }) 379 | }) 380 | 381 | // array 382 | describe('testodataQuery filter by array', () => { 383 | it('filter by empty array', () => { 384 | const query = odataQuery() 385 | const actual = query.filter(q => q.phoneNumbers.empty()).toString() 386 | const expected = '$filter=not phoneNumbers/any()' 387 | expect(actual).toBe(expected) 388 | }) 389 | 390 | it('filter by not empty array', () => { 391 | const query = odataQuery() 392 | const actual = query.filter(q => q.phoneNumbers.notEmpty()).toString() 393 | const expected = '$filter=phoneNumbers/any()' 394 | expect(actual).toBe(expected) 395 | }) 396 | 397 | it('filter by not empty related array', () => { 398 | const query = odataQuery() 399 | const actual = query.filter(q => q.posts.notEmpty()).toString() 400 | const expected = '$filter=posts/any()' 401 | expect(actual).toBe(expected) 402 | }) 403 | 404 | it('filter by any', () => { 405 | const query = odataQuery() 406 | const actual = query 407 | .filter(q => q.phoneNumbers.any(x => x.equals('test'))) 408 | .toString() 409 | const expected = "$filter=phoneNumbers/any(x: x eq 'test')" 410 | expect(actual).toBe(expected) 411 | }) 412 | 413 | it('filter by any nested', () => { 414 | const query = odataQuery() 415 | const actual = query 416 | .filter(q => q.posts.any(p => p.comments.any(c => c.isNull()))) 417 | .toString() 418 | const expected = '$filter=posts/any(p: p/comments/any(c: c eq null))' 419 | expect(actual).toBe(expected) 420 | }) 421 | 422 | it('filter by any (specific case)', () => { 423 | interface Product { 424 | productType: { 425 | category: { 426 | segmentCategories: { segmentId: number }[] 427 | } 428 | } 429 | } 430 | const selectedSegmentId = 1 431 | const query = odataQuery().filter(q => 432 | q.productType.category.segmentCategories.any((p: any) => 433 | p.segmentId.equals(selectedSegmentId) 434 | ) 435 | ) 436 | const actual = query.toString() 437 | const expected = 438 | '$filter=productType/category/segmentCategories/any(p: p/segmentId eq 1)' 439 | expect(actual).toBe(expected) 440 | }) 441 | 442 | it('filter by all', () => { 443 | const query = odataQuery() 444 | const actual = query 445 | .filter(q => q.phoneNumbers.all(x => x.equals('test'))) 446 | .toString() 447 | const expected = "$filter=phoneNumbers/all(x: x eq 'test')" 448 | expect(actual).toBe(expected) 449 | }) 450 | 451 | it('filter by all nested', () => { 452 | const query = odataQuery() 453 | const actual = query 454 | .filter(q => q.posts.all(p => p.comments.all(c => c.notNull()))) 455 | .toString() 456 | const expected = '$filter=posts/all(p: p/comments/all(c: c ne null))' 457 | expect(actual).toBe(expected) 458 | }) 459 | 460 | it('filter by null', () => { 461 | const query = odataQuery() 462 | const actual = query.filter(q => q.posts.isNull()).toString() 463 | const expected = '$filter=posts eq null' 464 | expect(actual).toBe(expected) 465 | }) 466 | 467 | it('filter by not null', () => { 468 | const query = odataQuery() 469 | const actual = query.filter(q => q.posts.notNull()).toString() 470 | const expected = '$filter=posts ne null' 471 | expect(actual).toBe(expected) 472 | }) 473 | }) 474 | 475 | // by another key 476 | describe('testodataQuery filter by another key', () => { 477 | it('string', () => { 478 | const query = odataQuery() 479 | const actual = query.filter(q => q.givenName.contains(q.surname)).toString() 480 | const expected = '$filter=contains(givenName, surname)' 481 | expect(actual).toBe(expected) 482 | }) 483 | 484 | it('number', () => { 485 | const query = odataQuery() 486 | const actual = query.filter(q => q.id.equals(q.id)).toString() 487 | const expected = '$filter=id eq id' 488 | expect(actual).toBe(expected) 489 | }) 490 | 491 | it('boolean', () => { 492 | const query = odataQuery() 493 | const actual = query 494 | .filter(q => q.accountEnabled.notEquals(q.accountEnabled)) 495 | .toString() 496 | const expected = '$filter=accountEnabled ne accountEnabled' 497 | expect(actual).toBe(expected) 498 | }) 499 | 500 | it('Date', () => { 501 | const query = odataQuery() 502 | const actual = query 503 | .filter(q => q.createDate.isSame(q.createDate, 'day')) 504 | .toString() 505 | const expected = '$filter=day(createDate) eq day(createDate)' 506 | expect(actual).toBe(expected) 507 | }) 508 | }) 509 | 510 | // composed 511 | describe('testodataQuery filter composed', () => { 512 | it('and [inline]', () => { 513 | const query = odataQuery() 514 | const actual = query 515 | .filter(q => 516 | q.email 517 | .startsWith('a') 518 | .and(q.email.contains('o')) 519 | .and(q.email.endsWith('z')) 520 | ) 521 | .toString() 522 | 523 | const expected = 524 | "$filter=startswith(email, 'a') and contains(email, 'o') and endswith(email, 'z')" 525 | expect(actual).toBe(expected) 526 | }) 527 | 528 | it('and [multilines]', () => { 529 | const query = odataQuery() 530 | const actual = query 531 | .filter(q => q.email.startsWith('a')) 532 | .filter(q => q.email.contains('o')) 533 | .filter(q => q.email.endsWith('z')) 534 | .toString() 535 | 536 | const expected = 537 | "$filter=startswith(email, 'a') and contains(email, 'o') and endswith(email, 'z')" 538 | expect(actual).toBe(expected) 539 | }) 540 | 541 | it('or', () => { 542 | const query = odataQuery() 543 | const actual = query 544 | .filter(q => 545 | q.email 546 | .startsWith('a') 547 | .or(q.email.contains('o')) 548 | .or(q.email.endsWith('z')) 549 | ) 550 | .toString() 551 | 552 | const expected = 553 | "$filter=startswith(email, 'a') or contains(email, 'o') or endswith(email, 'z')" 554 | expect(actual).toBe(expected) 555 | }) 556 | 557 | it('or with and [inline]', () => { 558 | const query = odataQuery() 559 | const actual = query 560 | .filter(q => 561 | q.givenName 562 | .startsWith('search') 563 | .and(q.surname.startsWith('search').or(q.email.startsWith('search'))) 564 | ) 565 | .toString() 566 | 567 | const expected = 568 | "$filter=startswith(givenName, 'search') and (startswith(surname, 'search') or startswith(email, 'search'))" 569 | expect(actual).toBe(expected) 570 | }) 571 | 572 | it('or with and [multilines]', () => { 573 | const query = odataQuery() 574 | const actual = query 575 | .filter(q => 576 | q.givenName 577 | .startsWith('search') 578 | .or(q.surname.startsWith('search')) 579 | .or(q.email.startsWith('search')) 580 | ) 581 | .filter(q => q.accountEnabled.equals(true)) 582 | .toString() 583 | 584 | const expected = 585 | "$filter=(startswith(givenName, 'search') or startswith(surname, 'search') or startswith(email, 'search')) and accountEnabled eq true" 586 | expect(actual).toBe(expected) 587 | }) 588 | 589 | it('not', () => { 590 | const query = odataQuery() 591 | const actual = query 592 | .filter(q => q.email.startsWith('a').or(q.email.startsWith('b')).not()) 593 | .toString() 594 | 595 | const expected = 596 | "$filter=not (startswith(email, 'a') or startswith(email, 'b'))" 597 | expect(actual).toBe(expected) 598 | }) 599 | }) 600 | 601 | // alt 602 | describe('testodataQuery filter alt', () => { 603 | it('alt', () => { 604 | const query = odataQuery() 605 | const actual = query.filter('email', q => q.startsWith('test')).toString() 606 | const expected = "$filter=startswith(email, 'test')" 607 | expect(actual).toBe(expected) 608 | }) 609 | 610 | it('alt or', () => { 611 | const query = odataQuery() 612 | const actual = query 613 | .filter('email', q => q.startsWith('test').or(q.startsWith('ok'))) 614 | .toString() 615 | const expected = 616 | "$filter=startswith(email, 'test') or startswith(email, 'ok')" 617 | expect(actual).toBe(expected) 618 | }) 619 | 620 | it('alt and', () => { 621 | const query = odataQuery() 622 | const actual = query 623 | .filter('email', q => q.startsWith('test').and(q.endsWith('.com'))) 624 | .toString() 625 | const expected = 626 | "$filter=startswith(email, 'test') and endswith(email, '.com')" 627 | expect(actual).toBe(expected) 628 | }) 629 | 630 | it('alt and [multilines]', () => { 631 | const query = odataQuery() 632 | const actual = query 633 | .filter('email', q => q.startsWith('test')) 634 | .filter('givenName', q => q.startsWith('test')) 635 | .toString() 636 | 637 | const expected = 638 | "$filter=startswith(email, 'test') and startswith(givenName, 'test')" 639 | expect(actual).toBe(expected) 640 | }) 641 | }) 642 | -------------------------------------------------------------------------------- /tests/groupby.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from './data/models' 2 | import { odataQuery } from '../src' 3 | 4 | describe('testing ODataQuery groupBy', () => { 5 | it('groupBy', () => { 6 | const query = odataQuery() 7 | const actual = query.groupBy(['email']).toString() 8 | const expected = '$apply=groupby((email))' 9 | expect(actual).toBe(expected) 10 | }) 11 | 12 | it('groupBy multiple', () => { 13 | const query = odataQuery() 14 | const actual = query.groupBy(['email', 'surname']).toString() 15 | const expected = '$apply=groupby((email, surname))' 16 | expect(actual).toBe(expected) 17 | }) 18 | 19 | it('groupBy aggregate', () => { 20 | const query = odataQuery() 21 | const actual = query 22 | .groupBy(['email', 'surname'], a => a.countdistinct('id', 'all')) 23 | .toString() 24 | const expected = 25 | '$apply=groupby((email, surname), aggregate(id with countdistinct as all))' 26 | expect(actual).toBe(expected) 27 | }) 28 | 29 | it('groupBy aggregate multiple', () => { 30 | const query = odataQuery() 31 | 32 | const actual = query 33 | .groupBy(['email', 'surname'], a => 34 | a.countdistinct('id', 'all').max('phoneNumbers', 'test') 35 | ) 36 | .toString() 37 | 38 | const expected = 39 | '$apply=groupby((email, surname), aggregate(id with countdistinct as all, phoneNumbers with max as test))' 40 | expect(actual).toBe(expected) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/orderby.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from './data/models' 2 | import { odataQuery } from '../src' 3 | 4 | describe('testing ODataQuery orderby', () => { 5 | it('orderby', () => { 6 | const query = odataQuery() 7 | const actual = query.orderBy(q => q.email).toString() 8 | const expected = '$orderby=email' 9 | expect(actual).toBe(expected) 10 | }) 11 | 12 | it('orderby alt', () => { 13 | const query = odataQuery() 14 | const actual = query.orderBy('email').toString() 15 | const expected = '$orderby=email' 16 | expect(actual).toBe(expected) 17 | }) 18 | 19 | it('orderby asc', () => { 20 | const query = odataQuery() 21 | const actual = query.orderBy(q => q.email.asc()).toString() 22 | const expected = '$orderby=email asc' 23 | expect(actual).toBe(expected) 24 | }) 25 | 26 | it('orderby asc alt', () => { 27 | const query = odataQuery() 28 | const actual = query.orderBy('email', 'asc').toString() 29 | const expected = '$orderby=email asc' 30 | expect(actual).toBe(expected) 31 | }) 32 | 33 | it('orderby desc', () => { 34 | const query = odataQuery() 35 | const actual = query.orderBy(q => q.email.desc()).toString() 36 | const expected = '$orderby=email desc' 37 | expect(actual).toBe(expected) 38 | }) 39 | 40 | it('orderby desc alt', () => { 41 | const query = odataQuery() 42 | const actual = query.orderBy('email', 'desc').toString() 43 | const expected = '$orderby=email desc' 44 | expect(actual).toBe(expected) 45 | }) 46 | 47 | it('orderby nested', () => { 48 | const query = odataQuery() 49 | const actual = query.orderBy(q => q.address.street).toString() 50 | const expected = '$orderby=address/street' 51 | expect(actual).toBe(expected) 52 | }) 53 | 54 | it('orderby nested array', () => { 55 | const query = odataQuery() 56 | const actual = query.orderBy(q => q.posts.id).toString() 57 | const expected = '$orderby=posts/id' 58 | expect(actual).toBe(expected) 59 | }) 60 | 61 | it('orderby multiple', () => { 62 | const query = odataQuery() 63 | const actual = query 64 | .orderBy(q => q.address.street.asc()) 65 | .orderBy(q => q.address2.street.desc()) 66 | .toString() 67 | const expected = '$orderby=address/street asc, address2/street desc' 68 | expect(actual).toBe(expected) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /tests/paginate.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from './data/models' 2 | import { odataQuery } from '../src' 3 | 4 | describe('testing ODataQuery paginate', () => { 5 | it('paginate', () => { 6 | const query = odataQuery() 7 | const actual = query.paginate(10).toString() 8 | const expected = '$top=10&$count=true' 9 | expect(actual).toBe(expected) 10 | }) 11 | 12 | it('paginate with skip', () => { 13 | const query = odataQuery() 14 | const actual = query.paginate(25, 5).toString() 15 | const expected = '$skip=125&$top=25&$count=true' 16 | expect(actual).toBe(expected) 17 | }) 18 | 19 | it('paginate object', () => { 20 | const query = odataQuery() 21 | const actual = query.paginate({ pagesize: 10 }).toString() 22 | const expected = '$top=10&$count=true' 23 | expect(actual).toBe(expected) 24 | }) 25 | 26 | it('paginate object with skip', () => { 27 | const query = odataQuery() 28 | const actual = query.paginate({ page: 5, pagesize: 25 }).toString() 29 | const expected = '$skip=125&$top=25&$count=true' 30 | expect(actual).toBe(expected) 31 | }) 32 | 33 | it('paginate disable count', () => { 34 | const query = odataQuery() 35 | const actual = query 36 | .paginate({ page: 5, pagesize: 25, count: false }) 37 | .toString() 38 | const expected = '$skip=125&$top=25' 39 | expect(actual).toBe(expected) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/query.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from './data/models' 2 | import { odataQuery } from '../src' 3 | 4 | describe('testing ODataQuery', () => { 5 | it('count', () => { 6 | const query = odataQuery() 7 | const actual = query.count().toString() 8 | const expected = '$count=true' 9 | expect(actual).toBe(expected) 10 | }) 11 | 12 | it('toString', () => { 13 | const query = odataQuery() 14 | const actual = query.toString() 15 | const expected = '' 16 | expect(actual).toBe(expected) 17 | }) 18 | 19 | it('toObject', () => { 20 | const query = odataQuery() 21 | const actual = query.toObject() 22 | const expected = {} 23 | expect(actual).toStrictEqual(expected) 24 | }) 25 | 26 | it('complete toObject', () => { 27 | const query = odataQuery() 28 | const actual = query 29 | .filter(q => q.email.contains('test')) 30 | .expand('address') 31 | .orderBy('email') 32 | .count() 33 | .toObject() 34 | const expected = { 35 | $count: 'true', 36 | $expand: 'address', 37 | $filter: "contains(email, 'test')", 38 | $orderby: 'email', 39 | } 40 | expect(actual).toStrictEqual(expected) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/select.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from './data/models' 2 | import { odataQuery } from '../src' 3 | 4 | describe('testing odataQuery select', () => { 5 | it('select one', () => { 6 | const query = odataQuery() 7 | const actual = query.select('id').toString() 8 | const expected = '$select=id' 9 | expect(actual).toBe(expected) 10 | }) 11 | 12 | it('select multiple', () => { 13 | const query = odataQuery() 14 | const actual = query.select('id', 'email', 'surname').toString() 15 | const expected = '$select=id,email,surname' 16 | expect(actual).toBe(expected) 17 | }) 18 | 19 | it('select with expression', () => { 20 | const query = odataQuery() 21 | const actual = query 22 | .select( 23 | x => x.id, 24 | x => x.address.street 25 | ) 26 | .toString() 27 | const expected = '$select=id,address/street' 28 | expect(actual).toBe(expected) 29 | }) 30 | 31 | it('select mixed', () => { 32 | const query = odataQuery() 33 | const actual = query 34 | .select('id', x => x.givenName, 'accountEnabled') 35 | .toString() 36 | const expected = '$select=id,givenName,accountEnabled' 37 | expect(actual).toBe(expected) 38 | }) 39 | 40 | it('select optional', () => { 41 | const query = odataQuery() 42 | const actual = query 43 | .select( 44 | x => x.givenName, 45 | x => x.surname 46 | ) 47 | .toString() 48 | const expected = '$select=givenName,surname' 49 | expect(actual).toBe(expected) 50 | }) 51 | 52 | it('remove expand after select', () => { 53 | const query = odataQuery() 54 | const expandedQuery = query.expand('address') 55 | let actual = expandedQuery.toString() 56 | let expected = '$expand=address' 57 | expect(actual).toBe(expected) 58 | actual = expandedQuery.select('email').toString() 59 | expected = '$select=email' 60 | expect(actual).toBe(expected) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "outDir": "./dist", 7 | "lib": [ 8 | "ESNext", 9 | "DOM" 10 | ], 11 | "moduleResolution": "Node", 12 | "strict": true, 13 | "sourceMap": true, 14 | "declaration": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "esModuleInterop": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "skipLibCheck": true 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } --------------------------------------------------------------------------------