
Security News
/Research
Wallet-Draining npm Package Impersonates Nodemailer to Hijack Crypto Transactions
Malicious npm package impersonates Nodemailer and drains wallets by hijacking crypto transactions across multiple blockchains.
odata-builder
Advanced tools
Generate Typesafe OData v4 Queries with Ease. odata-builder ensures your queries are correct as you write them, eliminating worries about incorrect query formats.
Important: This version introduces breaking changes to lambda expressions and function syntax. Please review the Migration Guide below.
Install odata-builder using your preferred package manager:
npm install --save odata-builder
or
yarn add odata-builder
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
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 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'
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
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
const query = new OdataQueryBuilder<Product>()
.filter({
field: 'name',
operator: 'contains',
value: 'apple',
ignoreCase: true
})
.toQuery();
// Result: ?$filter=contains(tolower(name), 'apple')
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'))
// 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
// 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
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 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
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
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')
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)
// 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'))
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'))
const query = new OdataQueryBuilder<Product>()
.search('laptop gaming')
.toQuery();
// Result: ?$search=laptop%20gaming
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))
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')
);
type Order = {
id: number;
customer: {
name: string;
email: string;
};
}
const query = new OdataQueryBuilder<Order>()
.expand('customer')
.toQuery();
// Result: ?$expand=customer
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
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');
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();
eq
, ne
, gt
, ge
, lt
, le
, contains
, startswith
, endswith
, indexof
concat
, contains
, endswith
, indexof
, length
, startswith
, substring
, tolower
, toupper
, trim
add
, sub
, mul
, div
, mod
, round
, floor
, ceiling
year
, month
, day
, hour
, minute
, second
, now
, date
, time
any
, all
with nested supportand
, or
with grouping$filter
, $select
, $expand
, $orderby
, $top
, $skip
, $count
, $search
Version 0.8.0 introduces breaking changes to improve type safety and add OData function support. Here's how to update your code:
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();
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();
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();
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'
// 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'))
innerProperty
with expression: { field: '...' }
field: ''
for simple array element filteringIf you encounter issues during migration:
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.
This project is licensed under the MIT License.
FAQs
odata builder for easier and typesafe usage
The npm package odata-builder receives a total of 84 weekly downloads. As such, odata-builder popularity was classified as not popular.
We found that odata-builder demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
/Research
Malicious npm package impersonates Nodemailer and drains wallets by hijacking crypto transactions across multiple blockchains.
Security News
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
Security News
/Research
Malicious Nx npm versions stole secrets and wallet info using AI CLI tools; Socket’s AI scanner detected the supply chain attack and flagged the malware.