odata-builder
TypeScript icon, indicating that this package has built-in type declarations

0.8.0 • Public • Published

odata-builder

Generate Typesafe OData v4 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

⚠️ Breaking Changes in v0.8.0

Important: This version introduces breaking changes to lambda expressions and function syntax. Please review the Migration Guide below.

Features

  • Complete TypeScript Support with perfect autocomplete
  • 🔒 Type-Safe Queries - catch errors at compile time
  • 📊 Comprehensive OData v4 Support including functions, lambda expressions, and advanced filtering
  • 🚀 Performance Optimized with minimal runtime overhead
  • 🎯 Developer Experience focused with extensive IntelliSense support

Install

Install odata-builder using your preferred package manager:

npm install --save odata-builder

or

yarn add odata-builder

Quick Start

import { OdataQueryBuilder } from 'odata-builder';

type Product = {
    id: number;
    name: string;
    price: number;
    category: string;
    isActive: boolean;
}

const query = new OdataQueryBuilder<Product>()
    .filter({ field: 'isActive', operator: 'eq', value: true })
    .filter({ field: 'price', operator: 'gt', value: 100 })
    .orderBy({ field: 'name', orderDirection: 'asc' })
    .top(10)
    .toQuery();
// Result: ?$filter=isActive eq true and price gt 100&$orderby=name asc&$top=10

Core Query Operations

Basic Filtering

const queryBuilder = new OdataQueryBuilder<Product>()
    .filter({ field: 'name', operator: 'eq', value: 'iPhone' })
    .filter({ field: 'price', operator: 'gt', value: 500 })
    .toQuery();
// Result: ?$filter=name eq 'iPhone' and price gt 500

Count and Data Retrieval

// Count with filters
const countQuery = new OdataQueryBuilder<Product>()
    .count()
    .filter({ field: 'isActive', operator: 'eq', value: true })
    .toQuery();
// Result: ?$count=true&$filter=isActive eq true

// Count only (no data)
const countOnlyQuery = new OdataQueryBuilder<Product>()
    .count(true)
    .filter({ field: 'category', operator: 'eq', value: 'Electronics' })
    .toQuery();
// Result: /$count?$filter=category eq 'Electronics'

Selection and Ordering

const query = new OdataQueryBuilder<Product>()
    .select('name', 'price', 'category')
    .orderBy({ field: 'price', orderDirection: 'desc' })
    .orderBy({ field: 'name', orderDirection: 'asc' })
    .top(20)
    .skip(40)
    .toQuery();
// Result: ?$select=name,price,category&$orderby=price desc,name asc&$top=20&$skip=40

Advanced Filtering

GUID Support

import { Guid } from 'odata-builder';

type User = {
    id: Guid;
    name: string;
}

const query = new OdataQueryBuilder<User>()
    .filter({
        field: 'id',
        operator: 'eq',
        value: 'f92477a9-5761-485a-b7cd-30561e2f888b',
        removeQuotes: true  // Optional: removes quotes around GUID
    })
    .toQuery();
// Result: ?$filter=id eq f92477a9-5761-485a-b7cd-30561e2f888b

Case-Insensitive Filtering

const query = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        operator: 'contains',
        value: 'apple',
        ignoreCase: true
    })
    .toQuery();
// Result: ?$filter=contains(tolower(name), 'apple')

Combined Filters

const query = new OdataQueryBuilder<Product>()
    .filter({
        logic: 'and',
        filters: [
            { field: 'isActive', operator: 'eq', value: true },
            {
                logic: 'or',
                filters: [
                    { field: 'category', operator: 'eq', value: 'Electronics' },
                    { field: 'category', operator: 'eq', value: 'Books' }
                ]
            }
        ]
    })
    .toQuery();
// Result: ?$filter=(isActive eq true and (category eq 'Electronics' or category eq 'Books'))

OData Functions

String Functions

// String length
const query = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        function: { type: 'length' },
        operator: 'gt',
        value: 10
    })
    .toQuery();
// Result: ?$filter=length(name) gt 10

// String concatenation
const concatQuery = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        function: {
            type: 'concat',
            values: [' - ', { fieldReference: 'category' }]
        },
        operator: 'eq',
        value: 'iPhone - Electronics'
    })
    .toQuery();
// Result: ?$filter=concat(name, ' - ', category) eq 'iPhone - Electronics'

// Substring extraction
const substringQuery = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        function: { type: 'substring', start: 0, length: 5 },
        operator: 'eq',
        value: 'iPhone'
    })
    .toQuery();
// Result: ?$filter=substring(name, 0, 5) eq 'iPhone'

// String search position
const indexQuery = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        function: { type: 'indexof', value: 'Pro' },
        operator: 'gt',
        value: -1
    })
    .toQuery();
// Result: ?$filter=indexof(name, 'Pro') gt -1

Mathematical Functions

// Basic arithmetic
const query = new OdataQueryBuilder<Product>()
    .filter({
        field: 'price',
        function: { type: 'add', operand: 100 },
        operator: 'gt',
        value: 1000
    })
    .toQuery();
// Result: ?$filter=price add 100 gt 1000

// Field references in arithmetic
const fieldRefQuery = new OdataQueryBuilder<Product>()
    .filter({
        field: 'price',
        function: { type: 'mul', operand: { fieldReference: 'taxRate' } },
        operator: 'lt',
        value: 500
    })
    .toQuery();
// Result: ?$filter=price mul taxRate lt 500

// Rounding functions
const roundQuery = new OdataQueryBuilder<Product>()
    .filter({
        field: 'price',
        function: { type: 'round' },
        operator: 'eq',
        value: 100
    })
    .toQuery();
// Result: ?$filter=round(price) eq 100

Date/Time Functions

type Order = {
    id: number;
    createdAt: Date;
    updatedAt: Date;
}

// Extract date parts
const yearQuery = new OdataQueryBuilder<Order>()
    .filter({
        field: 'createdAt',
        function: { type: 'year' },
        operator: 'eq',
        value: 2024
    })
    .toQuery();
// Result: ?$filter=year(createdAt) eq 2024

// Current time comparison
const nowQuery = new OdataQueryBuilder<Order>()
    .filter({
        field: 'updatedAt',
        function: { type: 'now' },
        operator: 'gt',
        value: new Date('2024-01-01')
    })
    .toQuery();
// Result: ?$filter=now() gt 2024-01-01T00:00:00.000Z

// Date extraction
const dateQuery = new OdataQueryBuilder<Order>()
    .filter({
        field: 'createdAt',
        function: { type: 'date', field: { fieldReference: 'createdAt' } },
        operator: 'eq',
        value: '2024-01-15',
        removeQuotes: true
    })
    .toQuery();
// Result: ?$filter=date(createdAt) eq 2024-01-15

Direct Boolean Functions

// Direct boolean function calls (no operator needed)
const containsQuery = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        function: { type: 'contains', value: 'Pro' }
    })
    .toQuery();
// Result: ?$filter=contains(name, 'Pro')

// Boolean function with explicit comparison
const notContainsQuery = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        function: { type: 'contains', value: 'Basic' },
        operator: 'eq',
        value: false
    })
    .toQuery();
// Result: ?$filter=contains(name, 'Basic') eq false

Property Transformations

Transform field values before comparison:

// String transformations
const query = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        operator: 'eq',
        value: 'iphone',
        transform: ['tolower', 'trim']
    })
    .toQuery();
// Result: ?$filter=tolower(trim(name)) eq 'iphone'

// Numeric transformations
const roundQuery = new OdataQueryBuilder<Product>()
    .filter({
        field: 'price',
        operator: 'eq',
        value: 100,
        transform: ['round']
    })
    .toQuery();
// Result: ?$filter=round(price) eq 100

// Date transformations
type Event = { startDate: Date; }
const dateQuery = new OdataQueryBuilder<Event>()
    .filter({
        field: 'startDate',
        operator: 'eq',
        value: 2024,
        transform: ['year']
    })
    .toQuery();
// Result: ?$filter=year(startDate) eq 2024

Array and Lambda Expressions

Filtering String Arrays

type Article = {
    title: string;
    tags: string[];
}

const query = new OdataQueryBuilder<Article>()
    .filter({
        field: 'tags',
        lambdaOperator: 'any',
        expression: {
            field: '',
            operator: 'eq',
            value: 'technology'
        }
    })
    .toQuery();
// Result: ?$filter=tags/any(s: s eq 'technology')

Filtering Object Arrays

type Product = {
    name: string;
    reviews: Array<{
        rating: number;
        comment: string;
        verified: boolean;
    }>;
}

const query = new OdataQueryBuilder<Product>()
    .filter({
        field: 'reviews',
        lambdaOperator: 'any',
        expression: {
            field: 'rating',
            operator: 'gt',
            value: 4
        }
    })
    .toQuery();
// Result: ?$filter=reviews/any(s: s/rating gt 4)

Functions in Lambda Expressions

// String functions in arrays
const lengthQuery = new OdataQueryBuilder<Article>()
    .filter({
        field: 'tags',
        lambdaOperator: 'any',
        expression: {
            field: '',
            function: { type: 'length' },
            operator: 'gt',
            value: 5
        }
    })
    .toQuery();
// Result: ?$filter=tags/any(s: length(s) gt 5)

// Complex nested expressions
const complexQuery = new OdataQueryBuilder<Product>()
    .filter({
        field: 'reviews',
        lambdaOperator: 'all',
        expression: {
            field: 'comment',
            function: { type: 'tolower' },
            operator: 'contains',
            value: 'excellent'
        }
    })
    .toQuery();
// Result: ?$filter=reviews/all(s: contains(tolower(s/comment), 'excellent'))

Nested Lambda Expressions

type Category = {
    name: string;
    products: Array<{
        name: string;
        variants: Array<{
            color: string;
            size: string;
        }>;
    }>;
}

const nestedQuery = new OdataQueryBuilder<Category>()
    .filter({
        field: 'products',
        lambdaOperator: 'any',
        expression: {
            field: 'variants',
            lambdaOperator: 'any',
            expression: {
                field: 'color',
                operator: 'eq',
                value: 'red'
            }
        }
    })
    .toQuery();
// Result: ?$filter=products/any(s: s/variants/any(t: t/color eq 'red'))

Full-Text Search

Simple Search

const query = new OdataQueryBuilder<Product>()
    .search('laptop gaming')
    .toQuery();
// Result: ?$search=laptop%20gaming

Advanced Search with SearchExpressionBuilder

import { SearchExpressionBuilder } from 'odata-builder';

const searchQuery = new OdataQueryBuilder<Product>()
    .search(
        new SearchExpressionBuilder()
            .term('laptop')
            .and()
            .phrase('high performance')
            .or()
            .group(
                new SearchExpressionBuilder()
                    .term('gaming')
                    .and()
                    .not(new SearchExpressionBuilder().term('budget'))
            )
    )
    .toQuery();
// Result: ?$search=laptop%20AND%20%22high%20performance%22%20OR%20(gaming%20AND%20(NOT%20budget))

SearchExpressionBuilder Methods

const builder = new SearchExpressionBuilder()
    .term('laptop')           // Single term: laptop
    .phrase('exact match')    // Quoted phrase: "exact match"  
    .and()                    // Logical AND
    .or()                     // Logical OR
    .not(                     // Logical NOT
        new SearchExpressionBuilder().term('budget')
    )
    .group(                   // Grouping with parentheses
        new SearchExpressionBuilder().term('gaming')
    );

Property Expansion

Basic Expansion

type Order = {
    id: number;
    customer: {
        name: string;
        email: string;
    };
}

const query = new OdataQueryBuilder<Order>()
    .expand('customer')
    .toQuery();
// Result: ?$expand=customer

Nested Property Expansion

type Order = {
    id: number;
    customer: {
        profile: {
            address: {
                city: string;
                country: string;
            };
        };
    };
}

const query = new OdataQueryBuilder<Order>()
    .expand('customer/profile/address')  // Full autocomplete support
    .toQuery();
// Result: ?$expand=customer/profile/address

Type-Safe Function Parameters

Create reusable, type-safe filter functions:

import { FilterFields, FilterOperators } from 'odata-builder';

const createStringFilter = <T>(
    field: FilterFields<T, string>,
    operator: FilterOperators<string>,
    value: string
) => {
    return new OdataQueryBuilder<T>()
        .filter({ field, operator, value })
        .toQuery();
};

// Usage with full type safety and autocomplete
const result = createStringFilter(product, 'contains', 'iPhone');

Complete Example: E-commerce Product Search

type Product = {
    id: number;
    name: string;
    price: number;
    category: string;
    isActive: boolean;
    tags: string[];
    reviews: Array<{
        rating: number;
        comment: string;
        verified: boolean;
    }>;
    createdAt: Date;
}

const complexQuery = new OdataQueryBuilder<Product>()
    // Text search
    .search(
        new SearchExpressionBuilder()
            .term('laptop')
            .and()
            .phrase('high performance')
    )
    // Combined filters
    .filter({
        logic: 'and',
        filters: [
            // Active products only
            { field: 'isActive', operator: 'eq', value: true },
            // Price range
            { field: 'price', operator: 'ge', value: 500 },
            { field: 'price', operator: 'le', value: 2000 },
            // Has gaming tag
            {
                field: 'tags',
                lambdaOperator: 'any',
                expression: {
                    field: '',
                    operator: 'eq',
                    value: 'gaming'
                }
            },
            // Has good reviews
            {
                field: 'reviews',
                lambdaOperator: 'any',
                expression: {
                    logic: 'and',
                    filters: [
                        { field: 'rating', operator: 'gt', value: 4 },
                        { field: 'verified', operator: 'eq', value: true }
                    ]
                }
            },
            // Created this year
            {
                field: 'createdAt',
                function: { type: 'year' },
                operator: 'eq',
                value: 2024
            }
        ]
    })
    // Sorting and pagination
    .orderBy({ field: 'price', orderDirection: 'asc' })
    .orderBy({ field: 'name', orderDirection: 'asc' })
    .top(20)
    .skip(0)
    .select('name', 'price', 'category')
    .count()
    .toQuery();

Supported OData v4 Features

✅ Fully Implemented

  • Operators: eq, ne, gt, ge, lt, le, contains, startswith, endswith, indexof
  • String Functions: concat, contains, endswith, indexof, length, startswith, substring, tolower, toupper, trim
  • Math Functions: add, sub, mul, div, mod, round, floor, ceiling
  • Date Functions: year, month, day, hour, minute, second, now, date, time
  • Lambda Operators: any, all with nested support
  • Logic Operators: and, or with grouping
  • Query Options: $filter, $select, $expand, $orderby, $top, $skip, $count, $search
  • Advanced Features: Field references, property transformations, case-insensitive filtering, GUID support

Type Safety Features

  • Strict TypeScript: Perfect autocomplete for all properties and methods
  • Compile-time Validation: Catch errors before runtime
  • Operator Restrictions: Only valid operators for each field type
  • Deep Object Navigation: Full autocomplete for nested properties
  • Function Parameter Validation: Ensures correct function usage

Migration Guide

Upgrading from v0.7.x to v0.8.0

Version 0.8.0 introduces breaking changes to improve type safety and add OData function support. Here's how to update your code:

1. Lambda Expression Changes

Before (v0.7.x):

// Old syntax with innerProperty
const query = new OdataQueryBuilder<MyType>()
    .filter({
        field: 'items',
        operator: 'contains',
        value: 'test',
        lambdaOperator: 'any',
        innerProperty: 'name',
        ignoreCase: true,
    })
    .toQuery();

After (v0.8.0):

// New syntax with expression object
const query = new OdataQueryBuilder<MyType>()
    .filter({
        field: 'items',
        lambdaOperator: 'any',
        expression: {
            field: 'name',
            operator: 'contains',
            value: 'test',
            ignoreCase: true
        }
    })
    .toQuery();

2. Simple Array Lambda Expressions

Before (v0.7.x):

// Old syntax for simple arrays
const query = new OdataQueryBuilder<MyType>()
    .filter({
        field: 'tags',
        operator: 'contains',
        value: 'important',
        lambdaOperator: 'any',
        ignoreCase: true,
    })
    .toQuery();

After (v0.8.0):

// New syntax - use empty string for field
const query = new OdataQueryBuilder<MyType>()
    .filter({
        field: 'tags',
        lambdaOperator: 'any',
        expression: {
            field: '',  // Empty string for array elements
            operator: 'contains',
            value: 'important',
            ignoreCase: true
        }
    })
    .toQuery();

3. New OData Function Support

Version 0.8.0 adds comprehensive OData function support:

// String functions
const lengthFilter = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        function: { type: 'length' },
        operator: 'gt',
        value: 10
    })
    .toQuery();

// Math functions  
const roundedPrice = new OdataQueryBuilder<Product>()
    .filter({
        field: 'price',
        function: { type: 'round' },
        operator: 'eq',
        value: 100
    })
    .toQuery();

// Date functions
const yearFilter = new OdataQueryBuilder<Order>()
    .filter({
        field: 'createdAt',
        function: { type: 'year' },
        operator: 'eq',
        value: 2024
    })
    .toQuery();

4. Property Transformations (New Feature)

Transform field values before comparison:

// String transformations
const query = new OdataQueryBuilder<Product>()
    .filter({
        field: 'name',
        operator: 'eq',
        value: 'iphone',
        transform: ['tolower', 'trim']  // Chain transformations
    })
    .toQuery();
// Result: ?$filter=tolower(trim(name)) eq 'iphone'

5. Nested Lambda Expressions (New Feature)

// Deep nesting support
const nestedQuery = new OdataQueryBuilder<Category>()
    .filter({
        field: 'products',
        lambdaOperator: 'any',
        expression: {
            field: 'variants',
            lambdaOperator: 'any', 
            expression: {
                field: 'color',
                operator: 'eq',
                value: 'red'
            }
        }
    })
    .toQuery();
// Result: ?$filter=products/any(s: s/variants/any(t: t/color eq 'red'))

Quick Migration Checklist

  1. Update lambda filters: Replace innerProperty with expression: { field: '...' }
  2. Fix array lambdas: Use field: '' for simple array element filtering
  3. Test your queries: All existing functionality should work with the new syntax
  4. Explore new features: Consider using OData functions and property transformations

Need Help?

If you encounter issues during migration:

  • Check the extensive examples in this README
  • Review the TypeScript autocomplete suggestions
  • Open an issue if you need assistance

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.

License

This project is licensed under the MIT License.

Package Sidebar

Install

npm i odata-builder

Weekly Downloads

239

Version

0.8.0

License

MIT

Unpacked Size

74.6 kB

Total Files

7

Last publish

Collaborators

  • nbyx