feat: support filtering JS objects using QueryFilter
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
export * from './query-parser.schemas.js';
|
export * from './query-parser.schemas.js';
|
||||||
export { QueryParser } from './query-parser.js';
|
export { QueryParser } from './query-parser.js';
|
||||||
|
export * from './utils.filter.js';
|
||||||
|
|||||||
171
packages/query-dsl/src/utils.filter.ts
Normal file
171
packages/query-dsl/src/utils.filter.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import type { QueryCondition, QueryConditionNumber, QueryConditionText, QueryFilter } from './query-parser.schemas.js';
|
||||||
|
|
||||||
|
const getFieldValue = <T extends Record<string, unknown>>(obj: T, field: string[]): unknown => {
|
||||||
|
let current: unknown = obj;
|
||||||
|
|
||||||
|
for (const key of field) {
|
||||||
|
if (current === null || current === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof current !== 'object') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
current = (current as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a SQL LIKE pattern to a RegExp.
|
||||||
|
* Handles % (any characters) and _ (single character) wildcards.
|
||||||
|
*/
|
||||||
|
const likeToRegex = (pattern: string): RegExp => {
|
||||||
|
const escaped = pattern
|
||||||
|
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
||||||
|
.replace(/%/g, '.*') // % matches any characters
|
||||||
|
.replace(/_/g, '.'); // _ matches single character
|
||||||
|
return new RegExp(`^${escaped}$`, 'i');
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyQueryConditionText = <T extends Record<string, unknown>>(
|
||||||
|
obj: T,
|
||||||
|
{ field, conditions }: QueryConditionText,
|
||||||
|
): boolean => {
|
||||||
|
const value = getFieldValue(obj, field);
|
||||||
|
|
||||||
|
if (conditions.equal !== undefined) {
|
||||||
|
if (conditions.equal === null) {
|
||||||
|
if (value !== null && value !== undefined) return false;
|
||||||
|
} else {
|
||||||
|
if (value !== conditions.equal) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.notEqual !== undefined) {
|
||||||
|
if (conditions.notEqual === null) {
|
||||||
|
if (value === null || value === undefined) return false;
|
||||||
|
} else {
|
||||||
|
if (value === conditions.notEqual) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.like !== undefined) {
|
||||||
|
if (typeof value !== 'string') return false;
|
||||||
|
const regex = likeToRegex(conditions.like);
|
||||||
|
if (!regex.test(value)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.notLike !== undefined) {
|
||||||
|
if (typeof value !== 'string') return false;
|
||||||
|
const regex = likeToRegex(conditions.notLike);
|
||||||
|
if (regex.test(value)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.in !== undefined) {
|
||||||
|
if (!conditions.in.includes(value as string)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.notIn !== undefined) {
|
||||||
|
if (conditions.notIn.includes(value as string)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyQueryConditionNumber = <T extends Record<string, unknown>>(
|
||||||
|
obj: T,
|
||||||
|
{ field, conditions }: QueryConditionNumber,
|
||||||
|
): boolean => {
|
||||||
|
const value = getFieldValue(obj, field);
|
||||||
|
|
||||||
|
if (conditions.equals !== undefined) {
|
||||||
|
if (conditions.equals === null) {
|
||||||
|
if (value !== null && value !== undefined) return false;
|
||||||
|
} else {
|
||||||
|
if (value !== conditions.equals) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.notEquals !== undefined) {
|
||||||
|
if (conditions.notEquals === null) {
|
||||||
|
if (value === null || value === undefined) return false;
|
||||||
|
} else {
|
||||||
|
if (value === conditions.notEquals) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.greaterThan !== undefined) {
|
||||||
|
if (typeof value !== 'number' || value <= conditions.greaterThan) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.greaterThanOrEqual !== undefined) {
|
||||||
|
if (typeof value !== 'number' || value < conditions.greaterThanOrEqual) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.lessThan !== undefined) {
|
||||||
|
if (typeof value !== 'number' || value >= conditions.lessThan) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.lessThanOrEqual !== undefined) {
|
||||||
|
if (typeof value !== 'number' || value > conditions.lessThanOrEqual) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.in !== undefined) {
|
||||||
|
if (!conditions.in.includes(value as number)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.notIn !== undefined) {
|
||||||
|
if (conditions.notIn.includes(value as number)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyQueryCondition = <T extends Record<string, unknown>>(obj: T, options: QueryCondition): boolean => {
|
||||||
|
switch (options.type) {
|
||||||
|
case 'text': {
|
||||||
|
return applyQueryConditionText(obj, options);
|
||||||
|
}
|
||||||
|
case 'number': {
|
||||||
|
return applyQueryConditionNumber(obj, options);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unknown filter type`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyQueryFilter = <T extends Record<string, unknown>>(obj: T, filter: QueryFilter): boolean => {
|
||||||
|
if (filter.type === 'operator') {
|
||||||
|
if (filter.conditions.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (filter.operator) {
|
||||||
|
case 'or': {
|
||||||
|
return filter.conditions.some((condition) => applyQueryFilter(obj, condition));
|
||||||
|
}
|
||||||
|
case 'and': {
|
||||||
|
return filter.conditions.every((condition) => applyQueryFilter(obj, condition));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return applyQueryCondition(obj, filter);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFilterFunction = <T extends Record<string, unknown>>(filter: QueryFilter): ((obj: T) => boolean) => {
|
||||||
|
return (obj: T) => applyQueryFilter(obj, filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterObjects = <T extends Record<string, unknown>>(objects: T[], filter: QueryFilter): T[] => {
|
||||||
|
return objects.filter(createFilterFunction(filter));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMatch = <T extends Record<string, unknown>>(input: T, filter: QueryFilter) => {
|
||||||
|
const fn = createFilterFunction(filter);
|
||||||
|
return fn(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { createFilterFunction, filterObjects, isMatch };
|
||||||
Reference in New Issue
Block a user