UNPKG

@lightningkite/lightning-server-simplified

Version:
291 lines (274 loc) 8.44 kB
export type Condition<T> = | { Never: true } | { Always: true } | { And: Array<Condition<T>> } | { Or: Array<Condition<T>> } | { Not: Condition<T> } | { Equal: T } | { NotEqual: T } | { Inside: Array<T> } | { NotInside: Array<T> } | { GreaterThan: T } | { LessThan: T } | { GreaterThanOrEqual: T } | { LessThanOrEqual: T } | { IntBitsClear: number } | { IntBitsSet: number } | { IntBitsAnyClear: number } | { IntBitsAnySet: number } | { Exists: string } | { IfNotNull: Condition<NonNullable<T>> } | { FullTextSearch: { value: string; ignoreCase: boolean; }; } | StringCondition<T> | ArrayCondition<T> | { [P in keyof T]?: Condition<T[P]> } type ArrayCondition<T> = T extends Array<infer E> ? | { ListAllElements: Condition<E> } | { ListAnyElements: Condition<E> } | { ListSizesEquals: number } | { SetAllElements: Condition<E> } | { SetAnyElements: Condition<E> } | { SetSizesEquals: number } : never; type StringCondition<T> = T extends string ? { StringContains: { value: string; ignoreCase: boolean; }; } : never; export function evaluateCondition<T>( condition: Condition<T>, model: T ): boolean { const key = Object.keys(condition)[0]; const value = (condition as any)[key]; switch (key) { case "Never": return false; case "Always": return true; case "And": return (value as Array<Condition<T>>).every((x) => evaluateCondition(x, model) ); case "Or": return (value as Array<Condition<T>>).some((x) => evaluateCondition(x, model) ); case "Not": return !evaluateCondition(value as Condition<T>, model); case "Equal": return model === value; case "NotEqual": return model !== value; case "Inside": return (value as Array<T>).indexOf(model) !== -1; case "NotInside": return (value as Array<T>).indexOf(model) === -1; case "GreaterThan": return model > value; case "LessThan": return model < value; case "GreaterThanOrEqual": return model >= value; case "LessThanOrEqual": return model <= value; case "StringContains": const v = value as { value: string; ignoreCase: boolean; }; if (v.ignoreCase) return (model as unknown as string).toLowerCase().indexOf(v.value) !== -1; else return (model as unknown as string).indexOf(v.value) !== -1; case "FullTextSearch": const v2 = value as { value: string; ignoreCase: boolean; }; if (v2.ignoreCase) return (model as unknown as string).toLowerCase().indexOf(v2.value) !== -1; else return (model as unknown as string).indexOf(v2.value) !== -1; case "IntBitsClear": return ((model as unknown as number) & value) === 0; case "IntBitsSet": return ((model as unknown as number) & value) === value; case "IntBitsAnyClear": return ((model as unknown as number) & value) < value; case "IntBitsAnySet": return ((model as unknown as number) & value) > 0; case "ListAllElements": return (model as unknown as Array<any>).every((x) => evaluateCondition(value as Condition<any>, x) ); case "ListAnyElements": return (model as unknown as Array<any>).some((x) => evaluateCondition(value as Condition<any>, x) ); case "ListSizesEquals": return (model as unknown as Array<any>).length === value; case "SetAllElements": return Array.from(model as unknown as Set<any>).every((x) => evaluateCondition(value as Condition<any>, x) ); case "SetAnyElements": return Array.from(model as unknown as Set<any>).some((x) => evaluateCondition(value as Condition<any>, x) ); case "SetSizesEquals": return (model as unknown as Set<any>).size === value; case "Exists": return true; case "IfNotNull": return ( model !== null && model !== undefined && evaluateCondition(value as Condition<any>, model) ); default: return evaluateCondition(value as Condition<any>, (model as unknown as any)[key]); } } type PathImpl<T, K extends keyof T> = K extends string ? T[K] extends Record<string, any> ? T[K] extends ArrayLike<any> ? K | `${K}.${PathImpl<T[K], Exclude<keyof T[K], keyof any[]>>}` : K | `${K}.${PathImpl<T[K], keyof T[K]>}` : K : never; type PathImpl2<T, K extends keyof T, V> = K extends string ? T[K] extends Record<string, any> ? T[K] extends ArrayLike<any> ? K | `${K}.${PathImpl2<T[K], Exclude<keyof T[K], keyof any[]>, V>}` : K | `${K}.${PathImpl2<T[K], keyof T[K], V>}` : T[K] extends V ? K : never : never; type Path<T> = PathImpl<T, keyof T> | (keyof T & string); export type DataClassProperty<T, V> = keyof { [P in keyof T as T[P] extends V ? P : never]: P; } & keyof T & string; type PathValue<T, P extends Path<T>> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? Rest extends Path<T[K]> ? PathValue<T[K], Rest> : never : never : P extends keyof T ? T[P] : never; type PathWithConditionImpl<T, P extends Path<T>> = `${P}:${keyof ConditionMap< PathValue<T, P> >}`; type PathWithCondition<T> = PathWithConditionImpl<T, Path<T>>; type PathWithConditionValue< T, P extends PathWithCondition<T> > = P extends `${infer K}:${infer ConditionKey}` ? K extends Path<T> ? ConditionKey extends keyof ConditionMap<PathValue<T, K>> ? ConditionMap<PathValue<T, K>>[ConditionKey] : never : never : never; type ConditionMap<T> = { Never?: true; Always?: true; Equal?: T; NotEqual?: T; Inside?: Array<T>; NotInside?: Array<T>; GreaterThan?: T; LessThan?: T; GreaterThanOrEqual?: T; LessThanOrEqual?: T; StringContains?: { value: string; ignoreCase: boolean; }; FullTextSearch?: { value: string; ignoreCase: boolean; }; IntBitsClear?: number; IntBitsSet?: number; IntBitsAnyClear?: number; IntBitsAnySet?: number; ListSizesEquals?: number; SetSizesEquals?: number; Exists?: boolean; }; /** * May God have mercy on your soul if you need to read the definition of this type. * Here's a more reasonable description by example: * Imagine you have type TestType = { a: {b: boolean, c: number}, d: boolean }. * You can put things like this in here: * { "a.c:Equal": 3, "d:Equal": true } */ export type ConditionBuilder<T> = { [P in PathWithCondition<T>]?: PathWithConditionValue<T, P>; } & { ""?: Condition<T> }; export function condition<T, P extends PathWithCondition<T>>( key: P, value: PathWithConditionValue<T, P> ): Condition<T> { const parts = key.split(":"); const path = parts[0]; const comparison = parts[1]; const pathParts = path.split("."); let current: Record<string, any> = {}; current[comparison] = value; for (const part of pathParts.reverse()) { const past = current; current = {}; current[part] = past; } return current as Condition<T>; } export function and<T>( conditionBuilder: ConditionBuilder<T> | Array<Condition<T>> ): Condition<T> { if (Array.isArray(conditionBuilder)) return { And: conditionBuilder }; let subconditions: Array<Condition<T>> = []; for (const key in conditionBuilder) { if (key.length === 0) { subconditions.push(conditionBuilder[""] as Condition<T>); continue; } subconditions.push( condition(key as PathWithCondition<T>, (conditionBuilder as any)[key]) ); } if (subconditions.length == 1) return subconditions[0]; else if (subconditions.length == 0) return { Always: true }; else return { And: subconditions }; } export function or<T>( conditionBuilder: ConditionBuilder<T> | Array<Condition<T>> ): Condition<T> { if (Array.isArray(conditionBuilder)) return { Or: conditionBuilder }; let subconditions: Array<Condition<T>> = []; for (const key in conditionBuilder) { if (key.length === 0) { subconditions.push(conditionBuilder[""] as Condition<T>); continue; } subconditions.push( condition(key as PathWithCondition<T>, (conditionBuilder as any)[key]) ); } if (subconditions.length == 1) return subconditions[0]; else if (subconditions.length == 0) return { Always: true }; else return { Or: subconditions }; }