Package Exports
- odata-builder
 - odata-builder/odata-builder.esm.js
 - odata-builder/odata-builder.js
 
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (odata-builder) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
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.
⚠️ 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-builderor
yarn add odata-builderQuick 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=10Core 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 500Count 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=40Advanced 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-30561e2f888bCase-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 -1Mathematical 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 100Date/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-15Direct 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 falseProperty 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 2024Array 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%20gamingAdvanced 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=customerNested 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/addressType-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,allwith nested support - Logic Operators: 
and,orwith 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
- ✅ Update lambda filters: Replace 
innerPropertywithexpression: { field: '...' } - ✅ Fix array lambdas: Use 
field: ''for simple array element filtering - ✅ Test your queries: All existing functionality should work with the new syntax
 - ✅ 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.