OData v4 query builder that uses a simple object-based syntax similar to MongoDB and js-data
npm install ps-odata-query
and then use the library
import buildQuery from 'ps-odata-query'
const query = buildQuery({...})
fetch(`http://localhost${query}`)
where the query object syntax for {...}
is defined below. There is also react-odata which utilizies this library for a declarative React component.
See tests for examples as well
- Filtering
- Selecting
- Ordering
- Expanding
- Pagination (skip and top)
- Single-item (key)
- Counting
- Actions
- Functions
- Transforms
buildQuery({ filter: {...} })
=> '?$filter=...'
const filter = { PropName: 1 };
buildQuery({ filter })
=> '?$filter=PropName eq 1'
const filter = { PropName: { gt: 5 } };
buildQuery({ filter })
=> '?$filter=PropName gt 5'
Supported operators: eq
, ne
, gt
, ge
, lt
, le
, in
Using the in
operator is also similar to the previous example.
const filter = { PropName: { in: [1, 2, 3] } };
buildQuery({ filter })
=> '?$filter=PropName in (1,2,3)'
const filter = [{ SomeProp: 1 }, { AnotherProp: 2 }, 'startswith(Name, "foo")'];
buildQuery({ filter })
=> '?$filter=SomeProp eq 1 and AnotherProp eq 2 and startswith(Name, "foo")'
Useful to perform a between
query on a Date
property
const startDate = new Date(Date.UTC(2017, 0, 1))
const endDate = new Date(Date.UTC(2017, 2, 1))
const filter = { DateProp: { ge: startDate, le: endDate } }
buildQuery({ filter })
=> "?$filter=DateProp ge 2017-01-01T00:00:00Z and DateProp le 2017-03-01T00:00:00Z"
const filter = {
and: [
{ SomeProp: 1 },
{ AnotherProp: 2 },
'startswith(Name, "foo")'
]
};
buildQuery({ filter })
=> '?$filter=SomeProp eq 1 and AnotherProp eq 2 and startswith(Name, "foo")'
const filter = {
not: {
and:[
{SomeProp: 1},
{AnotherProp: 2}
]
}
};
buildQuery({ filter })
=> '?$filter=(not (SomeProp eq 1) and (AnotherProp eq 2))'
Supported operators: and
, or
, and not
.
Using an empty object
const filter = {
ItemsProp: {
any: {}
}
};
buildQuery({ filter })
=> '?$filter=ItemsProp/any()'
or also as an empty array
const filter = {
ItemsProp: {
any: []
}
};
buildQuery({ filter })
=> '?$filter=ItemsProp/any()'
Using an object
const filter = {
ItemsProp: {
any: {
SomeProp: 1,
AnotherProp: 2
}
}
};
buildQuery({ filter })
=> '?$filter=ItemsProp/any(i:i/SomeProp eq 1 and i/AnotherProp eq 2)'
or also as an array of object
const filter = {
ItemsProp: {
any: [
{ SomeProp: 1 },
{ AnotherProp: 2},
]
}
};
buildQuery({ filter })
=> '?$filter=ItemsProp/any(i:i/SomeProp eq 1 and i/AnotherProp eq 2)'
const filter = {
ItemsProp: {
any: {
or: [
{ SomeProp: 1 },
{ AnotherProp: 2},
]
}
}
};
buildQuery({ filter })
=> '?$filter=ItemsProp/any(i:(i/SomeProp eq 1 or i/AnotherProp eq 2)'
const filter = {
not: {
ItemsProp: {
any: {
or: [
{ SomeProp: 1 },
{ AnotherProp: 2},
]
}
}
}
};
buildQuery({ filter })
=> '?$filter=not ItemsProp/any(i:((i/SomeProp eq 1) or (i/AnotherProp eq 2)))'
ITEM_ROOT is special constant to mark collection with primitive type
'in' operator
const filter = {
tags: {
any: {
[ITEM_ROOT]: { in: ['tag1', 'tag2']},
},
},
};
buildQuery({ filter })
=> "?$filter=tags/any(tags:tags in ('tag1','tag2'))"
'or' operator on collection item itself
const filter = {
tags: {
any: {
or: [
{ [ITEM_ROOT]: 'tag1'},
{ [ITEM_ROOT]: 'tag2'},
]
}
}
};
buildQuery({ filter })
=> "?$filter=tags/any(tags:((tags eq 'tag1') or (tags eq 'tag2')))";
'and' operator on collection item itself and nested item
const filter = {
tags: {
any: [
{ [ITEM_ROOT]: 'tag1'},
{ [ITEM_ROOT]: 'tag2'},
{ prop: 'tag3'},
]
}
};
buildQuery({ filter });
=> "?$filter=tags/any(tags:tags eq 'tag1' and tags eq 'tag2' and tags/prop eq 'tag3')";
Function on collection item itself
const filter = {
tags: {
any: {
[`tolower(${ITEM_ROOT})`]: 'tag1'
}
}
};
buildQuery({ filter });
=> "?$filter=tags/any(tags:tolower(tags) eq 'tag1')";
Supported operators: any
, all
const filter = { PropName: { contains: 'foo' } };
buildQuery({ filter })
=> "$filter=contains(PropName,'foo')"
Supported operators: startswith
, endswith
, contains
const filter = { 'length(PropName)': { gt: 10 } };
buildQuery({ filter })
=> "$filter=length(PropName) gt 10"
Supported operators: length
, tolower
, toupper
, trim
,
day
, month
, year
, hour
, minute
, second
,
round
, floor
, ceiling
const filter = { "indexof(PropName, 'foo')": { eq: 3 } };
buildQuery({ filter })
=> "$filter=indexof(PropName, 'foo') eq 3"
Supported operators: indexof
, substring
A 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 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.
import f from 'odata-filter-builder';
const filter = f().eq('TypeId', '1')
.contains(x => x.toLower('Name'), 'a')
.toString();
buildQuery({ filter })
GUID:
const filter = { "someProp": { eq: { type: 'guid', value: 'cd5977c2-4a64-42de-b2fc-7fe4707c65cd' } } };
buildQuery({ filter })
=> "?$filter=someProp eq cd5977c2-4a64-42de-b2fc-7fe4707c65cd"
Duration:
const filter = { "someProp": { eq: { type: 'duration', value: 'PT1H' } } };
buildQuery({ filter })
=> "?$filter=someProp eq duration'PT1H'"
Binary:
const filter = { "someProp": { eq: { type: 'binary', value: 'YmluYXJ5RGF0YQ==' } } };
buildQuery({ filter })
=> "?$filter=someProp eq binary'YmluYXJ5RGF0YQ=='"
Decimal:
const filter = { "someProp": { eq: { type: 'decimal', value: '12.3456789' } } };
buildQuery({ filter })
=> "?$filter=someProp eq 12.3456789M"
Raw:
const filter = { "someProp": { eq: { type: 'raw', value: `datetime'${date.toISOString()}'` } } };
buildQuery({ filter })
=> "?$filter=someProp eq datetime'2021-07-08T12:27:08.122Z'"
- Provides full control over the serialization of the value. Useful to pass a data type.
Note that as per OData specification, binary data is transmitted as a base64 encoded string. Refer to Primitive Types in JSON Format, and binary representation.
const search = 'blue OR green';
buildQuery({ search });
=> '?$search=blue OR green';
const select = ['Foo', 'Bar'];
buildQuery({ select })
=> '?$select=Foo,Bar'
const orderBy = ['Foo desc', 'Bar'];
buildQuery({ orderBy })
=> '?$orderby=Foo desc,Bar'
const expand = 'Friends/Photos'
buildQuery({ expand })
=> '?$expand=Friends($expand=Photos)';
const expand = { Friends: { expand: 'Photos' } }
buildQuery({ expand })
=> '?$expand=Friends($expand=Photos)';
Supports both string (with slash seperators) and objects
const expand = ['Foo', 'Baz'];
buildQuery({ expand })
=> '?$expand=Foo,Bar';
const expand = { Trips: { filter: { Name: 'Trip in US' } } };
buildQuery({ expand })
=> "?$expand=Trips($filter=Name eq 'Trip in US')";
const expand = { Friends: { select: ['Name', 'Age'] } };
buildQuery({ expand })
=> '?$expand=Friends($select=Name,Age)';
const expand = { Friends: { top: 10 } };
buildQuery({ expand })
=> '?$expand=Friends($top=10)';
const expand = { Products: { orderBy: 'ReleaseDate asc' } };
buildQuery({ expand })
=> "?$expand=Products($orderby=ReleaseDate asc)";
Select only the first and last name of the top 10 friends who's first name starts with "R" and order by their last name
const expand = {
Friends: {
select: ['FirstName', 'LastName'],
top: 10,
filter: {
FirstName: { startswith: 'R' }
},
orderBy: 'LastName asc'
}
};
buildQuery({ expand })
=> '?$expand=Friends($select=Name,Age;$top=10;$filter=startswith eq 'R'))';
const page = 3;
const perPage = 25;
const top = perPage;
const skip = perPage * (page - 1);
buildQuery({ top, skip })
=> '?$top=25&$skip=50'
Simple value
const key = 1;
buildQuery({ key })
=> '(1)'
As object (explicit key property
const key = { Id: 1 };
buildQuery({ key })
=> '(Id=1)'
Include count inline with result
const count = true;
const filter = { PropName: 1}
buildQuery({ count, filter })
=> '?$count=true&$filter=PropName eq 1'
Or you can return only the count by passing a filter object to count
(or empty object to count all)
const count = { PropName: 1 }
const query = buildQuery({ count })
=> '/$count?$filter=PropName eq 1'
Action on an entity
const key = 1;
const action = 'Test';
buildQuery({ key, action })
=> '(1)/Test'
Action on a collection
const action = 'Test';
buildQuery({ action })
=> '/Test'
Action parameters are passed in the body of the request.
Function on an entity
const key = 1;
const func = 'Test';
buildQuery({ key, func })
=> '(1)/Test'
Function on an entity with parameters
const key = 1;
const func = { Test: { One: 1, Two: 2 } };
buildQuery({ key, func })
=> '(1)/Test(One=1,Two=2)'
Function on a collection
const func = 'Test';
buildQuery({ func })
=> '/Test'
Function on a collection with parameters
const func = { Test: { One: 1, Two: 2 } };
buildQuery({ func })
=> '/Test(One=1,Two=2)'
Transforms can be passed as an object or an array (useful when applying the same transform more than once, such as filter
)
Aggregations
const transform = {
aggregate: {
Amount: {
with: 'sum',
as: 'Total'
}
}
};
buildQuery({ transform });
=> '?$apply=aggregate(Amount with sum as Total)';
Supported aggregations: sum
, min
, max
, average
, countdistinct
Group by (simple)
const transform = [{
groupBy: {
properties: ['SomeProp'],
}
}]
buildQuery({ transform });
=> '?$apply=groupby((SomeProp))';
Group by with aggregation
const transform = {
groupBy: {
properties: ['SomeProp'],
transform: {
aggregate: {
Id: {
with: 'countdistinct',
as: 'Total'
}
}
}
}
}
buildQuery({ transform });
=> '?$apply=groupby((SomeProp),aggregate(Id with countdistinct as Total))';
Group by with filtering before and after
const transform = [{
filter: {
PropName: 1
}
},{
groupBy: {
properties: ['SomeProp'],
transform: [{
aggregate: {
Id: {
with: 'countdistinct',
as: 'Total'
}
}
}]
}
},{
filter: {
Total: { ge: 5 }
}
}]
buildQuery({ transform });
=> '?$apply=filter(PropName eq 1)/groupby((SomeProp),aggregate(Id with countdistinct as Total))/filter(Total ge 5)';
Supported transforms: aggregate
, groupby
, filter
. Additional transforms may be added later