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 🙏

© 2025 – Pkg Stats / Ryan Hefner

odata-builder

v1.0.3

Published

Type-safe OData v4.01 query builder for TypeScript with compile-time validation. Fluent FilterBuilder API, lambda expressions (any/all), in/not/has operators.

Downloads

3,326

Readme

odata-builder

Generate Typesafe OData Queries with Ease. odata-builder ensures your queries are correct as you write them, eliminating worries about incorrect query formats.

build and test npm version

What you get

  • Fully type-safe OData v4.01 query generation
  • Compile-time validation for filters and search expressions
  • Fluent builder and serializable object syntax

What you need to know

  • in() requires OData 4.01 (legacy fallback available)
  • has() requires a valid OData enum literal (raw passthrough)
  • Server support for not may vary

Install

npm install --save odata-builder

or

yarn add odata-builder

Quick Start

import { OdataQueryBuilder } from 'odata-builder';

interface User {
    name: string;
    age: number;
}

new OdataQueryBuilder<User>()
    .filter(f => f.where(x => x.name.eq('John')))
    .select('name', 'age')
    .orderBy({ field: 'name', orderDirection: 'asc' })
    .top(10)
    .toQuery();
// ?$filter=name eq 'John'&$select=name,age&$orderby=name asc&$top=10

Filter Syntax

odata-builder offers two equivalent ways to build filters - both with full IntelliSense and type safety:

| Approach | Style | | ----------------- | ------------------------ | | FilterBuilder | Fluent, chainable | | Object Syntax | Declarative, data-driven |

Both produce identical OData queries. The FilterBuilder internally creates the same filter objects, making them fully interchangeable.


FilterBuilder

// Complex filter with AND/OR
new OdataQueryBuilder<User>()
    .filter(f =>
        f
            .where(x => x.name.contains('John'))
            .and(x => x.age.gt(18))
            .or(x => x.isActive.isTrue()),
    )
    .toQuery();
// ?$filter=((contains(name, 'John') and age gt 18) or isActive eq true)

// Array filtering with lambda expressions
new OdataQueryBuilder<User>()
    .filter(f => f.where(x => x.tags.any(t => t.s.eq('admin'))))
    .toQuery();
// ?$filter=tags/any(s: s eq 'admin')

Object Syntax

// Complex filter with AND/OR
new OdataQueryBuilder<User>()
    .filter({
        logic: 'or',
        filters: [
            {
                logic: 'and',
                filters: [
                    { field: 'name', operator: 'contains', value: 'John' },
                    { field: 'age', operator: 'gt', value: 18 },
                ],
            },
            { field: 'isActive', operator: 'eq', value: true },
        ],
    })
    .toQuery();
// ?$filter=((contains(name, 'John') and age gt 18) or isActive eq true)

// Array filtering with lambda expressions
new OdataQueryBuilder<User>()
    .filter({
        field: 'tags',
        lambdaOperator: 'any',
        expression: {
            field: 's',
            operator: 'eq',
            value: 'admin',
        },
    })
    .toQuery();
// ?$filter=tags/any(s: s eq 'admin')

Key Features

in Operator

Membership testing for values in a list (OData 4.01):

new OdataQueryBuilder<User>()
    .filter(f => f.where(x => x.name.in(['John', 'Jane', 'Bob'])))
    .toQuery();
// ?$filter=name in ('John', 'Jane', 'Bob')

// For OData 4.0 servers: use legacyInOperator option
new OdataQueryBuilder<User>({ legacyInOperator: true })
    .filter(f => f.where(x => x.name.in(['John', 'Jane'])))
    .toQuery();
// ?$filter=(name eq 'John' or name eq 'Jane')

not Operator

Negate any filter expression:

new OdataQueryBuilder<User>()
    .filter(f => f.where(x => x.name.contains('test')).not())
    .toQuery();
// ?$filter=not (contains(name, 'test'))

new OdataQueryBuilder<User>()
    .filter(f =>
        f
            .where(x => x.name.eq('John'))
            .and(x => x.age.gt(18))
            .not(),
    )
    .toQuery();
// ?$filter=not ((name eq 'John' and age gt 18))

Note: not() always negates the entire current filter expression, not just the last condition.

has Operator

Check for enum flag values:

new OdataQueryBuilder<Product>()
    .filter(f => f.where(x => x.style.has("Sales.Color'Yellow'")))
    .toQuery();
// ?$filter=style has Sales.Color'Yellow'

Important: has() does not validate enum literals. You must provide a valid OData enum literal (e.g. Namespace.EnumType'Value'). The value is passed through unchanged.


Advanced Filtering

String Operations

// Case-insensitive contains
f.where(x => x.name.ignoreCase().contains('john'));
// contains(tolower(name), 'john')

// String transforms
f.where(x => x.name.tolower().trim().eq('john'));
// trim(tolower(name)) eq 'john'

// String functions
f.where(x => x.name.length().gt(5));
// length(name) gt 5

f.where(x => x.name.substring(0, 3).eq('Joh'));
// substring(name, 0, 3) eq 'Joh'

Number Operations

// Arithmetic
f.where(x => x.price.mul(1.1).lt(100));
// price mul 1.1 lt 100

// Rounding
f.where(x => x.score.round().eq(5));
// round(score) eq 5

Date Operations

// Extract date parts
f.where(x => x.createdAt.year().eq(2024));
// year(createdAt) eq 2024

f.where(x => x.createdAt.month().ge(6));
// month(createdAt) ge 6

Lambda Expressions

Filter array fields with any and all:

// Simple array with contains
new OdataQueryBuilder<User>()
    .filter({
        field: 'tags',
        lambdaOperator: 'any',
        expression: {
            field: 's',
            operator: 'eq',
            value: true,
            ignoreCase: true,
            function: {
                type: 'contains',
                value: 'test',
            },
        },
    })
    .toQuery();
// ?$filter=tags/any(s: contains(tolower(s), 'test'))

// Array of objects
new OdataQueryBuilder<User>()
    .filter({
        field: 'addresses',
        lambdaOperator: 'any',
        expression: {
            field: 'city',
            operator: 'eq',
            value: 'Berlin',
        },
    })
    .toQuery();
// ?$filter=addresses/any(s: s/city eq 'Berlin')

Search

Simple Search

new OdataQueryBuilder<User>().search('simple search term').toQuery();
// ?$search=simple%20search%20term

SearchExpressionBuilder

For complex search requirements:

import { SearchExpressionBuilder } from 'odata-builder';

new OdataQueryBuilder<User>()
    .search(
        new SearchExpressionBuilder()
            .term('red')
            .and()
            .term('blue')
            .or()
            .group(
                new SearchExpressionBuilder()
                    .term('green')
                    .not(new SearchExpressionBuilder().term('yellow')),
            ),
    )
    .toQuery();
// ?$search=(red%20AND%20blue%20OR%20(green%20AND%20(NOT%20yellow)))

Methods: term(), phrase(), and(), or(), not(), group()


Query Options

select

Choose which properties to return. Supports nested paths:

new OdataQueryBuilder<User>()
    .select('name', 'address/city', 'address/zip')
    .toQuery();
// ?$select=name,address/city,address/zip

expand

Include related entities. Supports nested paths:

new OdataQueryBuilder<User>().expand('company', 'company/address').toQuery();
// ?$expand=company,company/address

orderBy

Sort results:

new OdataQueryBuilder<User>()
    .orderBy({ field: 'name', orderDirection: 'asc' })
    .orderBy({ field: 'age', orderDirection: 'desc' })
    .toQuery();
// ?$orderby=name asc,age desc

top / skip

Pagination:

new OdataQueryBuilder<User>().top(10).skip(20).toQuery();
// ?$top=10&$skip=20

count

Include total count:

new OdataQueryBuilder<User>().count().toQuery();
// ?$count=true

// Count endpoint only
new OdataQueryBuilder<User>().count(true).toQuery();
// /$count

GUID Handling

import { Guid, OdataQueryBuilder } from 'odata-builder';

interface Entity {
    id: Guid;
}

new OdataQueryBuilder<Entity>()
    .filter({
        field: 'id',
        operator: 'eq',
        value: 'f92477a9-5761-485a-b7cd-30561e2f888b',
        removeQuotes: true,
    })
    .toQuery();
// ?$filter=id eq f92477a9-5761-485a-b7cd-30561e2f888b

Most OData servers accept GUIDs without quotes. If your server requires quoted GUIDs, omit removeQuotes.


Server Compatibility

| Feature | OData Version | Notes | | -------------- | ------------- | ------------------------------ | | in operator | 4.01 | Use legacyInOperator for 4.0 | | not operator | 4.0+ | Some servers have limited support | | has operator | 4.0+ | Requires correct enum literal |

Check your server's $metadata endpoint or try a feature probe query. If in returns 400, switch to legacy mode.


Design Principles

  • Type safety over runtime validation
  • Explicit over implicit behavior
  • Server compatibility over clever syntax
  • No hidden query rewriting

Contributing

Your contributions are welcome! If there's a feature you'd like to see in odata-builder, or if you encounter any issues, please feel free to open an issue or submit a pull request.