type Primitive = string | number | Date | boolean export class ODataFilter { private filterParts: Array = [] private getFormattedValue(value: Primitive | unknown): string { if (typeof value === 'string') { return `'${value.replace(/'/g, '%27%27')}'` } else if (value instanceof Date) { return `datetimeoffset'${value.toISOString()}'` } else if (typeof value === 'undefined' || value === null) { return 'null' } else { return value.toString() } } private addCondition(condition: string) { this.filterParts.push(condition) } eq(field: K, value: T[K]): ODataFilter { this.addCondition(`${String(field)} eq ${this.getFormattedValue(value)}`) return this } ne(field: K, value: T[K]): ODataFilter { this.addCondition(`${String(field)} ne ${this.getFormattedValue(value)}`) return this } gt(field: K, value: T[K] | Date): ODataFilter { this.addCondition(`${String(field)} gt ${this.getFormattedValue(value)}`) return this } ge(field: K, value: T[K] | Date): ODataFilter { this.addCondition(`${String(field)} ge ${this.getFormattedValue(value)}`) return this } lt(field: K, value: T[K] | Date): ODataFilter { this.addCondition(`${String(field)} lt ${this.getFormattedValue(value)}`) return this } le(field: K, value: T[K] | Date): ODataFilter { this.addCondition(`${String(field)} le ${this.getFormattedValue(value)}`) return this } in(field: K, values: Array): ODataFilter { const formattedValues = values.map((v) => this.getFormattedValue(v)).join(',') this.addCondition(`${String(field)} in (${formattedValues})`) return this } startsWith(field: K, value: string): ODataFilter { this.addCondition(`startswith(${String(field)}, ${this.getFormattedValue(value)})`) return this } endsWith(field: K, value: string): ODataFilter { this.addCondition(`endswith(${String(field)}, ${this.getFormattedValue(value)})`) return this } contains(field: K, value: string): ODataFilter { this.addCondition(`contains(${String(field)}, ${this.getFormattedValue(value)})`) return this } includes(field: K, value: string): ODataFilter { this.addCondition(`substringof(${this.getFormattedValue(value)}, ${String(field)}) eq true`) return this } notIncludes(field: K, value: string): ODataFilter { this.addCondition(`substringof(${this.getFormattedValue(value)}, ${String(field)}) eq false`) return this } and(filterBuilder: (filter: ODataFilter) => ODataFilter): ODataFilter { const newFilter = filterBuilder(new ODataFilter()) if (newFilter.filterParts.length > 0) { if (this.filterParts.length > 0) { this.addCondition(`${this.toString()} and (${newFilter})`) } else { this.addCondition(`(${newFilter})`) } this.filterParts = [this.filterParts.pop()!] // keep only the latest compound condition } return this } or(filterBuilder: (filter: ODataFilter) => ODataFilter): ODataFilter { const newFilter = filterBuilder(new ODataFilter()) if (newFilter.filterParts.length > 0) { if (this.filterParts.length > 0) { this.addCondition(`${this.toString()} or (${newFilter})`) } else { this.addCondition(`(${newFilter})`) } this.filterParts = [this.filterParts.pop()!] // keep only the latest compound condition } return this } toString(): string { return this.filterParts.join(' and ') } } export const filter = (): ODataFilter => new ODataFilter()