npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ps-odata-query

v1.0.1

Published

A modified version of odata-query with custom compute feature

Downloads

120

Readme

ps-odata-query

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

Install

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.

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