Comparing version 0.0.3 to 0.0.4
@@ -1,6 +0,19 @@ | ||
export type Query<T> = { | ||
[P in keyof T]?: QueryCondition<T[P]>; | ||
} & Operations<T>; | ||
export type QueryCondition<FieldType> = FieldType | Conditions<FieldType> | Operations<QueryCondition<FieldType>>; | ||
export interface Conditions<ValueType> { | ||
export type RegExpOrString<T> = T extends string ? RegExp | T : T; | ||
export type ItemType<T> = T extends ReadonlyArray<infer U> ? T | RegExpOrString<U> : RegExpOrString<T>; | ||
export type FilterCondition<T> = T | ItemType<T> | PureFilterOperators<T> | PureFilterOperators<ItemType<T>> | LogicOperators<FilterCondition<T>>; | ||
export type QueryStatefulCondition<T> = T | ItemType<T> | StatefulFilterOperators<T> | StatefulFilterOperators<ItemType<T>> | LogicOperators<FilterCondition<T>>; | ||
export type Filter<T> = { | ||
[P in keyof T]?: FilterCondition<T[P]>; | ||
} & LogicOperators<T>; | ||
export interface LogicOperators<T> { | ||
$and?: Filter<T>[]; | ||
$or?: Filter<T>[]; | ||
$nor?: Filter<T>[]; | ||
$not?: Filter<T>; | ||
} | ||
export interface ConditionFn { | ||
(valuePath: string, value: any, context: Context): string; | ||
} | ||
export type OperationKeys = keyof LogicOperators<unknown>; | ||
export interface PureFilterOperators<ValueType> { | ||
$eq?: ValueType; | ||
@@ -12,10 +25,2 @@ $ne?: ValueType; | ||
$lte?: ValueType; | ||
$in?: ValueType[]; | ||
$nin?: ValueType[]; | ||
$sub?: ValueType[]; | ||
$nsub?: ValueType[]; | ||
$sup?: ValueType[]; | ||
$nsup?: ValueType[]; | ||
$con?: ValueType; | ||
$ncon?: ValueType; | ||
$exists?: boolean; | ||
@@ -26,30 +31,43 @@ $match?: RegExp | string; | ||
$nbits?: number; | ||
$typeof: string; | ||
$ntypeof: string; | ||
$type: string; | ||
$ntype: string; | ||
} | ||
export type ConditionKeys = keyof Conditions<unknown>; | ||
export interface Operations<T> { | ||
$and?: Query<T>[]; | ||
$or?: Query<T>[]; | ||
$nor?: Query<T>[]; | ||
$not?: Query<T>; | ||
export type PureFilterOperatorsNames = keyof PureFilterOperators<unknown>; | ||
export interface EffectFilterOperators<ValueType> { | ||
$in?: ReadonlyArray<ValueType>; | ||
$nin?: ReadonlyArray<ValueType>; | ||
$sub?: ReadonlyArray<ValueType>; | ||
$nsub?: ReadonlyArray<ValueType>; | ||
$sup?: ReadonlyArray<ValueType>; | ||
$nsup?: ReadonlyArray<ValueType>; | ||
$con?: ValueType; | ||
$ncon?: ValueType; | ||
} | ||
export type OperationKeys = keyof Operations<unknown>; | ||
export type EffectFilterOperatorsNames = keyof EffectFilterOperators<unknown>; | ||
export interface StatefulFilterOperators<ValueType> extends PureFilterOperators<ValueType> { | ||
$unique?: boolean; | ||
} | ||
export declare const stringify: (value: unknown) => string; | ||
export interface Condition { | ||
(valuePath: string, value: any, context: Context): string; | ||
} | ||
declare class Context { | ||
private scopeMap; | ||
private counter; | ||
private values; | ||
private cacheMap; | ||
constructor(scopeMap: Map<string, unknown>); | ||
scope<T>(value: T): string; | ||
register(code: string): string; | ||
toString(): string; | ||
} | ||
export declare const createFilter: <T extends object>(query?: Query<T>) => (data: T) => any; | ||
export type SortType = 'asc' | 'desc' | 1 | -1; | ||
export type Primitive = string | number | boolean | bigint | symbol; | ||
export declare const isPrimitive: (value: unknown) => value is Primitive; | ||
export declare const createFilter: <T extends object | number | string | boolean | bigint | symbol>(filter: Filter<T> | number | string | boolean | bigint | symbol) => ((data: T) => boolean); | ||
export type SortType<T> = T extends number ? 1 | -1 : T extends string ? 'asc' | 'desc' : T extends unknown[] ? { | ||
length?: 1 | -1; | ||
} & { | ||
[Index: number]: SortType<T[number]>; | ||
} : boolean; | ||
export type Sort<T> = T extends object ? { | ||
[K in keyof T]?: T[K] extends object ? Sort<T[K]> : SortType; | ||
} : SortType; | ||
export declare const createSort: <T>(sort: Sort<T>) => (a: T, b: T) => any; | ||
[K in keyof T]?: SortType<T[K]> | boolean; | ||
} : SortType<T> | boolean; | ||
export declare const createSort: <T>(sort?: Sort<T>) => ((a: T, b: T) => number); | ||
export {}; |
@@ -0,1 +1,16 @@ | ||
const pureFilters = { | ||
$eq: (valuePath, value)=>`(${valuePath} === ${value})`, | ||
$ne: (valuePath, value)=>`(${valuePath} !== ${value})`, | ||
$gt: (valuePath, value)=>`(${valuePath} > ${value})`, | ||
$gte: (valuePath, value)=>`(${valuePath} >= ${value})`, | ||
$lt: (valuePath, value)=>`(${valuePath} < ${value})`, | ||
$lte: (valuePath, value)=>`(${valuePath} <= ${value})`, | ||
$exists: (valuePath, value)=>`((${valuePath} !== undefined && ${valuePath} !== null) === ${value})`, | ||
$match: (valuePath, value)=>`${value}.test('' + ${valuePath})`, | ||
$nmatch: (valuePath, value)=>`!${value}.test('' + ${valuePath})`, | ||
$bits: (valuePath, value)=>`((${valuePath} & ${value}) === ${value})`, | ||
$nbits: (valuePath, value)=>`((${valuePath} & ${value}) !== ${value})`, | ||
$type: (valuePath, value)=>`(typeof ${valuePath} === ${value})`, | ||
$ntype: (valuePath, value)=>`(typeof ${valuePath} !== ${value})` | ||
}; | ||
export const stringify = (value)=>{ | ||
@@ -10,33 +25,21 @@ if (value === undefined) { | ||
}; | ||
const conditions = { | ||
$eq: (valuePath, value)=>`(${valuePath} === ${value})`, | ||
$ne: (valuePath, value)=>`(${valuePath} !== ${value})`, | ||
$gt: (valuePath, value)=>`(${valuePath} > ${value})`, | ||
$gte: (valuePath, value)=>`(${valuePath} >= ${value})`, | ||
$lt: (valuePath, value)=>`(${valuePath} < ${value})`, | ||
$lte: (valuePath, value)=>`(${valuePath} <= ${value})`, | ||
const effectFilters = { | ||
$in: (valuePath, value, context)=>{ | ||
const name = context.register(`new Set(${value})`); | ||
return `${name}.has(${valuePath})`; | ||
const scope = context.scope(new Set(value)); | ||
return `${scope}?.has(${valuePath})`; | ||
}, | ||
$nin: (valuePath, value, context)=>`!${conditions.$in(valuePath, value, context)}`, | ||
$nin: (valuePath, value, context)=>`!${effectFilters.$in(valuePath, value, context)}`, | ||
$sub: (valuePath, value, context)=>{ | ||
const name = context.register(`new Set(${value})`); | ||
return `${valuePath}.every((value) => ${name}.has(value))`; | ||
const scope = context.scope(new Set(value)); | ||
return `${valuePath}.every((value) => ${scope}?.has(value))`; | ||
}, | ||
$nsub: (valuePath, value, context)=>`!${conditions.$sub(valuePath, value, context)}`, | ||
$nsub: (valuePath, value, context)=>`!${effectFilters.$sub(valuePath, value, context)}`, | ||
$sup: (valuePath, value, context)=>{ | ||
const scope = context.scope(value); | ||
const name = context.register(`new Set(${valuePath})`); | ||
return `${value}.every((value) => ${name}.has(value))`; | ||
return `${scope}?.every((value) => ${name}.has(value))`; | ||
}, | ||
$nsup: (valuePath, value, context)=>`!${conditions.$sup(valuePath, value, context)}`, | ||
$con: (valuePath, value)=>`${valuePath}.includes(${value})`, | ||
$ncon: (valuePath, value)=>`!${valuePath}.includes(${value})`, | ||
$exists: (valuePath, value)=>`((${valuePath} !== undefined && ${valuePath} !== null) === ${value})`, | ||
$match: (valuePath, value)=>`${value}.test('' + ${valuePath})`, | ||
$nmatch: (valuePath, value)=>`!${value}.test('' + ${valuePath})`, | ||
$bits: (valuePath, value)=>`((${valuePath} & ${value}) !== 0)`, | ||
$nbits: (valuePath, value)=>`((${valuePath} & ${value}) === 0)`, | ||
$typeof: (valuePath, value)=>`(typeof ${valuePath} === ${value})`, | ||
$ntypeof: (valuePath, value)=>`(typeof ${valuePath} !== ${value})` | ||
$nsup: (valuePath, value, context)=>`!${effectFilters.$sup(valuePath, value, context)}`, | ||
$con: (valuePath, value)=>`new Set(${valuePath}).has(${stringify(value)})`, | ||
$ncon: (valuePath, value)=>`!new Set(${valuePath}).has(${stringify(value)})` | ||
}; | ||
@@ -50,5 +53,17 @@ const operations = { | ||
class Context { | ||
counter = 0; | ||
values = []; | ||
cacheMap = new Map(); | ||
scopeMap; | ||
counter; | ||
values; | ||
cacheMap; | ||
constructor(scopeMap){ | ||
this.scopeMap = scopeMap; | ||
this.counter = 0; | ||
this.values = []; | ||
this.cacheMap = new Map(); | ||
} | ||
scope(value) { | ||
const name = `s$$${this.counter++}`; | ||
this.scopeMap.set(name, value); | ||
return `scope.get('${name}')`; | ||
} | ||
register(code) { | ||
@@ -69,5 +84,8 @@ if (!this.cacheMap.has(code)) { | ||
const condition = Object.entries(query).map(([key, value])=>{ | ||
if (key in conditions) { | ||
return conditions[key](valuePath, stringify(value), context); | ||
if (key in pureFilters) { | ||
return pureFilters[key](valuePath, stringify(value), context); | ||
} | ||
if (key in effectFilters) { | ||
return effectFilters[key](valuePath, value, context); | ||
} | ||
if (key in operations) { | ||
@@ -81,3 +99,3 @@ const values = Array.isArray(value) ? value : [ | ||
if (value instanceof RegExp) { | ||
return conditions.$match(valueSubPath, value.toString(), context); | ||
return pureFilters.$match(valueSubPath, stringify(value), context); | ||
} | ||
@@ -87,3 +105,3 @@ if (typeof value === 'object' && value !== null) { | ||
} | ||
return conditions.$eq(valueSubPath, JSON.stringify(value), context); | ||
return pureFilters.$eq(valueSubPath, stringify(value), context); | ||
}); | ||
@@ -94,11 +112,22 @@ if (condition.length > 0) { | ||
} | ||
return 'true'; | ||
return `${!!query}`; | ||
}; | ||
export const createFilter = (query)=>{ | ||
const context = new Context(); | ||
const code = filterCodeGen(query, 'data', context); | ||
const filter = new Function('data', `${context}\nreturn ${code};`); | ||
return (data)=>filter(data); | ||
const PRIMITIVES = new Set([ | ||
'string', | ||
'number', | ||
'boolean', | ||
'bigint', | ||
'symbol' | ||
]); | ||
export const isPrimitive = (value)=>PRIMITIVES.has(typeof value); | ||
export const createFilter = (filter)=>{ | ||
if (isPrimitive(filter)) { | ||
return (data)=>data === filter; | ||
} | ||
const scope = new Map(); | ||
const context = new Context(scope); | ||
const code = filterCodeGen(filter, 'data', context); | ||
const filterFn = new Function('data', 'scope', `${context}\nreturn ${code};`); | ||
return (data)=>filterFn(data, scope); | ||
}; | ||
const SORT_EXPR = /^(asc|desc|1|-1)$/i; | ||
const SORT_MAP = { | ||
@@ -108,13 +137,12 @@ asc: 1, | ||
1: 1, | ||
'-1': -1 | ||
'-1': -1, | ||
true: 1, | ||
false: -1 | ||
}; | ||
const sortCodeGen = (sort, valuePath)=>{ | ||
const sortCodeGen = (sort, valuePathA, valuePathB)=>{ | ||
if (typeof sort === 'object' && sort !== null) { | ||
const condition = Object.entries(sort).map(([key, value])=>{ | ||
const valueSubPathA = `${valuePath[0]}?.[${JSON.stringify(key)}]`; | ||
const valueSubPathB = `${valuePath[1]}?.[${JSON.stringify(key)}]`; | ||
return sortCodeGen(value, [ | ||
valueSubPathA, | ||
valueSubPathB | ||
]); | ||
const valueSubPathA = `${valuePathA}?.[${JSON.stringify(key)}]`; | ||
const valueSubPathB = `${valuePathB}?.[${JSON.stringify(key)}]`; | ||
return sortCodeGen(value, valueSubPathA, valueSubPathB); | ||
}); | ||
@@ -125,19 +153,22 @@ if (condition.length > 0) { | ||
} | ||
if (typeof sort === 'string' && SORT_EXPR.test(sort)) { | ||
return `(${valuePath[0]}?.localeCompare(${valuePath[1]}) * ${SORT_MAP[sort]})`; | ||
if (typeof sort === 'string' && (sort === 'asc' || sort === 'desc')) { | ||
return `(${valuePathA}?.localeCompare(${valuePathB}) * ${SORT_MAP[sort]})`; | ||
} | ||
if (typeof sort === 'number' && SORT_EXPR.test('' + sort)) { | ||
return `((${valuePath[0]} - ${valuePath[1]}) * ${SORT_MAP[sort]})`; | ||
if (typeof sort === 'number' && (sort === 1 || sort === -1)) { | ||
return `((${valuePathA} - ${valuePathB}) * ${SORT_MAP[`${sort}`]})`; | ||
} | ||
if (typeof sort === 'boolean') { | ||
return `(${valuePathA} > ${valuePathB} === ${sort} ? 1 : (${valuePathA} < ${valuePathB} === ${sort} ? -1 : 0)) `; | ||
} | ||
return `0`; | ||
}; | ||
export const createSort = (sort)=>{ | ||
const code = sortCodeGen(sort, [ | ||
'a', | ||
'b' | ||
]); | ||
if (!sort) { | ||
return ()=>0; | ||
} | ||
const code = sortCodeGen(sort, 'a', 'b'); | ||
const sortFn = new Function('a', 'b', `return ${code};`); | ||
return (a, b)=>sortFn(a, b); | ||
return sortFn; | ||
}; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "ajoq", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"description": "", | ||
@@ -31,2 +31,3 @@ "type": "module", | ||
"jest": "^29.7.0", | ||
"mongodb": "^6.11.0", | ||
"prettier": "^3.4.1", | ||
@@ -33,0 +34,0 @@ "ts-node": "^10.9.2", |
# AJOQ | ||
Another Javascript Object Query | ||
Another Javascript Object Query. | ||
[![Coverage Status][codecov-image]][codecov-url] | ||
[![Github Build Status][github-image]][github-url] | ||
[![NPM version][npm-image]][npm-url] | ||
[![Downloads][downloads-image]][npm-url] | ||
[![Snyk][snyk-image]][snyk-url] | ||
Uses mongodb like query structure to execute in function | ||
## Installation | ||
```bash | ||
npm install ajoq | ||
``` | ||
## Usage | ||
### Filter function | ||
```ts | ||
import { createFilter } from "ajoq"; | ||
interface Data { | ||
name: string; | ||
age: number; | ||
// data type | ||
} | ||
const values: Data[] = [{ name: 'John', age: 42 }, { name: 'Jane', age: 18 }]; | ||
const filterFn = createFilter<Data>({ name: 'John', age: { $gte: 21 } }); | ||
const filtered = values.filter(filterFn); // [{ name: 'John', age: 42 }] | ||
``` | ||
### Sort function | ||
```ts | ||
import { createSort } from "ajoq"; | ||
interface Data { | ||
name: string; | ||
age: number; | ||
// data type | ||
} | ||
const values: Data[] = [{ name: 'John', age: 42 }, { name: 'Jane', age: 18 }]; | ||
const sortFn = createSort<Data>({ name: 'asc', age: -1 }); | ||
const sorted = values.toSorted(sortFn); // [{ name: 'John', age: 42 }, { name: 'Jane', age: 18 }] | ||
``` | ||
## Documentation | ||
### Filter | ||
The `Filter` type represents a flexible and expressive way to filter and search data. It supports combining conditions with logical operators, making it suitable for complex query requirements. | ||
#### Logical Operators | ||
- `$and`: Matches if all sub-conditions are true (logical AND). | ||
- `$or`: Matches if at least one sub-condition is true (logical OR). | ||
- `$nor`: Matches if none of the sub-conditions are true (logical NOR). | ||
- `$not`: Matches if the sub-condition is false (logical NOT). | ||
#### Filter Operators | ||
- `$eq`: Matches if the field equals the value. | ||
- `$ne`: Matches if the field does not equal the value. | ||
- `$gt`: Matches if the field is greater than the value. | ||
- `$gte`: Matches if the field is greater than or equal to the value. | ||
- `$lt`: Matches if the field is less than the value. | ||
- `$lte`: Matches if the field is less than or equal to the value. | ||
- `$exists`: Matches if the field exists or does not exist (true or false). | ||
- `$match`: Matches if the field value matches a regex or string. | ||
- `$nmatch`: Matches if the field value does not match a regex or string. | ||
- `$bits`: Matches if any bits from the field value match the specified bitmask. | ||
- `$nbits`: Matches if no bits from the field value match the specified bitmask. | ||
- `$type`: Matches if the field's type matches the specified type. | ||
- `$ntype`: Matches if the field's type does not match the specified type. | ||
- `$in`: Matches if the field value is in the specified array. | ||
- `$nin`: Matches if the field value is not in the specified array. | ||
- `$sub`: Matches if all elements of the field array are in the specified array (subset). | ||
- `$nsub`: Matches if the field array is not a subset of the specified array. | ||
- `$sup`: Matches if the field array contains all elements of the specified array (superset). | ||
- `$nsup`: Matches if the field array does not contain all elements of the specified array. | ||
- `$con`: Matches if the field array contains the specified value. | ||
- `$ncon`: Matches if the field array does not contain the specified value. | ||
## License | ||
License [The MIT License](http://opensource.org/licenses/MIT) | ||
License [The MIT License](./LICENSE) | ||
Copyright (c) 2024 Ivan Zakharchanka | ||
[npm-url]: https://www.npmjs.com/package/ajoq | ||
[downloads-image]: https://img.shields.io/npm/dw/ajoq.svg?maxAge=43200 | ||
[npm-image]: https://img.shields.io/npm/v/ajoq.svg?maxAge=43200 | ||
[github-url]: https://github.com/3axap4eHko/ajoq/actions | ||
[github-image]: https://github.com/3axap4eHko/ajoq/workflows/Build%20Package/badge.svg?branch=master | ||
[snyk-url]: https://snyk.io/test/npm/ajoq/latest | ||
[snyk-image]: https://snyk.io/test/github/3axap4eHko/ajoq/badge.svg?maxAge=43200 |
220
src/index.ts
@@ -1,8 +0,29 @@ | ||
export type Query<T> = { | ||
[P in keyof T]?: QueryCondition<T[P]>; | ||
} & Operations<T>; | ||
export type RegExpOrString<T> = T extends string ? RegExp | T : T; | ||
export type ItemType<T> = T extends ReadonlyArray<infer U> ? T | RegExpOrString<U> : RegExpOrString<T>; | ||
export type FilterCondition<T> = T | ItemType<T> | PureFilterOperators<T> | PureFilterOperators<ItemType<T>> | LogicOperators<FilterCondition<T>>; | ||
export type QueryStatefulCondition<T> = | ||
| T | ||
| ItemType<T> | ||
| StatefulFilterOperators<T> | ||
| StatefulFilterOperators<ItemType<T>> | ||
| LogicOperators<FilterCondition<T>>; | ||
export type QueryCondition<FieldType> = FieldType | Conditions<FieldType> | Operations<QueryCondition<FieldType>>; | ||
export type Filter<T> = { | ||
[P in keyof T]?: FilterCondition<T[P]>; | ||
} & LogicOperators<T>; | ||
export interface Conditions<ValueType> { | ||
export interface LogicOperators<T> { | ||
$and?: Filter<T>[]; | ||
$or?: Filter<T>[]; | ||
$nor?: Filter<T>[]; | ||
$not?: Filter<T>; | ||
} | ||
export interface ConditionFn { | ||
(valuePath: string, value: any, context: Context): string; | ||
} | ||
export type OperationKeys = keyof LogicOperators<unknown>; | ||
export interface PureFilterOperators<ValueType> { | ||
$eq?: ValueType; | ||
@@ -14,10 +35,3 @@ $ne?: ValueType; | ||
$lte?: ValueType; | ||
$in?: ValueType[]; | ||
$nin?: ValueType[]; | ||
$sub?: ValueType[]; | ||
$nsub?: ValueType[]; | ||
$sup?: ValueType[]; | ||
$nsup?: ValueType[]; | ||
$con?: ValueType; | ||
$ncon?: ValueType; | ||
$exists?: boolean; | ||
@@ -28,17 +42,41 @@ $match?: RegExp | string; | ||
$nbits?: number; | ||
$typeof: string; | ||
$ntypeof: string; | ||
$type: string; | ||
$ntype: string; | ||
} | ||
export type ConditionKeys = keyof Conditions<unknown>; | ||
export type PureFilterOperatorsNames = keyof PureFilterOperators<unknown>; | ||
export interface Operations<T> { | ||
$and?: Query<T>[]; | ||
$or?: Query<T>[]; | ||
$nor?: Query<T>[]; | ||
$not?: Query<T>; | ||
const pureFilters: Record<PureFilterOperatorsNames, ConditionFn> = { | ||
$eq: (valuePath: string, value: unknown) => `(${valuePath} === ${value})`, | ||
$ne: (valuePath: string, value: unknown) => `(${valuePath} !== ${value})`, | ||
$gt: (valuePath: string, value: unknown) => `(${valuePath} > ${value})`, | ||
$gte: (valuePath: string, value: unknown) => `(${valuePath} >= ${value})`, | ||
$lt: (valuePath: string, value: unknown) => `(${valuePath} < ${value})`, | ||
$lte: (valuePath: string, value: unknown) => `(${valuePath} <= ${value})`, | ||
$exists: (valuePath: string, value: boolean) => `((${valuePath} !== undefined && ${valuePath} !== null) === ${value})`, | ||
$match: (valuePath: string, value: RegExp) => `${value}.test('' + ${valuePath})`, | ||
$nmatch: (valuePath: string, value: RegExp) => `!${value}.test('' + ${valuePath})`, | ||
$bits: (valuePath: string, value: unknown) => `((${valuePath} & ${value}) === ${value})`, | ||
$nbits: (valuePath: string, value: unknown) => `((${valuePath} & ${value}) !== ${value})`, | ||
$type: (valuePath: string, value: unknown) => `(typeof ${valuePath} === ${value})`, | ||
$ntype: (valuePath: string, value: unknown) => `(typeof ${valuePath} !== ${value})`, | ||
}; | ||
export interface EffectFilterOperators<ValueType> { | ||
$in?: ReadonlyArray<ValueType>; | ||
$nin?: ReadonlyArray<ValueType>; | ||
$sub?: ReadonlyArray<ValueType>; | ||
$nsub?: ReadonlyArray<ValueType>; | ||
$sup?: ReadonlyArray<ValueType>; | ||
$nsup?: ReadonlyArray<ValueType>; | ||
$con?: ValueType; | ||
$ncon?: ValueType; | ||
} | ||
export type OperationKeys = keyof Operations<unknown>; | ||
export type EffectFilterOperatorsNames = keyof EffectFilterOperators<unknown>; | ||
export interface StatefulFilterOperators<ValueType> extends PureFilterOperators<ValueType> { | ||
$unique?: boolean; | ||
} | ||
export const stringify = (value: unknown) => { | ||
@@ -54,40 +92,24 @@ if (value === undefined) { | ||
export interface Condition { | ||
(valuePath: string, value: any, context: Context): string; | ||
} | ||
const conditions: Record<ConditionKeys, Condition> = { | ||
$eq: (valuePath: string, value: unknown) => `(${valuePath} === ${value})`, | ||
$ne: (valuePath: string, value: unknown) => `(${valuePath} !== ${value})`, | ||
$gt: (valuePath: string, value: unknown) => `(${valuePath} > ${value})`, | ||
$gte: (valuePath: string, value: unknown) => `(${valuePath} >= ${value})`, | ||
$lt: (valuePath: string, value: unknown) => `(${valuePath} < ${value})`, | ||
$lte: (valuePath: string, value: unknown) => `(${valuePath} <= ${value})`, | ||
$in: (valuePath: string, value: unknown, context: Context) => { | ||
const name = context.register(`new Set(${value})`); | ||
return `${name}.has(${valuePath})`; | ||
const effectFilters: Record<EffectFilterOperatorsNames, ConditionFn> = { | ||
$in: (valuePath: string, value: unknown[], context: Context) => { | ||
const scope = context.scope(new Set(value)); | ||
return `${scope}?.has(${valuePath})`; | ||
}, | ||
$nin: (valuePath: string, value: unknown, context: Context) => `!${conditions.$in(valuePath, value, context)}`, | ||
$sub: (valuePath: string, value: unknown, context: Context) => { | ||
const name = context.register(`new Set(${value})`); | ||
return `${valuePath}.every((value) => ${name}.has(value))`; | ||
$nin: (valuePath: string, value: unknown[], context: Context) => `!${effectFilters.$in(valuePath, value, context)}`, | ||
$sub: (valuePath: string, value: unknown[], context: Context) => { | ||
const scope = context.scope(new Set(value)); | ||
return `${valuePath}.every((value) => ${scope}?.has(value))`; | ||
}, | ||
$nsub: (valuePath: string, value: unknown, context: Context) => `!${conditions.$sub(valuePath, value, context)}`, | ||
$nsub: (valuePath: string, value: unknown, context: Context) => `!${effectFilters.$sub(valuePath, value, context)}`, | ||
$sup: (valuePath: string, value: unknown, context: Context) => { | ||
const scope = context.scope(value); | ||
const name = context.register(`new Set(${valuePath})`); | ||
return `${value}.every((value) => ${name}.has(value))`; | ||
return `${scope}?.every((value) => ${name}.has(value))`; | ||
}, | ||
$nsup: (valuePath: string, value: unknown, context: Context) => `!${conditions.$sup(valuePath, value, context)}`, | ||
$con: (valuePath: string, value: unknown) => `${valuePath}.includes(${value})`, | ||
$ncon: (valuePath: string, value: unknown) => `!${valuePath}.includes(${value})`, | ||
$exists: (valuePath: string, value: boolean) => `((${valuePath} !== undefined && ${valuePath} !== null) === ${value})`, | ||
$match: (valuePath: string, value: RegExp) => `${value}.test('' + ${valuePath})`, | ||
$nmatch: (valuePath: string, value: RegExp) => `!${value}.test('' + ${valuePath})`, | ||
$bits: (valuePath: string, value: unknown) => `((${valuePath} & ${value}) !== 0)`, | ||
$nbits: (valuePath: string, value: unknown) => `((${valuePath} & ${value}) === 0)`, | ||
$typeof: (valuePath: string, value: unknown) => `(typeof ${valuePath} === ${value})`, | ||
$ntypeof: (valuePath: string, value: unknown) => `(typeof ${valuePath} !== ${value})`, | ||
$nsup: (valuePath: string, value: unknown, context: Context) => `!${effectFilters.$sup(valuePath, value, context)}`, | ||
$con: (valuePath: string, value: unknown) => `new Set(${valuePath}).has(${stringify(value)})`, | ||
$ncon: (valuePath: string, value: unknown) => `!new Set(${valuePath}).has(${stringify(value)})`, | ||
}; | ||
const operations: Record<keyof Operations<unknown>, any> = { | ||
const operations: Record<keyof LogicOperators<unknown>, any> = { | ||
$and: (value: unknown[]) => `(${value.join(' && ')})`, | ||
@@ -104,2 +126,10 @@ $or: (value: unknown[]) => `(${value.join(' || ')})`, | ||
constructor(private scopeMap: Map<string, unknown>) {} | ||
scope<T>(value: T): string { | ||
const name = `s$$${this.counter++}`; | ||
this.scopeMap.set(name, value); | ||
return `scope.get('${name}')`; | ||
} | ||
register(code: string) { | ||
@@ -113,2 +143,3 @@ if (!this.cacheMap.has(code)) { | ||
} | ||
toString() { | ||
@@ -119,8 +150,11 @@ return this.values.join('\n'); | ||
const filterCodeGen = <T extends object>(query: Query<T> | undefined, valuePath: string, context: Context): string => { | ||
const filterCodeGen = <T extends object>(query: Filter<T> | undefined, valuePath: string, context: Context): string => { | ||
if (typeof query === 'object' && query !== null) { | ||
const condition = Object.entries(query).map(([key, value]) => { | ||
if (key in conditions) { | ||
return conditions[key as ConditionKeys](valuePath, stringify(value), context); | ||
if (key in pureFilters) { | ||
return pureFilters[key as PureFilterOperatorsNames](valuePath, stringify(value), context); | ||
} | ||
if (key in effectFilters) { | ||
return effectFilters[key as EffectFilterOperatorsNames](valuePath, value, context); | ||
} | ||
if (key in operations) { | ||
@@ -132,3 +166,3 @@ const values = Array.isArray(value) ? value : [value]; | ||
if (value instanceof RegExp) { | ||
return conditions.$match(valueSubPath, value.toString(), context); | ||
return pureFilters.$match(valueSubPath, stringify(value), context); | ||
} | ||
@@ -138,3 +172,3 @@ if (typeof value === 'object' && value !== null) { | ||
} | ||
return conditions.$eq(valueSubPath, JSON.stringify(value), context); | ||
return pureFilters.$eq(valueSubPath, stringify(value), context); | ||
}); | ||
@@ -145,23 +179,39 @@ if (condition.length > 0) { | ||
} | ||
return 'true'; | ||
return `${!!query}`; | ||
}; | ||
export const createFilter = <T extends object>(query?: Query<T>) => { | ||
const context = new Context(); | ||
const code = filterCodeGen(query, 'data', context); | ||
const filter = new Function('data', `${context}\nreturn ${code};`); | ||
return (data: T) => filter(data); | ||
const PRIMITIVES = new Set(['string', 'number', 'boolean', 'bigint', 'symbol']); | ||
export type Primitive = string | number | boolean | bigint | symbol; | ||
export const isPrimitive = (value: unknown): value is Primitive => PRIMITIVES.has(typeof value); | ||
export const createFilter = <T extends object | number | string | boolean | bigint | symbol>( | ||
filter: Filter<T> | number | string | boolean | bigint | symbol, | ||
): ((data: T) => boolean) => { | ||
if (isPrimitive(filter)) { | ||
return (data: T) => data === filter; | ||
} | ||
const scope = new Map<string, unknown>(); | ||
const context = new Context(scope); | ||
const code = filterCodeGen(filter as object, 'data', context); | ||
const filterFn = new Function('data', 'scope', `${context}\nreturn ${code};`); | ||
return (data: T) => filterFn(data, scope); | ||
}; | ||
export type SortType = 'asc' | 'desc' | 1 | -1; | ||
export type SortType<T> = T extends number | ||
? 1 | -1 | ||
: T extends string | ||
? 'asc' | 'desc' | ||
: T extends unknown[] | ||
? { length?: 1 | -1 } & { [Index: number]: SortType<T[number]> } | ||
: boolean; | ||
export type Sort<T> = T extends object | ||
? { | ||
[K in keyof T]?: T[K] extends object ? Sort<T[K]> : SortType; | ||
[K in keyof T]?: SortType<T[K]> | boolean; | ||
} | ||
: SortType; | ||
: SortType<T> | boolean; | ||
const SORT_EXPR = /^(asc|desc|1|-1)$/i; | ||
const SORT_MAP: Record<SortType, 1 | -1> = { | ||
type SortDirection = 'asc' | 'desc' | 1 | -1 | true | false; | ||
const SORT_MAP: Record<`${SortDirection}`, 1 | -1> = { | ||
asc: 1, | ||
@@ -171,10 +221,12 @@ desc: -1, | ||
'-1': -1, | ||
true: 1, | ||
false: -1, | ||
}; | ||
const sortCodeGen = <T>(sort: Sort<T>, valuePath: [string, string]): string => { | ||
const sortCodeGen = <T>(sort: Sort<T>, valuePathA: string, valuePathB: string): string => { | ||
if (typeof sort === 'object' && sort !== null) { | ||
const condition = Object.entries(sort).map(([key, value]) => { | ||
const valueSubPathA = `${valuePath[0]}?.[${JSON.stringify(key)}]`; | ||
const valueSubPathB = `${valuePath[1]}?.[${JSON.stringify(key)}]`; | ||
return sortCodeGen(value, [valueSubPathA, valueSubPathB]); | ||
const valueSubPathA = `${valuePathA}?.[${JSON.stringify(key)}]`; | ||
const valueSubPathB = `${valuePathB}?.[${JSON.stringify(key)}]`; | ||
return sortCodeGen(value, valueSubPathA, valueSubPathB); | ||
}); | ||
@@ -185,15 +237,21 @@ if (condition.length > 0) { | ||
} | ||
if (typeof sort === 'string' && SORT_EXPR.test(sort)) { | ||
return `(${valuePath[0]}?.localeCompare(${valuePath[1]}) * ${SORT_MAP[sort as SortType]})`; | ||
if (typeof sort === 'string' && (sort === 'asc' || sort === 'desc')) { | ||
return `(${valuePathA}?.localeCompare(${valuePathB}) * ${SORT_MAP[sort as `${SortDirection}`]})`; | ||
} | ||
if (typeof sort === 'number' && SORT_EXPR.test('' + sort)) { | ||
return `((${valuePath[0]} - ${valuePath[1]}) * ${SORT_MAP[sort as SortType]})`; | ||
if (typeof sort === 'number' && (sort === 1 || sort === -1)) { | ||
return `((${valuePathA} - ${valuePathB}) * ${SORT_MAP[`${sort}` as `${SortDirection}`]})`; | ||
} | ||
if (typeof sort === 'boolean') { | ||
return `(${valuePathA} > ${valuePathB} === ${sort} ? 1 : (${valuePathA} < ${valuePathB} === ${sort} ? -1 : 0)) `; | ||
} | ||
return `0`; | ||
}; | ||
export const createSort = <T>(sort: Sort<T>) => { | ||
const code = sortCodeGen(sort, ['a', 'b']); | ||
export const createSort = <T>(sort?: Sort<T>): ((a: T, b: T) => number) => { | ||
if (!sort) { | ||
return () => 0; | ||
} | ||
const code = sortCodeGen(sort, 'a', 'b'); | ||
const sortFn = new Function('a', 'b', `return ${code};`); | ||
return (a: T, b: T) => sortFn(a, b); | ||
return sortFn as (a: T, b: T) => number; | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
64960
713
101
18