{"id":16820427,"url":"https://github.com/techniq/odata-query","last_synced_at":"2025-05-14T19:09:46.017Z","repository":{"id":17277762,"uuid":"81605799","full_name":"techniq/odata-query","owner":"techniq","description":"OData v4 query builder","archived":false,"fork":false,"pushed_at":"2025-03-06T13:46:24.000Z","size":851,"stargazers_count":247,"open_issues_count":26,"forks_count":62,"subscribers_count":11,"default_branch":"main","last_synced_at":"2025-05-07T16:51:47.386Z","etag":null,"topics":["odata","query-builder","query-dsl"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/odata-query","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/techniq.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["techniq"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2017-02-10T20:52:46.000Z","updated_at":"2025-04-28T07:03:32.000Z","dependencies_parsed_at":"2024-06-18T13:34:50.286Z","dependency_job_id":"6c9ae067-c4c5-400a-8115-d26b3ec186e3","html_url":"https://github.com/techniq/odata-query","commit_stats":{"total_commits":221,"total_committers":23,"mean_commits":9.608695652173912,"dds":0.3755656108597285,"last_synced_commit":"ff4ae3b536084ffddaf53783d76d7a69c4f5d8d9"},"previous_names":[],"tags_count":54,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techniq%2Fodata-query","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techniq%2Fodata-query/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techniq%2Fodata-query/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/techniq%2Fodata-query/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/techniq","download_url":"https://codeload.github.com/techniq/odata-query/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253856617,"owners_count":21974576,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["odata","query-builder","query-dsl"],"created_at":"2024-10-13T10:56:45.046Z","updated_at":"2025-05-14T19:09:44.745Z","avatar_url":"https://github.com/techniq.png","language":"TypeScript","funding_links":["https://github.com/sponsors/techniq"],"categories":[],"sub_categories":[],"readme":"# odata-query\n\nOData v4 query builder that uses a simple object-based syntax similar to [MongoDB](https://docs.mongodb.com/manual/reference/operator/query/) and [js-data](http://www.js-data.io/v3.0/docs/query-syntax)\n\n## Install\n```\nnpm install odata-query\n```\n\nand then use the library\n```js\nimport buildQuery from 'odata-query'\n\nconst query = buildQuery({...})\nfetch(`http://localhost${query}`)\n```\nwhere the query object syntax for `{...}` is defined below.  There is also [react-odata](https://github.com/techniq/react-odata) which utilizies this library for a declarative React component.\n\n## Usage\nSee [tests](src/index.test.js) for examples as well\n\n- [Filtering](#filtering)\n  - [Simple equality filter](#simple-equality-filter)\n  - [Comparison operators](#comparison-operators)\n  - [Logical operators](#logical-operators)\n    - [Implied `and` with an array of objects](#implied-and-with-an-array-of-objects)\n    - [Implied `and` with multiple comparison operators for a single property](#implied-and-with-multiple-comparison-operators-for-a-single-property)\n    - [Explicit operator](#explicit-operator)\n  - [Collection operators](#collection-operators) - `any`, `all`\n    - [Implied and with an object or array of objects](#implied-and)\n    - [Explicit operator (`and`, `or`, and `not`)](#explicit-operator-and-or)\n    - [Implied all operators on collection item itself](#implied-all-operators-on-collection-item-itself)\n  - [Functions](#functions)\n    - [String functions returning boolean](#string-functions-returning-boolean)\n    - [Functions returning non-boolean values (string, int)](#functions-returning-non-boolean-values-string-int)\n    - [Functions returning non-boolean values (string, int) with parameters](#functions-returning-non-boolean-values-string-int-with-parameters)\n  - [Strings](#strings)\n  - [Data types](#data-types)\n  - [Search](#search)\n- [Selecting](#selecting)\n- [Ordering](#ordering)\n- [Expanding](#expanding)\n  - [Nested expand using slash seperator](#nested-expand-using-slash-seperator)\n  - [Nested expand with an object](#nested-expand-with-an-object)\n  - [Multiple expands as an array](#multiple-expands-as-an-array)\n  - [Filter expanded items](#filter-expanded-items)\n  - [Select only specific properties of expanded items](#select-only-specific-properties-of-expanded-items)\n  - [Return only a subset of expanded items](#return-only-a-subset-of-expanded-items)\n  - [Order expanded items](#order-expanded-items)\n  - [filter, select, top, and orderBy can be used together](#filter-select-top-and-orderby-can-be-used-together)\n- [Pagination (skip and top)](#pagination-skip-and-top)\n- [Single-item (key)](#single-item-key)\n- [Counting](#counting)\n- [Actions](#actions)\n- [Functions](#functions-1)\n- [Transforms](#transforms)\n\n### Filtering\n```js\nbuildQuery({ filter: {...} })\n=\u003e '?$filter=...'\n```\n\n#### Simple equality filter\n```js\nconst filter = { PropName: 1 };\nbuildQuery({ filter })\n=\u003e '?$filter=PropName eq 1'\n```\n\n#### Comparison operators\n```js\nconst filter = { PropName: { gt: 5 } };\nbuildQuery({ filter })\n=\u003e '?$filter=PropName gt 5'\n```\nSupported operators: `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `in`\n\nUsing the `in` operator is also similar to the previous example.\n\n```js\nconst filter = { PropName: { in: [1, 2, 3] } };\nbuildQuery({ filter })\n=\u003e '?$filter=PropName in (1,2,3)'\n```\n\n#### Logical operators\n##### Implied `and` with an array of objects\n```js\nconst filter = [{ SomeProp: 1 }, { AnotherProp: 2 }, 'startswith(Name, \"foo\")'];\nbuildQuery({ filter })\n=\u003e '?$filter=SomeProp eq 1 and AnotherProp eq 2 and startswith(Name, \"foo\")'\n```\n\n##### Implied `and` with multiple comparison operators for a single property\nUseful to perform a `between` query on a `Date` property\n```js\nconst startDate = new Date(Date.UTC(2017, 0, 1))\nconst endDate = new Date(Date.UTC(2017, 2, 1))\nconst filter = { DateProp: { ge: startDate, le: endDate } }\nbuildQuery({ filter })\n=\u003e \"?$filter=DateProp ge 2017-01-01T00:00:00Z and DateProp le 2017-03-01T00:00:00Z\"\n```\n\n##### Explicit operator\n```js\nconst filter = {\n  and: [\n    { SomeProp: 1 },\n    { AnotherProp: 2 },\n    'startswith(Name, \"foo\")'\n  ]\n};\n\nbuildQuery({ filter })\n=\u003e '?$filter=SomeProp eq 1 and AnotherProp eq 2 and startswith(Name, \"foo\")'\n```\n\n```js\nconst filter = {\n  not: {\n    and:[\n      {SomeProp: 1}, \n      {AnotherProp: 2}\n    ]\n  }\n};\n\nbuildQuery({ filter })\n=\u003e '?$filter=(not (SomeProp eq 1) and (AnotherProp eq 2))'\n```\n\nSupported operators: `and`, `or`, and `not`.\n\n#### Collection operators\n##### Empty `any`\nUsing an empty object\n```js\nconst filter = {\n  ItemsProp: {\n    any: {}\n  }\n};\n\nbuildQuery({ filter })\n=\u003e '?$filter=ItemsProp/any()'\n```\n\nor also as an empty array\n```js\nconst filter = {\n  ItemsProp: {\n    any: []\n  }\n};\n\nbuildQuery({ filter })\n=\u003e '?$filter=ItemsProp/any()'\n```\n\n##### Implied `and`\n\nUsing an object\n```js\nconst filter = {\n  ItemsProp: {\n    any: {\n      SomeProp: 1,\n      AnotherProp: 2\n    }\n  }\n};\n\nbuildQuery({ filter })\n=\u003e '?$filter=ItemsProp/any(i:i/SomeProp eq 1 and i/AnotherProp eq 2)'\n```\n\nor also as an array of object\n```js\nconst filter = {\n  ItemsProp: {\n    any: [\n      { SomeProp: 1 },\n      { AnotherProp: 2},\n    ]\n  }\n};\n\nbuildQuery({ filter })\n=\u003e '?$filter=ItemsProp/any(i:i/SomeProp eq 1 and i/AnotherProp eq 2)'\n```\n\n##### Explicit operator (`and`, `or`, and `not`)\n```js\nconst filter = {\n  ItemsProp: {\n    any: {\n      or: [\n        { SomeProp: 1 },\n        { AnotherProp: 2},\n      ]\n    }\n  }\n};\n\nbuildQuery({ filter })\n=\u003e '?$filter=ItemsProp/any(i:(i/SomeProp eq 1 or i/AnotherProp eq 2)'\n```\n```js\nconst filter = {\n  not: {\n    ItemsProp: {\n      any: {\n        or: [\n          { SomeProp: 1 },\n          { AnotherProp: 2},\n        ]\n      }\n    }\n  }\n};\n\nbuildQuery({ filter })\n=\u003e '?$filter=not ItemsProp/any(i:((i/SomeProp eq 1) or (i/AnotherProp eq 2)))'\n```\n\n##### Implied all operators on collection item itself \nITEM_ROOT is special constant to mark collection with primitive type\n\n'in' operator\n```js\nconst filter = {\n    tags: {\n      any: {\n        [ITEM_ROOT]: { in: ['tag1', 'tag2']},\n      },\n    },\n};\n\nbuildQuery({ filter })\n=\u003e \"?$filter=tags/any(tags:tags in ('tag1','tag2'))\"\n```\n'or' operator on collection item itself\n```js\nconst filter = {\n    tags: {\n      any: {\n        or: [\n          { [ITEM_ROOT]: 'tag1'},\n          { [ITEM_ROOT]: 'tag2'},\n        ]\n      }\n    }\n};\n\nbuildQuery({ filter })\n=\u003e \"?$filter=tags/any(tags:((tags eq 'tag1') or (tags eq 'tag2')))\";\n```\n\n'and' operator on collection item itself and nested item\n```js\n const filter = {\n    tags: {\n      any: [\n          { [ITEM_ROOT]: 'tag1'},\n          { [ITEM_ROOT]: 'tag2'},\n          { prop: 'tag3'},\n        ]\n    }\n};\n\nbuildQuery({ filter });\n=\u003e \"?$filter=tags/any(tags:tags eq 'tag1' and tags eq 'tag2' and tags/prop eq 'tag3')\";\n```\nFunction on collection item itself \n```js\nconst filter = {\n    tags: {\n      any: {\n        [`tolower(${ITEM_ROOT})`]: 'tag1'\n      }\n    }\n};\n\nbuildQuery({ filter });\n=\u003e \"?$filter=tags/any(tags:tolower(tags) eq 'tag1')\";\n```\n\nSupported operators: `any`, `all`\n\n#### Functions\n##### String functions returning boolean\n```js\nconst filter = { PropName: { contains: 'foo' } };\nbuildQuery({ filter })\n=\u003e \"$filter=contains(PropName,'foo')\"\n```\nSupported operators: `startswith`, `endswith`, `contains`, `matchespattern`\n\n##### Functions returning non-boolean values (string, int)\n```js\nconst filter = { 'length(PropName)': { gt: 10 } };\nbuildQuery({ filter })\n=\u003e \"$filter=length(PropName) gt 10\"\n```\nSupported operators: `length`, `tolower`, `toupper`, `trim`,\n`day`, `month`, `year`, `hour`, `minute`, `second`,\n`round`, `floor`, `ceiling`\n\n##### Functions returning non-boolean values (string, int) with parameters\n```js\nconst filter = { \"indexof(PropName, 'foo')\": { eq: 3 } };\nbuildQuery({ filter })\n=\u003e \"$filter=indexof(PropName, 'foo') eq 3\"\n```\nSupported operators: `indexof`, `substring`\n\n#### Strings\nA string can also be passed as the value of the filter and it will be taken as is.  This can be useful when using something like [odata-filter-builder](https://github.com/bodia-uz/odata-filter-builder) or if you want to just write the OData filter sytnax yourself but use the other benefits of the library, such as groupBy, expand, etc.\n```js\nimport f from 'odata-filter-builder';\n\nconst filter = f().eq('TypeId', '1')\n                  .contains(x =\u003e x.toLower('Name'), 'a')\n                  .toString();\nbuildQuery({ filter })\n```\n\n#### Data types\nGUID:\n```js\nconst filter = { \"someProp\": { eq: { type: 'guid', value: 'cd5977c2-4a64-42de-b2fc-7fe4707c65cd' } } };\nbuildQuery({ filter })\n=\u003e \"?$filter=someProp eq cd5977c2-4a64-42de-b2fc-7fe4707c65cd\"\n```\n\nDuration:\n```js\nconst filter = { \"someProp\": { eq: { type: 'duration', value: 'PT1H' } } };\nbuildQuery({ filter })\n=\u003e \"?$filter=someProp eq duration'PT1H'\"\n```\n\nBinary:\n```js\nconst filter = { \"someProp\": { eq: { type: 'binary', value: 'YmluYXJ5RGF0YQ==' } } };\nbuildQuery({ filter })\n=\u003e \"?$filter=someProp eq binary'YmluYXJ5RGF0YQ=='\"\n```\n\nDecimal: \n```js\nconst filter = { \"someProp\": { eq: { type: 'decimal', value: '12.3456789' } } };\nbuildQuery({ filter })\n=\u003e \"?$filter=someProp eq 12.3456789M\"\n```\n\n\nRaw:\n```js\nconst filter = { \"someProp\": { eq: { type: 'raw', value: `datetime'${date.toISOString()}'` } } };\nbuildQuery({ filter })\n=\u003e \"?$filter=someProp eq datetime'2021-07-08T12:27:08.122Z'\"\n```\n- Provides full control over the serialization of the value.  Useful to pass a data type.\n\nNote that as per OData specification, binary data is transmitted as a base64 encoded string. Refer to [Primitive Types in JSON Format](https://www.odata.org/documentation/odata-version-2-0/json-format/), and [binary representation](https://www.odata.org/documentation/odata-version-2-0/overview/).\n\n#### Search\n```js\nconst search = 'blue OR green';\nbuildQuery({ search });\n=\u003e '?$search=blue OR green';\n```\n\n### Selecting\n```js\nconst select = ['Foo', 'Bar'];\nbuildQuery({ select })\n=\u003e '?$select=Foo,Bar'\n```\n\n### Ordering\n```js\nconst orderBy = ['Foo desc', 'Bar'];\nbuildQuery({ orderBy })\n=\u003e '?$orderby=Foo desc,Bar'\n```\n\n### Expanding\n#### Nested expand using slash seperator\n```js\nconst expand = 'Friends/Photos'\nbuildQuery({ expand })\n=\u003e '?$expand=Friends($expand=Photos)';\n```\n\n#### Nested expand with an object\n```js\nconst expand = { Friends: { expand: 'Photos' } }\nbuildQuery({ expand })\n=\u003e '?$expand=Friends($expand=Photos)';\n```\n\n#### Multiple expands as an array\nSupports both string (with slash seperators) and objects\n```js\nconst expand = ['Foo', 'Baz'];\nbuildQuery({ expand })\n=\u003e '?$expand=Foo,Bar';\n```\n#### Filter expanded items\n```js\nconst expand = { Trips: { filter: { Name: 'Trip in US' } } };\nbuildQuery({ expand })\n=\u003e \"?$expand=Trips($filter=Name eq 'Trip in US')\";\n```\n\n#### Select only specific properties of expanded items\n```js\nconst expand = { Friends: { select: ['Name', 'Age'] } };\nbuildQuery({ expand })\n=\u003e '?$expand=Friends($select=Name,Age)';\n```\n\n#### Return only a subset of expanded items\n```js\nconst expand = { Friends: { top: 10 } };\nbuildQuery({ expand })\n=\u003e '?$expand=Friends($top=10)';\n```\n\n#### Order expanded items\n```js\nconst expand = { Products: { orderBy: 'ReleaseDate asc' } };\nbuildQuery({ expand })\n=\u003e \"?$expand=Products($orderby=ReleaseDate asc)\";\n```\n\n#### `filter`, `select`, `top`, and `orderBy` can be used together\nSelect only the first and last name of the top 10 friends who's first name starts with \"R\" and order by their last name\n```js\nconst expand = {\n  Friends: {\n    select: ['FirstName', 'LastName'],\n    top: 10,\n    filter: {\n      FirstName: { startswith: 'R' }\n    },\n    orderBy: 'LastName asc'\n  }\n};\nbuildQuery({ expand })\n=\u003e '?$expand=Friends($select=Name,Age;$top=10;$filter=startswith eq 'R'))';\n```\n\n### Pagination (skip and top)\n#### Get page 3 (25 records per page)\n```js\nconst page = 3;\nconst perPage = 25;\nconst top = perPage;\nconst skip = perPage * (page - 1);\nbuildQuery({ top, skip })\n=\u003e '?$top=25\u0026$skip=50'\n```\n\n### Single-item (key)\nSimple value\n```js\nconst key = 1;\nbuildQuery({ key })\n=\u003e '(1)'\n```\n\nAs object (explicit key property\n```js\nconst key = { Id: 1 };\nbuildQuery({ key })\n=\u003e '(Id=1)'\n```\n\n### Counting\nInclude count inline with result\n```js\nconst count = true;\nconst filter = { PropName: 1}\nbuildQuery({ count, filter })\n=\u003e '?$count=true\u0026$filter=PropName eq 1'\n```\n\nOr you can return only the count by passing a filter object to `count` (or empty object to count all)\n```js\nconst count = { PropName: 1 }\nconst query = buildQuery({ count })\n=\u003e '/$count?$filter=PropName eq 1'\n```\n\n### Actions\nAction on an entity\n```js\nconst key = 1;\nconst action = 'Test';\nbuildQuery({ key, action })\n=\u003e '(1)/Test'\n```\n\nAction on a collection\n```js\nconst action = 'Test';\nbuildQuery({ action })\n=\u003e '/Test'\n```\nAction parameters are passed in the body of the request.\n\n### Functions\nFunction on an entity\n```js\nconst key = 1;\nconst func = 'Test';\nbuildQuery({ key, func })\n=\u003e '(1)/Test'\n```\n\nFunction on an entity with parameters\n```js\nconst key = 1;\nconst func = { Test: { One: 1, Two: 2 } };\nbuildQuery({ key, func })\n=\u003e '(1)/Test(One=1,Two=2)'\n```\n\nFunction on a collection\n```js\nconst func = 'Test';\nbuildQuery({ func })\n=\u003e '/Test'\n```\n\nFunction on a collection with parameters\n```js\nconst func = { Test: { One: 1, Two: 2 } };\nbuildQuery({ func })\n=\u003e '/Test(One=1,Two=2)'\n```\n\n\n### Transforms\nTransforms can be passed as an object or an array (useful when applying the same transform more than once, such as `filter`)\n\nAggregations\n```js\nconst transform = {\n  aggregate: {\n    Amount: {\n      with: 'sum',\n      as: 'Total'\n    }\n  }\n};\nbuildQuery({ transform });\n=\u003e '?$apply=aggregate(Amount with sum as Total)';\n```\nSupported aggregations: `sum`, `min`, `max`, `average`, `countdistinct`\n\nGroup by (simple)\n```js\nconst transform = [{\n  groupBy: {\n    properties: ['SomeProp'],\n  }\n}]\nbuildQuery({ transform });\n=\u003e '?$apply=groupby((SomeProp))';\n```\n\nGroup by with aggregation\n```js\nconst transform = {\n  groupBy: {\n    properties: ['SomeProp'],\n    transform: {\n      aggregate: {\n        Id: {\n          with: 'countdistinct',\n          as: 'Total'\n        }\n      }\n    }\n  }\n}\nbuildQuery({ transform });\n=\u003e '?$apply=groupby((SomeProp),aggregate(Id with countdistinct as Total))';\n```\n\nGroup by with filtering before and after\n```js\nconst transform = [{\n  filter: {\n    PropName: 1\n  }\n},{\n  groupBy: {\n    properties: ['SomeProp'],\n    transform: [{\n      aggregate: {\n        Id: {\n          with: 'countdistinct',\n          as: 'Total'\n        }\n      }\n    }]\n  }\n},{\n  filter: {\n    Total: { ge: 5 }\n  }\n}]\nbuildQuery({ transform });\n=\u003e '?$apply=filter(PropName eq 1)/groupby((SomeProp),aggregate(Id with countdistinct as Total))/filter(Total ge 5)';\n```\n\nSupported transforms: `aggregate`, `groupby`, `filter`.  Additional transforms may be added later\n\n## OData specs\n- [OData Version 4.0. Part 1: Protocol Plus Errata 03](http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part1-protocol.html)\n- [OData Version 4.0. Part 2: URL Conventions Plus Errata 03](http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part2-url-conventions.html)\n- [OData Extension for Data Aggregation Version 4.0](http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/odata-data-aggregation-ext-v4.0.html)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechniq%2Fodata-query","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechniq%2Fodata-query","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechniq%2Fodata-query/lists"}