Code Monkey home page Code Monkey logo

odata-query's Introduction

odata-query

OData v4 query builder that uses a simple object-based syntax similar to MongoDB and js-data

Install

npm install odata-query

and then use the library

import buildQuery from '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.

Usage

See tests for examples as well

Filtering

buildQuery({ filter: {...} })
=> '?$filter=...'

Simple equality filter

const filter = { PropName: 1 };
buildQuery({ filter })
=> '?$filter=PropName eq 1'

Comparison operators

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)'

Logical operators

Implied and with an array of objects
const filter = [{ SomeProp: 1 }, { AnotherProp: 2 }, 'startswith(Name, "foo")'];
buildQuery({ filter })
=> '?$filter=SomeProp eq 1 and AnotherProp eq 2 and startswith(Name, "foo")'
Implied and with multiple comparison operators for a single property

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"
Explicit operator
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.

Collection operators

Empty any

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()'
Implied and

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)'
Explicit operator (and, or, and not)
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)))'
Implied all operators on collection item itself

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

Functions

String functions returning boolean
const filter = { PropName: { contains: 'foo' } };
buildQuery({ filter })
=> "$filter=contains(PropName,'foo')"

Supported operators: startswith, endswith, contains

Functions returning non-boolean values (string, int)
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

Functions returning non-boolean values (string, int) with parameters
const filter = { "indexof(PropName, 'foo')": { eq: 3 } };
buildQuery({ filter })
=> "$filter=indexof(PropName, 'foo') eq 3"

Supported operators: indexof, substring

Strings

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 })

Data types

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.

Search

const search = 'blue OR green';
buildQuery({ search });
=> '?$search=blue OR green';

Selecting

const select = ['Foo', 'Bar'];
buildQuery({ select })
=> '?$select=Foo,Bar'

Ordering

const orderBy = ['Foo desc', 'Bar'];
buildQuery({ orderBy })
=> '?$orderby=Foo desc,Bar'

Expanding

Nested expand using slash seperator

const expand = 'Friends/Photos'
buildQuery({ expand })
=> '?$expand=Friends($expand=Photos)';

Nested expand with an object

const expand = { Friends: { expand: 'Photos' } }
buildQuery({ expand })
=> '?$expand=Friends($expand=Photos)';

Multiple expands as an array

Supports both string (with slash seperators) and objects

const expand = ['Foo', 'Baz'];
buildQuery({ expand })
=> '?$expand=Foo,Bar';

Filter expanded items

const expand = { Trips: { filter: { Name: 'Trip in US' } } };
buildQuery({ expand })
=> "?$expand=Trips($filter=Name eq 'Trip in US')";

Select only specific properties of expanded items

const expand = { Friends: { select: ['Name', 'Age'] } };
buildQuery({ expand })
=> '?$expand=Friends($select=Name,Age)';

Return only a subset of expanded items

const expand = { Friends: { top: 10 } };
buildQuery({ expand })
=> '?$expand=Friends($top=10)';

Order expanded items

const expand = { Products: { orderBy: 'ReleaseDate asc' } };
buildQuery({ expand })
=> "?$expand=Products($orderby=ReleaseDate asc)";

filter, select, top, and orderBy can be used together

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'))';

Pagination (skip and top)

Get page 3 (25 records per page)

const page = 3;
const perPage = 25;
const top = perPage;
const skip = perPage * (page - 1);
buildQuery({ top, skip })
=> '?$top=25&$skip=50'

Single-item (key)

Simple value

const key = 1;
buildQuery({ key })
=> '(1)'

As object (explicit key property

const key = { Id: 1 };
buildQuery({ key })
=> '(Id=1)'

Counting

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'

Actions

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.

Functions

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

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

OData specs

odata-query's People

Contributors

antonespo avatar ashwindeshpande06 avatar boilingwaterr avatar diegomvh avatar fabio-bertone avatar gomball avatar jjavierdguezas avatar jods4 avatar martypowell avatar morganpollock avatar mrigne avatar odobosh-lohika-tix avatar samuelportz avatar sayan751 avatar sitek94 avatar techniq avatar toxik avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

odata-query's Issues

Add support for "legacy" compilation. Allow the library to be loaded with a simple <script> tag

Hi all.

Let me start with a "thank you for this simple but great piece of code". It saves me hours and hours.

Right now the lib has two compilation targets: commonjs and esm. Its enough by far if you use the library in a compiled (i.e webpack) project. Today i found myself in the need of "importing" the library on a legacy, almost vanillajs, es5 web project. It's not possible to 'import' or 'require' anything, so i cannot consume the library in this kind of project.

FEATURE REQUEST:
Add a new build target to bundle the lib for browsers (using browserify) so it creates a property buildQuery in the global 'window' object of the browsers.

NOTE: I opened a local brach with the new functionallity; everything seems to work fine. I would PR the changes but I cannot push the branch.

Expanding Properties on Derived Types

Hello,

I am using this library via https://github.com/diegomvh/angular-odata and have run into something that isn't currently supported. (I think)

My API has some derived types that I am trying to expand. It seems that odata-query is not able to describe the cast to the derived type in object form and manually entering the cast as a string value is interpreted as a nested expand. I understand the benefit of the string valued shorthand, but it precludes the ability to cast to the derived type.

Here is a contrived scenario:
Namespace: Example
Entities:

  • Shape { ID: number }
  • Circle : Shape { Radius: number }
  • Square : Shape { Size: number, Vertices: Vertex[] }
  • Vertex

Endpoints:

  • /Shapes
  • /Vertices

If I want to query all of the Shapes but expand the Vertices for any Square objects, the query would be:

[root]/Shapes?$expand=Example.Square/Vertices

The object form doesn't have the ability to specify type, which isn't too big a deal since it is a more complex scenario. However, passing the string of "Example.Square/Vertices" to the expand property results in the this query:

[root]/Shapes?$expand=Example.Square($expand=Vertices)

Obviously, the above isn't correct given the model.

I realize that removing or modifying the string shorthand would be a large breaking change for many. Given that, is there a workaround for this scenario? Would it be possible to add this ability within the object form? Other ideas?

Quick Edit: OData v4

Handling Typed Property Name Filter

It would be nice to have typed filter in order to avoid typo in the property names.
As an example:

interface Square {
  width: number;
  height: number; 
}
const filter: Filter<Square> = { width: { lt: 5 } };

It would be the foundation to introduce even typed nested properties handling.

$apply GroupBy Transform not capable of producing the query needed

This query works fine:

$apply=groupby(mfs/ID, aggregate(mfs(im_foo with average as Value)))

But odata-query can't produce that nested 'mfs' part wrapped in the aggregate ()

$apply=groupby(mfs/ID, aggregate(mfs(im_foo with average as Value)))

I tried transform with array, or objects, and nothing can produce that string:

  const transform = {
    groupBy: {
      properties: ['mfs/ID'],
      transform: {
        aggregate: [
          'mfs',
          {
            'mfs/im_Jitter': {
              with: 'average',
              as: 'Value',
            },
            'mfs/im_Latench': {
              with: 'average',
              as: 'Value1',
            },
            'mfs/im_PacketLossPercentage': {
              with: 'average',
              as: 'Value2',
            },
          },
        ],
      },
    },
  };

Support data aggregation transforms

Support all transforms defined within spec

  • aggregate
  • topcount
  • topsum
  • toppercent
  • bottomcount
  • bottomsum
  • bottompercent
  • identity
  • concat
  • groupby
  • filter
  • expand
  • search
  • compute
  • isdefined

Currently only aggregate, groupby, and filter are supported by the odata.net's UriQueryExpressionParser so supporting additional transforms are not a priority for a while (unless another backend shows support for these)

Created invalid query string by use collection operators

This filter:

const filter = {
  Results: {
    any: {
      ResultsProperties: {
        any: {
          SomeProp: 1
        }
      }
    }
  }
}

Create next query string:
?$filter=Results/any(r:r/ResultsProperties/any(r:r/SomeProp eq 1))
OData failed by this query with next exception:
The range variable 'r' has already been declared.

Support functions with a CollectionParameter as an array of a complex type

Per this StackOverflow answer we need to url encode the json for the array and use a parameter alias

 builder.EntityType<TestEntity>().Collection
    .Function("TestFunction2")
    .ReturnsCollectionFromEntitySet<TestEntity>("TestEntities")
    .CollectionParameter<person>("ArrayHere");
http://yourRestService/API/TestEntities/NS.TestFunction2(ArrayHere=@ArrayData)?@ArrayData=%5B%7B%22FirstName%22%3A%22Bob%22%2C+%22LastName%22%3A%22Dole%22%7D%2C%7B%22FirstName%22%3A%22Bill%22%2C+%22LastName%22%3A%22Clinton%22%7D%5D

Nodejs Compatible?

Can this be used with nodejs / express? If so, how?

The following results in: TypeError: buildQuery is not a function

const buildQuery = require('odata-query');
const filter = { PropName: 1 };
const query = buildQuery({filter});

Support for casting fields

Need to be able to create a filter like the following:

contains(cast(id,'String'),'medib') or contains(obj, 'medib')

Is there anyway to do this using this library? Read through the docs but can't find anything.

Double filters in querystring

Hi Sean,

First of all, thanks for your package, it saves a lot of time creating those queries. I'm facing an issue that deeper objects result in double queries. Please consider the following object:

{
    "filter": {
        "Nomination": {
            "ScheduledAt": {
                "ge": "2021-12-12T23:00:00.000Z",
                "le": "2021-12-13T22:59:59.999Z"
            },
            "NominationStatusValues": {
                "any": {
                    "NominationStatusID": 3
                }
            }
        }
    },
    "orderBy": "id",
    "count": true,
    "top": 15
}

this results in the following querystring (with some enters for readability):

$filter=
Nomination/ScheduledAt ge 2021-12-12T23:00:00.000Z and 
Nomination/ScheduledAt le 2021-12-13T22:59:59.999Z and 
Nomination/NominationStatusValues/any(nominationstatusvalues:nominationstatusvalues/NominationStatusID eq 3) and 
Nomination/ScheduledAt ge 2021-12-12T23:00:00.000Z and 
Nomination/ScheduledAt le 2021-12-13T22:59:59.999Z and 
Nomination/NominationStatusValues/any(nominationstatusvalues:nominationstatusvalues/NominationStatusID eq 3)
&$orderby=id&$count=true&$top=15

Am I doing something wrong or did I stumble upon a bug? I've tried several versions (6.7.1, 6.7.0, 6.6.0) but to no avail.

Thanks in advance

Comparing two columns

There doesn't seem to be an easy way of comparing two columns using this library, with raw odata we can do simple
?$filter=SomeProp eq AnotherProp
but if we do
const filter = { SomeProp: 'AnotherProp' };
then we would get
?$filter=SomeProp eq 'AnotherProp'
which would do string comparison with string 'AnotherProp' and not the column of that name.

I was thinking if there is no better way if we could expand data types with type 'column' to achieve this
const filter = { "someProp": { eq: { type: 'column', value: 'AnotherProp' } } };
Is there maybe a better way to do it, that I have missed?

Odata in and and

Hi I am trying to implement 'and' and 'in' in conjunction and it's not working because of the placement of () around the 'in'.

filter= { 
      and: [
          {prop1: `${prop1}`}, { prop2:`${prop2}`},  ({prop3: {'in': ['value1','value2'] }})]              
      };

The issue is it is doing
$filter=(prop1 eq prop1 and prop2 eq prop2 or prop3 eq value1 or prop3 eq value2)

what I want
$filter=(prop1 eq prop1 and prop2 eq prop2 and (prop3 eq value1 or prop3 eq value2 or...)

Where am I going wrong?

Cannot group filter with or operator

Request

"filter": {
    or: [{
        PropName: { gt: 5 },
        and: [
            { SomeProp: 1 },
            { AnotherProp: 2 }
        ]
    }

Expected

$filter=((PropName gt 5 or ((SomeProp eq 1) and (AnotherProp eq 2))))

Response

$filter=((PropName gt 5 and ((SomeProp eq 1) and (AnotherProp eq 2))))

Issue with not operator inside collection filter

I have the following scenario:

class User {
 id: string;
 [...]
 teams: Member[];
}

class Member {
 userId: string;
 user: User;
 teamId: string;
 team: Team;
}

class Team {
  id: string;
  title: string;
  [...]
}

and doing:

import buildQuery  from "odata-query";

let query = buildQuery({
  filter: {
    Teams: {
      all: {
        and: [
          {
            "Team/Title": {
              contains: "team1",
            },
          },
          {
            not: {
              "Team/Title": {
                contains: "team2",
              },
            },
          },
        ],
      },
    },
  },
});

console.log(query);

I expect:
?$filter=Teams/all(teams:((contains(teams/Team/Title,'team1')) and (not contains(teams/Team/Title,'team2'))))

but odata-query produce an invalid query:
?$filter=Teams/all(teams:((contains(teams/Team/Title,'team1')) and (contains(teams/not/Team/Title,'team2'))))

as you can see teams/not/Team/Title is wrong

Is this an issue or is there a way to achieve the expected query?

thanks in advance

ESM default export is not properly defined

I have a a following buildQueryTest.mjs:

import buildQuery from "odata-query";

console.log(buildQuery({ top: 5 }));

It fails the following way:

michal@dzieni dataverse-poc % node buildQueryTest.mjs 
file:///Users/michal/Projects/dataverse-poc/buildQueryTest.mjs:3
console.log(buildQuery({ top: 5 }));
            ^

TypeError: buildQuery is not a function
    at file:///Users/michal/Projects/dataverse-poc/buildQueryTest.mjs:3:13
    at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:541:24)
    at async loadESM (node:internal/process/esm_loader:83:5)
    at async handleMainPromise (node:internal/modules/run_main:65:12)

Node.js v18.6.0

Workaround is to use a following piece of code:

import _buildQuery from "odata-query";
const buildQuery = _buildQuery.default;

console.log(buildQuery({ top: 5 }));

Although it is incorrect according to what TS types say:
image

To sum it up: to properly import odata-query in ESM-powered app with TypeScript, I have to perform following gymnastics:

import _buildQuery from 'odata-query'
const buildQuery = (_buildQuery as unknown as {default: typeof _buildQuery}).default

Enable Support for Non Type Script Projects

When importing this package into a basic create-react-app you will get the following error:

Could not find dependency: 'tslib' relative to '/node_modules/odata-query/dist/commonjs/index.js'

Live Example.

The work around is to install tslib along side this package.

My recommendation would be to install tslib as a dependency rather than a devDependency, it seems tslib is recommending that anyway.

Can v6 be considered stable?

Hello,

First of all, thanks for the great tool!

Have a question - can v6 be considered stable?
I'm going to use it in the TypeScript project, so I need type definitions, which are missing in the latest one, which is 5.7.0.

Change how arrays/parens are handled

After some discussion in the PR about how to handle in and parens, I think we should implement a breaking change to make array ([ ]) usage more consistent and easier to reason about.

Current

Currently an array wraps each items within it in their own parens. (code)

} else if (Array.isArray(filters)) {
    const builtFilters = filters.map(f => buildFilter(f, propPrefix)).filter(f => f !== undefined);
    if (builtFilters.length) {
      return `${builtFilters.map(f => `(${f})`).join(` and `)}`
    }

which produces:

buildFilter({ filter: [{ SomeProp: 1 }, { AnotherProp: 2 }, "startswith(Name, 'R')"] });
=> "?$filter=(SomeProp eq 1) and (AnotherProp eq 2) and (startswith(Name, 'R'))";

Proposal

The proposal is to change the code to:

} else if (Array.isArray(filters)) {
    const builtFilters = filters.map(f => buildFilter(f, propPrefix)).filter(f => f !== undefined);
    if (builtFilters.length) {
      return `(${builtFilters.join(` and `)})`
    }

which would produce:

buildFilter({ filter: [{ SomeProp: 1 }, { AnotherProp: 2 }, "startswith(Name, 'R')"] });
=> "?$filter=(SomeProp eq 1 and AnotherProp eq 2 and startswith(Name, 'R'))";

You also do not need the individual objects for the first two (not a change, just a simplification of the example).

buildFilter({ filter: [{ SomeProp: 1, AnotherProp: 2 }, "startswith(Name, 'R')"] });
=> "?$filter=(SomeProp eq 1 and AnotherProp eq 2 and startswith(Name, 'R'))";

If you wanted to reproduce the original results (albeit with an extra wrapping parens around it all), you would wrap each item in an array:

buildFilter({ filter: [[{ SomeProp: 1 }], [{ AnotherProp: 2 }], ["startswith(Name, 'R')"]] });
=> "?$filter=((SomeProp eq 1) and (AnotherProp eq 2) and (startswith(Name, 'R')))";

In summary, the [ ] would literally translate as ( ) in the query. If this change is made, I would release it as a new major version since it could be breaking for some users.

Fix "null" handling

This comment (60f825b) mistakenly broke checking for null values.

For example, the following now returns nothing

const filter = { Foo: null };
buildQuery({ filter })
// => ""

A workaround is to specify the eq operator

const filter = { Foo: { eq: null };
buildQuery({ filter })
// => "?$filter=Foo eq null"

I think we should only check for undefined which will fix this regression but still support easier conditional filters

Possibility to extend escapeIllegalChars

Hi,

The library works great but I have an issue in combination with AWS API Gateway. It is choking on some characters { } ^ because they are not encoded.

So it would be nice to pass an option to the buildQuery function that makes it possible to extends escapeIllegalChars or pass a custom escapeIllegalChars function. I'm also wondering if it is possible to use encodeURIComponent instead of the current implementation? Is there a specific reason why it is not used?

I there an approach you prefer?

I'm happy to help you out by creating a pull request.

Kind regards

Add raw's description in documentation

I found raw usage in one of the PR - #9

{ SomeProp: { type: 'raw', value: datetime'${date.toISOString()}' }

Can you please add it's details in the documentation?

"Transforms" breaks sequence of transformations

Hi,

buildTransforms method firstly takes "aggregate" value and pushes to the result and only after it takes "filter" value and pushes to the result.

For example, we have the following query object:

{
  "transform": {
    "filter": [
      {
        "project": "some value"
      }
    ],
    "aggregate": {
      "id": {
        "with": "countdistinct",
        "as": "count"
      }
    }
  }
}

And result is:
$apply=aggregate(id with countdistinct as count)/filter(project eq 'some value')

And it's a wrong expression because first of all, we have to apply filter transformation.

Parse query-string back into queryOptions-object

I was wondering if there is a way to parse the query string and "recreate" the queryOptions.
Could be useful to receive the query in a node Mocking-Server and return some values based on a given filter for example, like this:

// frontend
const query = buildQuery(queryOptions);

// backend
const queryOptions = parseBackIntoObjectSomehow(query)

Query size limits

If my query is bigger than 50 KB, does it still work? :) (please assume server-side has no limits)

Collection operation error with nested functions

Say I want to have a case insensitive search on a property in a nested collection of items, where what I'm filtering is formed like:

DTO:

name: "<name>"
items: "<object array with property to search on 'searchProp'>"

So I form a filter like (where search is a search string):

        const filter = {
            or: {
                "toupper(name)": {
                    contains: search.toUpperCase()
                },
                "items": {
                    any: {
                        "toupper(searchProp)": {
                            contains: search.toUpperCase()
                        }
                    }
                }
            }
        }

        buildQuery({filter});

Currently the second toupper (in the any call) is prefixed with the lambda variable, resulting in an invalid call -- instead searchProp should be prefixed.

Result : $filter=contains(toupper(name),'C') or items/any(a:contains(a/toupper(searchProp),'C')

Which should actually be: $filter=contains(toupper(name),'C') or items/any(a:contains(toupper(a/searchProp),'C')

edit: I'm rather new to odata-queries, please do let me know if there is a prefered way to do this nested collection type of query.

Support `date` and `datetime` data type

Extracted from #20 (comment)

Regarding dates and times they do not appear to qualify the values with a type except optionally with a duration.

DateValue eq 2012-12-03
DateTimeOffsetValue eq 2012-12-03T07:16:23Z
DurationValue eq duration'P12DT23H59M59.999999999999S'
DurationValue eq 'P12DT23H59M59.999999999999S'
TimeOfDayValue eq 07:59:59.999

We could support the following in handleValue

  • { type: 'datetimeoffset', value: date } and a Date instance (like we do now):
    • date.toISOString() => 2012-12-03T07:16:23Z
  • { type: 'date', value: date }:
    • date.toISOString().split('T')[0] => 2012-12-03

We should also support date as either a Date instance or a string (ex. 2018-01-02):

const dateValue = (typeof date === 'string') ? new Date(date) : date instanceof Date ? date : null
const value = dateValue && dateValue.toISOString();

'in'-Operator needs to be grouped together

When using the in-Operator with following filter the query doesn't reflect the implied and

filter: {
        bar: 1,
        foo: {
            in: ['a', 'b'],
            contains: 'foo'
        }
    }
}

Result

?$filter=bar eq 1 and foo eq 'a' or foo eq 'b' and contains(foo,'foo')

Expected Outcome

?$filter=bar eq 1 and (foo eq 'a' or foo eq 'b' and contains(foo,'foo'))

Import issues

Hi Team ,
I have installed this library as per instruction, But when i am using it, its giving issue odataQuery is not defined.
Below are my code for test. I tested it on 6.9 and 8.0 both.
var odataQuery = require("odata-query")
const filter = [{ SomeProp: 1 }, { AnotherProp: 2 }, 'startswith(Name, "foo")'];
oDataQuery({ filter })
console.log('oDataQuery..'+oDataQuery);.

Thanks ,

indexOf function with collection not applying prefix variable when used with nested function

Input:

{
  Item: {
    any: {
      indexof(tolower(Name), 'gead'): {
        eq: -1
      }
    }
  }
}

Outputs -> Item/any(p:indexof(p/tolower(Name), 'adf') eq -1)

Expecting -> Item/any(p:indexof(tolower(p/Name), 'adf') eq -1)

Is there a syntax to achieve the above output? I tried switching indexof with tolower but it caused the lambda operator to not output. Any help would be appreciated! Thanks.

Originally posted by @fabio-b in #12 (comment)

Date type not reading UTC format

When converting from ISO String to Date object, the library leaves off the milliseconds before the timezone string.

Ex.
Convert 2017-06-05T00:00:00.000Z to date object and pass to filter.
Filter creates URL string with 2017-06-05T00:00:00Z
The URL string should be 2017-06-05T00:00:00.000Z

Inside the filter object, I'm passing this date object:
Sun Jun 04 2017 20:00:00 GMT-0400 (EDT)

Let me know if you need me to provide anything else

Filter functions contain space causing Odata error

When passing filter functions such as contains, startswith, endswith, etc. there's a space being added between column name and value.

With updates to Odata it might have a stricter check to avoid spacing inside the function.

startswith(Name, "foo") should be
startswith(Name,"foo") <<<<<<< No space inside

Can't enter datetime (moment) objects in a filter

Hi!

I'm trying to figure out how to supply a date as an argument to a filter.

My code:

StatusHistory: { filter: { StartDateTime: { le: '2019-06-08T00:00:00Z' }, EndDateTime: { gt: '2019-06-08T00:00:00Z' }, ServiceStatusEnum: 'supplied' } }

This generates a query with both the dates and 'supplied' in quotes, but i don't want the dates in quotes because my backend thinks theyre string and not dates so i get invalid request.

I saw another issue about datetime object but didn't really understand the result of that post or if it has really anything to do with this problem

Any idea on how I could achieve this? Changing the backend is no option. Sorry for any ignorance i'm pretty new to odata and its queries, thanks

IN operator

Currently we explode an in operator into multiple eq/or. For example:

const filter = { SomeProp: { in: [1, 2, 3] } };
const actual = buildQuery({ filter });
=> '?$filter=(SomeProp eq 1 or SomeProp eq 2 or SomeProp eq 3)'

When attempting to do this with a lot of values, I received a 414 (URI Too Long) error. Upon more researching, I found this proposal to support an actual in operator, and it appears to be accepted.: https://issues.oasis-open.org/browse/ODATA-556

I then tested against my WebApi/OData backend and it appears to work when using $filter=Id in (1,2,3,4,5) or $filter=Code in ('abc','def','ghi')

While not in the 4.0 spec, I just found mention of it in the 4.01 spec part1 and part 2.

Considering supporting this and releasing as a breaking change (6.0.0)

Support cast operator

I would like to submit a feature request.

At present there is no support for cast operator (search for "cast" here). For example, if I want to filter my collection based on some binary data, I would write the query as follows.

https://awesome.service/odata/v4/AwesomeEntities?$filter=Files/any(a:a/Hash eq cast('S0mEhAsH', Edm.Binary))&$expand=Files

However, currently I can't pass an object to query builder that can sufficiently represent this clause, like I can do it for other operators like eq, such as {PropertyName: {eq: "SomeValue"}}. I need to pass the string instead to the query builder. It would be much prettier if I can pass objects like below to the query builder for this purpose.

{
  PropertyName: { eq: { cast: "Edm.TYPE", ValueExpr } }
}

// produces --> PropertyName eq cast(ValueExpr, "Edm.TYPE")

And similar behavior is naturally expected for navigational properties (including collections). What are your thoughts on this?

Lastly, thank you for this cool package. ๐Ÿ˜ƒ

Filter with Any clause bad conversion

Hi,
thank you for your amazing query builder.

I ran into this problem: if I pass a filter object with this structure:
{ "Module1/Module_CategoryModule": { "any": { "Module_CategoryId": { "eq": 2 } } } }

I get this query, which throws a Syntax error in Restier OData:
$filter=((Module1/Module_CategoryModule/any(module1/module_categorymodule:module1/module_categorymodule/Module_CategoryId%20eq%202)))

but actually the correct conversion would be this:
$filter=((Module1/Module_CategoryModule/any(c:c/Module_CategoryId%20eq%202)))

Did I do something wrong in the filter syntax?

Thank you
Marco

Automatically Title-Case Camel-Cased props

So to better enforce type safety in a project - a developer may want to say let's only allow a string that adheres to keyof some TYPE for OData operations $orderBy or $filter- the issue is that keys of some TYPE are usually camelCased, but OData wants prop names in TitleCase format. Does it make sense to enhance buildQuery to properly format a camelCased "key" into a valid odata prop name that is TitleCased?

'not' operator is being applied only to the first condition with and/or operators

I've tried to use the new 'not' operator for some of my logic while I noticed it doesn't get applied correctly - it is applied 'not' to the first condition instead of the whole group

It loolks like the example presents the issue as well:
const filter = {
not: {
and:[
{SomeProp: 1},
{AnotherProp: 2}
]
}
};

buildQuery({ filter })
=> '?$filter=(not (SomeProp eq 1) and (AnotherProp eq 2))'

It should be
=> '?$filter=(not ((SomeProp eq 1) and (AnotherProp eq 2)))'

as otherwise it would be considered as
(not SomeProp eq 1)
and (AnotherProp eq 2)

Nested filter on collections not constructing proper prefixes

Hello, any help would be appreciated!
When filtering inside an $expand, the lamda that gets applied is missing the necessary prefixes.

Input:

{ expand: { Item: { filter: { any: { ItemsProp: true } } } } }

output:

$expand=Item($filter=any/ItemsProp eq true)

Expected output:

$expand=Item($filter=Item/any(x:x/ItemsProp eq true))

Regular top level filter applies the proper lamda. Is this a problem in how I'm constructing the object?

Thanks

Feature Request: Add option to forego Illegal Character Encoding

Great library and very helpful in working with OData! However, there is one small pain point I have encountered in use.

In some instances, I'd like to perform HTML encoding on Unicode chars via encodeURIComponent. When doing that in conjunction with buildQuery, some of the chars get encoded twice.

Would you consider adding the option of passing in an argument to forego character escaping (defaulting to performing the escape of course, so as not to affect existing users).

Perhaps implemented as a second positional parameter that destructures into possible configuration options. Something like below for instance:

export default function({
  select,
  filter,
  search,
  groupBy,
  transform,
  orderBy,
  top,
  skip,
  key,
  count,
  expand,
  action,
  func,
  format
} = {},
{
  illegalCharEscape = true,
} = {}) {

An option like that would be very helpful for me, and I imagine other users as well.

$top and $skip are ignored when the value is 0

Here:

odata-query/src/index.ts

Lines 496 to 500 in f38eb3d

for (const key of Object.getOwnPropertyNames(params)) {
if (params[key]) {
queries.push(`${key}=${params[key]}`);
}
}

params[key] is 0 so is a falsy value, but it would be nice to make requests with $top=0 and $skip=0 (Right know it is ignored)

buildQuery({ filter: { SomeProp: 1 } } ); // top is undefined skip is undefined
// Expected: ?$filter=SomeProp eq 1
//   Actual: ?$filter=SomeProp eq 1 โœ…

buildQuery({ filter: { SomeProp: 1 }, top: 0, skip: 0 } );
// Expected: ?$filter=SomeProp eq 1&$top=0&skip=0
//   Actual: ?$filter=SomeProp eq 1 โŒ

buildQuery({ filter: { SomeProp: 1 }, top: 3 } ); //skip is undefined
// Expected: ?$filter=SomeProp eq 1&$top=3
//   Actual: ?$filter=SomeProp eq 1&$top=3 โœ…

buildQuery({ filter: { SomeProp: 1 }, top: 3, skip: 0 } );
// Expected: ?$filter=SomeProp eq 1&$top=3&skip=0
//   Actual: ?$filter=SomeProp eq 1&$top=3 โŒ

buildQuery({ filter: { SomeProp: 1 }, skip: 3 } ); // top is undefined 
// Expected: ?$filter=SomeProp eq 1&$skip=3
//   Actual: ?$filter=SomeProp eq 1&$skip=3 โœ…

buildQuery({ filter: { SomeProp: 1 }, top: 0, skip: 3 } );
// Expected: ?$filter=SomeProp eq 1&$top=0&skip=3
//   Actual: ?$filter=SomeProp eq 1&$skip=3 โŒ

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.