feat: support filtering JS objects using QueryFilter
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
export * from './query-parser.schemas.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