UNPKG

odata-builder

Version:

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

1,043 lines (1,042 loc) 38 kB
type QueryFilter<T> = StringQueryFilter<T> | StringPredicateQueryFilter<T> | NumberQueryFilter<T> | DateQueryFilter<T> | GuidQueryFilter<T> | BooleanQueryFilter<T> | LambdaFilter<T> | InFilter | NegatedFilter<T> | HasFilter<T>; interface StringQueryFilter<T> extends BaseFilter<T, string> { operator: StringFilterOperators; ignoreCase?: boolean; removeQuotes?: boolean; function?: StringFunctionDefinition<T>; transform?: StringTransform[]; } /** * String filter with boolean predicate function (contains, startswith, endswith) * These functions return boolean, so value is boolean not string * Example: contains(name, 'test') eq true */ interface StringPredicateQueryFilter<T> { field: FilterFields<T, string>; operator: GeneralFilterOperators; value: boolean | null; ignoreCase?: boolean; function: { type: "contains" | "startswith" | "endswith"; value: string | FieldReference<T, string>; }; } interface NumberQueryFilter<T> extends BaseFilter<T, number> { operator: NumberFilterOperators | GeneralFilterOperators; function?: ArithmeticFunctionDefinition<T>; transform?: NumberTransform[]; } interface DateQueryFilter<T> extends BaseFilter<T, Date> { operator: DateFilterOperators | GeneralFilterOperators; function?: DateFunctionDefinition<T>; transform?: DateTransform[]; } interface GuidQueryFilter<T> extends BaseFilter<T, Guid> { operator: GeneralFilterOperators; removeQuotes?: boolean; transform?: GuidTransform[]; } interface BooleanQueryFilter<T> extends BaseFilter<T, boolean> { operator: GeneralFilterOperators; } /** * Filter for membership testing using 'in' operator (OData 4.01) * Example: Name in ('A', 'B', 'C') */ interface InFilter { field: string; operator: "in"; values: InFilterValue[]; } type InFilterValue = string | number | boolean | Date | Guid | null; /** * Negated filter using 'not' operator * Example: not (contains(Name, 'test')) * Example: not (Age gt 18) */ interface NegatedFilter<T> { type: "not"; filter: QueryFilter<T> | CombinedFilter<T>; } /** * Filter for enum flag checking using 'has' operator * Example: Style has Sales.Color'Yellow' * * Note: The value must be a valid OData enum literal, * typically in format Namespace.EnumType'Value' * The library does not validate or modify the enum literal. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars interface HasFilter<_T> { field: string; operator: "has"; value: string; } interface BaseFilter<T, V> { field: FilterFields<T, V>; operator: FilterOperators<V>; value: V | null; } type PrimitiveArrayElementModel<V> = { s: V; }; type LambdaFilter<T> = { [K in ArrayFields<T>]: { field: K; lambdaOperator: "any" | "all"; expression: ArrayElement<T, K> extends object ? QueryExpression<ArrayElement<T, K>> | CombinedQueryExpression<ArrayElement<T, K>> : QueryExpression<PrimitiveArrayElementModel<ArrayElement<T, K>>> | CombinedQueryExpression<PrimitiveArrayElementModel<ArrayElement<T, K>>>; }; }[ArrayFields<T>]; type QueryExpression<T, F extends SupportedFunction<T> | undefined = undefined> = F extends { type: "now"; } ? { field: FilterFields<T, Date>; function: F; operator?: never; value?: never; } : F extends { type: "length"; } ? { field: FilterFields<T, string>; function: F; operator: FilterOperators<number>; value: number | null; } : F extends { type: "indexof"; } ? { field: FilterFields<T, string>; function: F; operator: FilterOperators<number>; value: number | null; } : F extends { type: "substring"; } ? { field: FilterFields<T, string>; function: F; operator: FilterOperators<string>; value: string | null; } : F extends { type: "contains"; } ? { field: FilterFields<T, string>; function: F; operator?: FilterOperators<boolean> | undefined; value?: boolean | null | undefined; } : QueryFilter<T>; type CombinedQueryExpression<T> = { logic: "and" | "or"; filters: Array<QueryExpression<T> | CombinedQueryExpression<T>>; }; type StringFunctionDefinition<T> = { type: "concat"; values: (string | FieldReference<T, string>)[]; } | { type: "contains"; value: string | FieldReference<T, string>; } | { type: "endswith"; value: string | FieldReference<T, string>; } | { type: "indexof"; value: string | FieldReference<T, string>; } | { type: "length"; } | { type: "startswith"; value: string | FieldReference<T, string>; } | { type: "substring"; start: number | FieldReference<T, number>; length?: number | FieldReference<T, number>; }; type ArithmeticOperator = "add" | "sub" | "mul" | "div" | "mod"; type ArithmeticFunctionDefinition<T> = { type: ArithmeticOperator; operand: number | FieldReference<T, number>; }; type DateFunctionDefinition<T> = { type: "now"; } | { type: "date"; field: FieldReference<T, Date>; } | { type: "time"; field: FieldReference<T, Date>; }; type FieldReference<T, V extends string | number | Date | boolean> = { fieldReference: FilterFields<T, V>; }; type SupportedFunction<T> = StringFunctionDefinition<T> | ArithmeticFunctionDefinition<T> | DateFunctionDefinition<T>; type ArrayFields<T> = { [K in keyof T]: T[K] extends Array<unknown> ? K : never; }[keyof T]; type ArrayElement<T, K extends ArrayFields<T>> = T[K] extends (infer U)[] ? U : T[K] extends readonly (infer U)[] ? U : never; type FilterFields<T, VALUETYPE> = { [K in Extract<keyof T, string>]: T[K] extends Record<string, unknown> ? T[K] extends VALUETYPE ? K : `${K}/${NestedFilterFields<T[K], VALUETYPE>}` : T[K] extends VALUETYPE | null | undefined ? K : T[K] extends readonly VALUETYPE[] ? K : T[K] extends readonly Record<string, infer INNERVALUE>[] ? INNERVALUE extends VALUETYPE ? K : never : never; }[Extract<keyof T, string>]; type NestedFilterFieldsHelper<T, VALUETYPE> = T extends Record<string, unknown> ? { [K in keyof T & string]: T[K] extends VALUETYPE | null | undefined ? K : T[K] extends Record<string, unknown> ? `${K}/${NestedFilterFieldsHelper<Exclude<T[K], undefined>, VALUETYPE>}` extends `${infer P}` ? P : never : never; }[keyof T & string] : never; type NestedFilterFields<T, VALUETYPE> = NestedFilterFieldsHelper<T, VALUETYPE>; type LambdaFilterFields<T, VALUETYPE> = { [K in Extract<keyof T, string>]: T[K] extends readonly (infer TYPE)[] ? TYPE extends object // Nur Arrays von Objekten ? { [Key in keyof TYPE]: TYPE[Key] extends VALUETYPE ? Key : never; }[keyof TYPE] // Extrahiere nur Felder mit VALUETYPE : never : never; }[Extract<keyof T, string>]; type GeneralFilterOperators = "eq" | "ne"; type StringFilterOperators = GeneralFilterOperators; type StringTransform = "tolower" | "toupper" | "trim" | "length"; type DateTransform = "year" | "month" | "day" | "hour" | "minute" | "second"; type NumberTransform = "round" | "floor" | "ceiling"; type GuidTransform = "tolower"; type NumberFilterOperators = "ge" | "gt" | "le" | "lt"; type DateFilterOperators = NumberFilterOperators; type DependentFilterOperators<VALUETYPE> = VALUETYPE extends string ? StringFilterOperators : VALUETYPE extends number ? NumberFilterOperators : VALUETYPE extends Date ? DateFilterOperators : never; type FilterOperators<VALUETYPE> = GeneralFilterOperators | DependentFilterOperators<VALUETYPE>; interface CombinedFilter<T> { logic: "and" | "or"; filters: Array<QueryFilter<T> | CombinedFilter<T>>; } type ExpandFields<T, Depth extends number = 5> = Depth extends 0 ? never : { [K in Extract<keyof T, string>]: IsObjectType<NonNullable<T[K]>> extends true ? // Check for empty object HasKeys<NonNullable<T[K]>> extends true ? // If there's at least one key, include `K` and deeper expansions K | (Depth extends 1 ? never : `${K}/${ExpandFields<NonNullable<T[K]>, PrevDepth<Depth>>}`) : never : never; }[Extract<keyof T, string>]; /** * Generates type-safe field paths for $select queries. * * Supports nested property paths using '/' separator (e.g., 'address/city'). * Recursively generates all valid paths up to the specified depth. * Works with both interfaces and inline types. * * @typeParam T - The entity type * @typeParam Depth - Maximum recursion depth (default: 5) * * @example * interface User { * name: string; * address: { city: string; zip: number }; * } * * type Fields = SelectFields<User>; * // 'name' | 'address' | 'address/city' | 'address/zip' */ type SelectFields<T, Depth extends number = 5> = [ Depth ] extends [ never ] ? never : { [K in Extract<keyof T, string>]-?: IsObjectType<NonNullable<T[K]>> extends true ? K | (string extends SelectFields<NonNullable<T[K]>, PrevDepth<Depth>> ? never : `${K}/${SelectFields<NonNullable<T[K]>, PrevDepth<Depth>> & string}`) : K; }[Extract<keyof T, string>]; type Guid = string & { _type: Guid; }; type HasKeys<T> = [ keyof T ] extends [ never ] ? false : true; /** * Helper type to check if a type is an object (not array, not primitive, not function, not Date) * Works correctly with both interfaces and inline types */ type IsObjectType<T> = T extends object ? T extends readonly unknown[] ? false : T extends (...args: unknown[]) => unknown ? false : T extends Date ? false : true : false; type PrevDepth<T extends number> = [ never, // 0 0, // 1 1, // 2 2, // 3 3, // 4 4 ][T]; interface OrderByDescriptor<T> { field: OrderByFields<T>; orderDirection: "asc" | "desc"; } type OrderByFields<T, Depth extends number = 5> = [ Depth ] extends [ never ] ? never : { [K in Extract<keyof T, string>]-?: IsObjectType<NonNullable<T[K]>> extends true ? K | (string extends OrderByFields<NonNullable<T[K]>, PrevDepth<Depth>> ? never : `${K}/${OrderByFields<NonNullable<T[K]>, PrevDepth<Depth>> & string}`) : K; }[Extract<keyof T, string>]; type SearchTerm = string & { __type: "SearchTerm"; }; interface SearchPhrase { phrase: string; } type SearchOperator = "AND" | "OR" | "NOT"; interface SearchGroup { expression: SearchExpression; } type SearchExpressionPart = SearchTerm | SearchPhrase | SearchOperator | SearchGroup; type SearchExpression = readonly SearchExpressionPart[]; /** * Builder for constructing OData $search query expressions. * * Supports terms, phrases, boolean operators (AND, OR, NOT), and grouping. * The builder is immutable - each method returns a new instance. * * @example * // Simple term search * new SearchExpressionBuilder() * .term('coffee') * .toString() // "coffee" * * @example * // Multiple terms with AND * new SearchExpressionBuilder() * .term('coffee') * .and() * .term('shop') * .toString() // "coffee AND shop" * * @example * // Phrase search (exact match) * new SearchExpressionBuilder() * .phrase('coffee shop') * .toString() // "\"coffee shop\"" * * @example * // Complex expression with grouping * const inner = new SearchExpressionBuilder().term('latte').or().term('espresso'); * new SearchExpressionBuilder() * .term('coffee') * .and() * .group(inner) * .toString() // "coffee AND (latte OR espresso)" * * @example * // Using with OdataQueryBuilder * new OdataQueryBuilder<Product>() * .search(new SearchExpressionBuilder().term('coffee').and().term('organic')) * .toQuery() // "?$search=coffee%20AND%20organic" */ declare class SearchExpressionBuilder { private readonly parts; constructor(parts?: SearchExpression); /** * Adds a search term to the expression. * * Terms are single words that will be searched for in the data. * Multiple terms can be combined with and() or or(). * * @param term - The search term (must not be empty or whitespace only) * @returns A new SearchExpressionBuilder with the term added * @throws Error if term is empty or whitespace only * * @example * builder.term('coffee') // Adds "coffee" to search */ term(term: string): SearchExpressionBuilder; /** * Adds an exact phrase to the expression. * * Phrases are enclosed in double quotes and search for exact matches. * Use this when you need to match multiple words in sequence. * * @param phrase - The exact phrase to search for (must not be empty) * @returns A new SearchExpressionBuilder with the phrase added * @throws Error if phrase is empty or whitespace only * * @example * builder.phrase('coffee shop') // Adds "\"coffee shop\"" to search */ phrase(phrase: string): SearchExpressionBuilder; /** * Adds an AND operator between search terms. * * Both terms on either side of AND must match for results to be returned. * * @returns A new SearchExpressionBuilder with AND added * * @example * builder.term('coffee').and().term('organic') // "coffee AND organic" */ and(): SearchExpressionBuilder; /** * Adds an OR operator between search terms. * * Either term on either side of OR can match for results to be returned. * * @returns A new SearchExpressionBuilder with OR added * * @example * builder.term('coffee').or().term('tea') // "coffee OR tea" */ or(): SearchExpressionBuilder; /** * Negates a search expression. * * Results matching the negated expression will be excluded. * * @param expressionBuilder - The expression to negate * @returns A new SearchExpressionBuilder with the negated expression * * @example * const decaf = new SearchExpressionBuilder().term('decaf'); * builder.term('coffee').and().not(decaf) // "coffee AND (NOT decaf)" */ not(expressionBuilder: SearchExpressionBuilder): SearchExpressionBuilder; /** * Groups an expression with parentheses for precedence control. * * Use this to ensure correct evaluation order in complex expressions. * * @param builder - The expression to group * @returns A new SearchExpressionBuilder with the grouped expression * * @example * const options = new SearchExpressionBuilder().term('latte').or().term('espresso'); * builder.term('coffee').and().group(options) // "coffee AND (latte OR espresso)" */ group(builder: SearchExpressionBuilder): SearchExpressionBuilder; /** * Builds and returns the raw search expression array. * * @returns The internal search expression representation */ build(): SearchExpression; /** * Converts the search expression to its OData string representation. * * This is the format used in the $search query parameter. * * @returns The OData search expression string * * @example * new SearchExpressionBuilder() * .term('coffee') * .and() * .phrase('fair trade') * .toString() // 'coffee AND "fair trade"' */ toString(): string; /** * Compares this expression with another for equality. * * @param other - The other SearchExpressionBuilder to compare with * @returns true if both expressions are structurally equal */ equals(other: SearchExpressionBuilder): boolean; private stringifyPart; } // ============================================================================ // FilterExpression - Internal wrapper for filter objects // ============================================================================ interface FilterExpression<T> { readonly _type: "expression"; readonly _filter: QueryFilter<T> | CombinedFilter<T>; } // ============================================================================ // Field Operations - Type-specific operations available on each field type // ============================================================================ /** * Helper type: checks if null is part of T */ type IncludesNull<T> = null extends T ? true : false; /** * Helper type: conditionally adds null to V if AllowNull is true */ type MaybeNull<V, AllowNull extends boolean> = AllowNull extends true ? V | null : V; /** * Base operations available on all field types * AllowNull determines whether eq/ne accept null (based on original field type) */ interface BaseFieldOperations<T, V, AllowNull extends boolean = false> { eq(value: MaybeNull<V, AllowNull>): FilterExpression<T>; ne(value: MaybeNull<V, AllowNull>): FilterExpression<T>; /** * Membership test using 'in' operator (OData 4.01) * @throws Error if values array is empty */ in(values: MaybeNull<V, AllowNull>[]): FilterExpression<T>; } /** * String-specific operations * V preserves literal string types (e.g., 'open' | 'closed') * AllowNull determines whether eq/ne accept null */ interface StringFieldOperations<T, V extends string = string, AllowNull extends boolean = false> extends BaseFieldOperations<T, V, AllowNull> { // Case insensitivity modifier ignoreCase(): StringFieldOperations<T, V, AllowNull>; // String predicates (return boolean - no further comparison needed) contains(value: string): FilterExpression<T>; startswith(value: string): FilterExpression<T>; endswith(value: string): FilterExpression<T>; // String functions returning values (can be chained with comparison) length(): NumberFieldOperations<T>; indexof(value: string): NumberFieldOperations<T>; substring(start: number, length?: number): StringFieldOperations<T, string, AllowNull>; concat(...values: string[]): StringFieldOperations<T, string, AllowNull>; // String transforms tolower(): StringFieldOperations<T, V, AllowNull>; toupper(): StringFieldOperations<T, V, AllowNull>; trim(): StringFieldOperations<T, V, AllowNull>; /** * Enum flag check using 'has' operator * Value must be a valid OData enum literal (e.g., "Sales.Color'Yellow'") * The library passes the value through unchanged - caller is responsible for correct format. */ has(enumLiteral: string): FilterExpression<T>; } /** * Number-specific operations * AllowNull determines whether eq/ne accept null */ interface NumberFieldOperations<T, AllowNull extends boolean = false> extends BaseFieldOperations<T, number, AllowNull> { // Comparison operators gt(value: number): FilterExpression<T>; ge(value: number): FilterExpression<T>; lt(value: number): FilterExpression<T>; le(value: number): FilterExpression<T>; // Arithmetic functions add(operand: number): NumberFieldOperations<T, AllowNull>; sub(operand: number): NumberFieldOperations<T, AllowNull>; mul(operand: number): NumberFieldOperations<T, AllowNull>; div(operand: number): NumberFieldOperations<T, AllowNull>; mod(operand: number): NumberFieldOperations<T, AllowNull>; // Number transforms round(): NumberFieldOperations<T, AllowNull>; floor(): NumberFieldOperations<T, AllowNull>; ceiling(): NumberFieldOperations<T, AllowNull>; } /** * Date-specific operations * AllowNull determines whether eq/ne accept null */ interface DateFieldOperations<T, AllowNull extends boolean = false> extends BaseFieldOperations<T, Date, AllowNull> { // Comparison operators gt(value: Date): FilterExpression<T>; ge(value: Date): FilterExpression<T>; lt(value: Date): FilterExpression<T>; le(value: Date): FilterExpression<T>; // Date extraction transforms (return number for comparison) // Note: extracted values are never null, so AllowNull=false year(): NumberFieldOperations<T>; month(): NumberFieldOperations<T>; day(): NumberFieldOperations<T>; hour(): NumberFieldOperations<T>; minute(): NumberFieldOperations<T>; second(): NumberFieldOperations<T>; } /** * Boolean-specific operations * AllowNull determines whether eq/ne accept null */ interface BooleanFieldOperations<T, AllowNull extends boolean = false> extends BaseFieldOperations<T, boolean, AllowNull> { // Convenience methods isTrue(): FilterExpression<T>; isFalse(): FilterExpression<T>; } /** * Guid-specific operations * AllowNull determines whether eq/ne accept null */ interface GuidFieldOperations<T, AllowNull extends boolean = false> extends BaseFieldOperations<T, Guid, AllowNull> { // Guid modifiers removeQuotes(): GuidFieldOperations<T, AllowNull>; tolower(): GuidFieldOperations<T, AllowNull>; } /** * Array-specific operations (for lambda filters) */ interface ArrayFieldOperations<T, E> { any(predicate: (element: FieldProxy<E>) => FilterExpression<E>): FilterExpression<T>; all(predicate: (element: FieldProxy<E>) => FilterExpression<E>): FilterExpression<T>; } // ============================================================================ // FieldProxy - Maps object properties to their type-specific operations // ============================================================================ /** * Internal type for primitive array element wrapper * When array is string[], number[], etc., we wrap it as { s: T } */ type PrimitiveArrayWrapper<V> = { s: V; }; /** * Determines the appropriate field operations type based on the field's value type * Note: Guid must be checked before string since Guid extends string * TRoot is the root entity type (for FilterExpression return type) * V is the current field value type (after NonNullable) * AllowNull indicates if the original field type included null * * Uses [V] extends [...] pattern to prevent distributive conditional types * This ensures literal unions like 'open' | 'closed' are preserved as-is */ type FieldOperationsFor<TRoot, V, AllowNull extends boolean = false> = [ V ] extends [ Guid ] ? GuidFieldOperations<TRoot, AllowNull> : [ V ] extends [ string ] ? StringFieldOperations<TRoot, V & string, AllowNull> : [ V ] extends [ number ] ? NumberFieldOperations<TRoot, AllowNull> : [ V ] extends [ boolean ] ? BooleanFieldOperations<TRoot, AllowNull> : [ V ] extends [ Date ] ? DateFieldOperations<TRoot, AllowNull> : [ V ] extends [ Array<infer E> ] ? E extends object ? ArrayFieldOperations<TRoot, E> : ArrayFieldOperations<TRoot, PrimitiveArrayWrapper<E>> : [ V ] extends [ object ] ? NestedFieldProxy<TRoot, V> : never; /** * NestedFieldProxy for accessing nested object properties while preserving the root type * TRoot is the root entity type (for FilterExpression return type) * T is the current nested object type (for property access) */ type NestedFieldProxy<TRoot, T> = { [K in keyof T]-?: FieldOperationsFor<TRoot, NonNullable<T[K]>, IncludesNull<T[K]>>; }; /** * The main FieldProxy type - provides type-safe property access with operations * * @example * // For type User = { name: string; age: number; tags: string[]; address: { city: string } } * // FieldProxy<User> provides: * // - x.name -> StringFieldOperations<User, string, false> * // - x.age -> NumberFieldOperations<User, false> * // - x.tags -> ArrayFieldOperations<User, { s: string }> * // - x.address.city -> StringFieldOperations<User, string, false> (Root type preserved!) * // - x.nullableName (string | null) -> StringFieldOperations<User, string, true> (allows eq(null)) */ type FieldProxy<T> = { [K in keyof T]-?: FieldOperationsFor<T, NonNullable<T[K]>, IncludesNull<T[K]>>; }; // ============================================================================ // FilterBuilder Input Types // ============================================================================ /** * Predicate function type for where/and/or methods */ type FilterPredicate<T> = (x: FieldProxy<T>) => FilterExpression<T>; /** * Input type that accepts either a predicate function or another FilterBuilder */ type FilterInput<T> = FilterPredicate<T> | FilterBuilderLike<T>; /** * Interface for FilterBuilder-like objects (allows type-safe composition) */ interface FilterBuilderLike<T> { build(): QueryFilter<T> | CombinedFilter<T> | null; } // ============================================================================ // Internal Types for FilterBuilder // ============================================================================ /** * Internal representation of a filter part in the builder */ interface FilterPart<T> { /** Logic operator connecting this part to the previous one */ logic?: "and" | "or"; /** The actual filter */ filter: QueryFilter<T> | CombinedFilter<T>; } // ============================================================================ // FilterBuilder - Fluent API for building type-safe OData filters // ============================================================================ /** * FilterBuilder provides a fluent, type-safe API for building OData filters. * * @example * // Simple filter * new FilterBuilder<User>() * .where(x => x.name.eq('John')) * .build(); * * @example * // Complex filter with AND/OR * new FilterBuilder<User>() * .where(x => x.name.eq('John')) * .and(x => x.age.gt(18)) * .or(x => x.isAdmin.isTrue()) * .build(); * * @example * // Composing filters * const activeFilter = new FilterBuilder<User>().where(x => x.isActive.isTrue()); * new FilterBuilder<User>() * .where(x => x.name.eq('John')) * .and(activeFilter) * .build(); */ declare class FilterBuilder<T> implements FilterBuilderLike<T> { private readonly parts; constructor(parts?: FilterPart<T>[]); // ======================================================================== // Public API // ======================================================================== /** * Starts a new filter condition. * This is typically the first method called when building a filter. * * @param input - Either a predicate function or another FilterBuilder * @returns A new FilterBuilder with the condition added * * @example * new FilterBuilder<User>().where(x => x.name.eq('John')) */ where(input: FilterInput<T>): FilterBuilder<T>; /** * Adds an AND condition to the filter. * * @param input - Either a predicate function or another FilterBuilder * @returns A new FilterBuilder with the AND condition added * * @example * builder.where(x => x.name.eq('John')).and(x => x.age.gt(18)) */ and(input: FilterInput<T>): FilterBuilder<T>; /** * Adds an OR condition to the filter. * * @param input - Either a predicate function or another FilterBuilder * @returns A new FilterBuilder with the OR condition added * * @example * builder.where(x => x.name.eq('John')).or(x => x.name.eq('Jane')) */ or(input: FilterInput<T>): FilterBuilder<T>; /** * Negates the entire current filter expression. * * @returns A new FilterBuilder with the negated filter * @throws Error if called on an empty builder * * @example * // not (name eq 'John') * builder.where(x => x.name.eq('John')).not() * * @example * // not (name eq 'John' and age gt 18) * builder.where(x => x.name.eq('John')).and(x => x.age.gt(18)).not() */ not(): FilterBuilder<T>; /** * Creates a grouped sub-expression. * Useful for controlling operator precedence. * * @param builder - A FilterBuilder to group * @returns A new FilterBuilder representing the grouped expression * * @example * // (name eq 'John' and age gt 18) or isAdmin eq true * builder * .where(builder.group( * new FilterBuilder<User>() * .where(x => x.name.eq('John')) * .and(x => x.age.gt(18)) * )) * .or(x => x.isAdmin.isTrue()) */ group(builder: FilterBuilder<T>): FilterBuilder<T>; /** * Builds the final filter object. * Returns null if no conditions have been added. * * @returns QueryFilter<T> | CombinedFilter<T> | null */ build(): QueryFilter<T> | CombinedFilter<T> | null; /** * Builds all filters as an array (useful for the existing filter() method). * * @returns Array of QueryFilter<T> | CombinedFilter<T> */ buildArray(): Array<QueryFilter<T> | CombinedFilter<T>>; /** * Checks if the builder has any conditions. */ isEmpty(): boolean; // ======================================================================== // Private Methods // ======================================================================== /** * Resolves a FilterInput to a QueryFilter or CombinedFilter. */ private resolveInput; /** * Type guard for FilterBuilderLike objects. */ private isFilterBuilderLike; /** * Builds the filter tree respecting operator precedence. * AND has higher precedence than OR. */ private buildWithPrecedence; } // ============================================================================ // Factory Function // ============================================================================ /** * Creates a new FilterBuilder instance. * This is a convenience function that can be used instead of `new FilterBuilder<T>()`. * * @example * import { filter } from './filter-builder'; * const f = filter<User>().where(x => x.name.eq('John')); */ declare function filter<T>(): FilterBuilder<T>; /** * Options for OdataQueryBuilder */ interface OdataQueryBuilderOptions { /** * Use legacy 'or' fallback for 'in' operator (for OData 4.0 servers) * Default: false (uses OData 4.01 'in' syntax) * * @example * // OData 4.01 (default): name in ('A', 'B') * // Legacy (4.0): (name eq 'A' or name eq 'B') */ legacyInOperator?: boolean; } /** * Type-safe OData query builder for constructing OData v4.01 query strings. * * Supports all major OData query options: $filter, $select, $expand, $orderby, * $top, $skip, $count, and $search. * * @typeParam T - The entity type to build queries for * * @example * // Basic query with filter and select * const query = new OdataQueryBuilder<User>() * .filter(f => f.where(x => x.isActive.isTrue())) * .select('name', 'email') * .top(10) * .toQuery(); * // "?$filter=isActive eq true&$top=10&$select=name, email" * * @example * // Complex filter with type-safe field access * new OdataQueryBuilder<User>() * .filter(f => f * .where(x => x.name.contains('John')) * .and(x => x.age.gt(18)) * .or(x => x.tags.any(t => t.s.eq('admin'))) * ) * .toQuery(); * * @example * // Using in() operator (OData 4.01) * new OdataQueryBuilder<User>() * .filter(f => f.where(x => x.status.in(['active', 'pending']))) * .toQuery(); * // "?$filter=status in ('active', 'pending')" * * @example * // Legacy mode for OData 4.0 servers * new OdataQueryBuilder<User>({ legacyInOperator: true }) * .filter(f => f.where(x => x.status.in(['active', 'pending']))) * .toQuery(); * // "?$filter=(status eq 'active' or status eq 'pending')" * * @example * // Using has() for enum flags * new OdataQueryBuilder<Product>() * .filter(f => f.where(x => x.color.has("Sales.Color'Yellow'"))) * .toQuery(); * // "?$filter=color has Sales.Color'Yellow'" * * @example * // Negation with not() * new OdataQueryBuilder<User>() * .filter(f => f.where(x => x.name.contains('test')).not()) * .toQuery(); * // "?$filter=not (contains(name, 'test'))" */ declare class OdataQueryBuilder<T> { private queryComponents; private readonly filterContext; /** * Creates a new OdataQueryBuilder instance. * * @param options - Configuration options for query generation */ constructor(options?: OdataQueryBuilderOptions); /** * Limits the number of results returned. * * @param topCount - Maximum number of entities to return (must be positive) * @returns This builder for chaining * @throws Error if topCount is negative * * @example * builder.top(10) // $top=10 */ top(topCount: number): this; /** * Skips a number of results for pagination. * * @param skipCount - Number of entities to skip (must be positive) * @returns This builder for chaining * @throws Error if skipCount is negative * * @example * builder.skip(20).top(10) // $skip=20&$top=10 (page 3) */ skip(skipCount: number): this; /** * Selects specific properties to return (projection). * * Supports nested property paths using '/' separator for type-safe * selection of properties in complex types and navigation properties. * * @param selectProps - Property paths to include in the response * @returns This builder for chaining * @throws Error if any property name is invalid * * @example * // Simple properties * builder.select('name', 'email') // $select=name, email * * @example * // Nested properties with IntelliSense support * builder.select('name', 'address/city', 'address/zip') * // $select=name, address/city, address/zip */ select(...selectProps: SelectFields<Required<T>>[]): this; /** * Adds filter conditions to the query. * * Supports two syntaxes: * 1. Callback syntax with FilterBuilder (recommended for type safety) * 2. Object syntax with raw filter objects * * @param filters - Filter objects or callback function * @returns This builder for chaining * @throws Error if filter is invalid * * @example * // Callback syntax (recommended) * builder.filter(f => f.where(x => x.name.eq('John'))) * * @example * // Object syntax * builder.filter({ field: 'name', operator: 'eq', value: 'John' }) */ filter(...filters: (CombinedFilter<Required<T>> | QueryFilter<Required<T>>)[]): this; /** * Adds filter conditions using FilterBuilder callback syntax. * * @param callback - Function that receives a FilterBuilder and returns it * @returns This builder for chaining */ filter(callback: (f: FilterBuilder<Required<T>>) => FilterBuilder<Required<T>>): this; /** * Expands navigation properties to include related entities. * * @param expandFields - Navigation properties to expand * @returns This builder for chaining * @throws Error if any expand field is invalid * * @example * builder.expand('orders') // $expand=orders * * @example * // Nested expand with select * builder.expand({ orders: { select: ['id', 'price'] } }) */ expand(...expandFields: ExpandFields<T>[]): this; /** * Adds count to the query. * * @param countEntities - If true, returns only the count (/$count). * If false, includes count in response ($count=true). * @returns This builder for chaining * * @example * builder.count() // $count=true (includes count with results) * * @example * builder.count(true) // /$count (returns only the count number) */ count(countEntities?: boolean): this; /** * Orders the results by one or more properties. * * @param orderBy - Order descriptors with field and direction * @returns This builder for chaining * * @example * builder.orderBy({ field: 'name', order: 'asc' }) * * @example * // Multiple sort criteria * builder.orderBy( * { field: 'lastName', order: 'asc' }, * { field: 'firstName', order: 'asc' } * ) */ orderBy(...orderBy: OrderByDescriptor<Required<T>>[]): this; /** * Adds a free-text search to the query. * * Accepts either a simple string or a SearchExpressionBuilder for * complex search expressions with AND, OR, NOT operators. * * @param searchExpression - Search string or SearchExpressionBuilder * @returns This builder for chaining * @throws Error if searchExpression is not a string or SearchExpressionBuilder * * @example * builder.search('coffee') // $search=coffee * * @example * // Complex search with SearchExpressionBuilder * builder.search( * new SearchExpressionBuilder() * .term('coffee') * .and() * .term('organic') * ) // $search=coffee%20AND%20organic */ search(searchExpression: string | SearchExpressionBuilder): this; /** * Builds and returns the OData query string. * * Combines all configured query options into a properly formatted * OData query string ready to append to an endpoint URL. * * @returns The complete OData query string (e.g., "?$filter=...&$top=10") * Returns empty string if no query options are set. * Returns "/$count..." format if count(true) was used. * * @example * new OdataQueryBuilder<User>() * .filter(f => f.where(x => x.isActive.isTrue())) * .top(10) * .toQuery(); * // "?$filter=isActive eq true&$top=10" */ toQuery(): string; private addComponent; } declare const isCombinedFilter: <T>(filters: unknown) => filters is CombinedFilter<T>; declare const isQueryFilter: <T>(filter: unknown) => filter is QueryFilter<T>; export { OdataQueryBuilder, SearchExpressionBuilder, FilterBuilder, filter, isCombinedFilter, isQueryFilter }; export type { FilterExpression, FieldProxy, NestedFieldProxy, FilterPredicate, FilterInput, FilterBuilderLike, OrderByDescriptor, OrderByFields, QueryFilter, FilterFields, FilterOperators, LambdaFilterFields, SearchExpression, SearchTerm, SearchPhrase, CombinedFilter, Guid, ExpandFields };