import { flatten } from 'lodash' import { type Table } from '../dyngoose' import { type SimpleTypesOnly, type ContainsType, type Filter, type IntersectsType } from './filters' import { type MagicSearch } from './search' export class Condition { private readonly key: Attr | string private _not = false private filter: Filter constructor(private readonly search: MagicSearch, attributeName: Attr) { this.key = attributeName return this } not(): this { this._not = !this._not return this } /** * Equal * * Works for all data types, including lists and maps. * Will look for 100% equality, when performing a search on a list, the docum * * - If the target attribute of the comparison is of type String, Number, or Binary, then the operator checks for * a absolute match (similar to === in javascript). * - If the target attribute of the comparison is a set ("SS", "NS", or "BS"), then the operator evaluates to true if * it finds every item specified and only the items specified (similar to _.isEqual in javascript). * - If the target attribute of the comparison is a Map, then the operator evaluates to true if the two objects map * recursively (similar to _.isEqual in javascript). */ eq(value: NonNullable): MagicSearch { if (this._not) { this.filter = ['<>', value] } else { this.filter = ['=', value] } return this.finalize() } /** * Less than * * Works for String, Number, or Binary (not a set type) attribute. * Does not work on sets. */ lt(value: AttributeValueType extends any[] ? never : NonNullable): MagicSearch { if (this._not) { this.filter = ['>=', value] } else { this.filter = ['<', value] } return this.finalize() } /** * Less than or equal * * Works for String, Number, or Binary (not a set type) attribute. * Does not work on sets. */ lte(value: AttributeValueType extends any[] ? never : NonNullable): MagicSearch { if (this._not) { this.filter = ['>', value] } else { this.filter = ['<=', value] } return this.finalize() } /** * Greater than * * Works for String, Number, or Binary (not a set type) attribute. * Does not work on sets. */ gt(value: AttributeValueType extends any[] ? never : NonNullable): MagicSearch { if (this._not) { this.filter = ['<=', value] } else { this.filter = ['>', value] } return this.finalize() } /** * Greater than or equal * * Works for String, Number, or Binary (not a set type) attribute. * Does not work on sets. */ gte(value: AttributeValueType extends any[] ? never : NonNullable): MagicSearch { if (this._not) { this.filter = ['<', value] } else { this.filter = ['>=', value] } return this.finalize() } /** * Checks for a prefix. * * Only works for String or Binary fields. * Does not work for numbers or sets. */ beginsWith(value: AttributeValueType extends any[] ? never : Exclude): MagicSearch { this.filter = ['beginsWith', value] return this.finalize() } /** * Checks for a subsequence, or value in a set. * * Condition value can contain only be a String, Number, or Binary (not a set type). * * - If the target attribute of the comparison is of type String, then the operator checks for a substring match. * - If the target attribute of the comparison is of type Binary, then the operator looks for a subsequence of the * target that matches the input. * - If the target attribute of the comparison is a set ("SS", "NS", or "BS"), then the operator evaluates to true if * it finds an exact match with any member of the set. * * When using `.not().contains(value)` this checks for the absence of a subsequence, or absence of a value in a set. */ contains(value: ContainsType): MagicSearch { if (this._not) { this.filter = ['not contains', value as any] } else { this.filter = ['contains', value as any] } return this.finalize() } exists(): MagicSearch { if (this._not) { this.filter = ['not exists'] } else { this.filter = ['exists'] } return this.finalize() } /** * Check if the attribute value matches of one of the given values. * * If the attribute value matches any of the values specified, the expression evaluates to true. * * Can provide up to 100 values to compare against. * * Works for String, Number, or Binary (not a set type) attributes. **Note: Does not work on sets.** */ includes(...values: Array> | Array>>): MagicSearch { if (this._not) { this.filter = ['excludes', flatten(values)] } else { this.filter = ['includes', flatten(values)] } return this.finalize() } /** * Check if the attribute value does NOT match any of the given values. * * This is a utility method, identical as if you did `.not().includes(…)` * * Works for String, Number, or Binary (not a set type) attributes. **Note: Does not work on sets.** */ excludes(...values: Array>): MagicSearch { if (this._not) { this.filter = ['includes', flatten(values)] } else { this.filter = ['excludes', flatten(values)] } return this.finalize() } /** * Checks if a Set contains any of the provided values in a list. * * If any of the values specified are contained in the attribute's Set, the expression evaluates to true. * * Works for StringSet, NumberSet, or BinarySet attributes. */ someOf(...values: Array> | Array>>): MagicSearch { return this.intersects('some', values) } /** * Checks if a Set contains all of the provided values in a list. * * If every one of the values are contained with the attribute's Set, the expression evaluates to true. * * Works for StringSet, NumberSet, or BinarySet attributes. */ allOf(...values: Array> | Array>>): MagicSearch { return this.intersects('all', values) } /** * Greater than or equal to the first (lower) value, and less than or equal to the second (upper) value. * * Works for String, Number, or Binary (not a set type) attribute. * Does not work on sets. */ between( lower: AttributeValueType extends any[] ? never : NonNullable, upper: AttributeValueType extends any[] ? never : NonNullable, ): MagicSearch { this.filter = ['between', lower, upper] return this.finalize() } /** * The attribute does not exist. * * NULL is supported for all data types, including lists and maps. */ null(): MagicSearch { if (this._not) { this.filter = ['not null'] } else { this.filter = ['null'] } return this.finalize() } private intersects(allOrSome: 'all' | 'some', values: Array> | Array>>): MagicSearch { const key = this.key as any const filters: any[] = [] // any is because of the Exclude on beginsWith const operator: 'contains' | 'not contains' = this._not ? 'not contains' : 'contains' const options = flatten(values) for (let i = 0; i < options.length; i++) { const value = options[i] const filter: Filter = [operator, value] filters.push({ [key]: filter }) if (allOrSome === 'some' && i !== options.length - 1) { filters.push('OR') } } if (allOrSome === 'some') { this.search.parenthesis(group => group.addFilterGroup(filters)) } else { this.search.addFilterGroup(filters) } return this.search } private finalize(): MagicSearch { const key = this.key as any this.search.addFilterGroup([ { [key]: this.filter, } as any, // any is because of the Exclude on beginsWith ]) return this.search } }