mongo filtering via sql tree

This commit is contained in:
Jan Prochazka 2024-08-19 16:25:16 +02:00
parent 9fedfcbb0e
commit 303bd659ad
6 changed files with 129 additions and 7 deletions

View File

@ -67,6 +67,57 @@ const negateCondition = condition => {
};
};
const numberTestCondition = () => value => {
return {
conditionType: 'or',
conditions: [
{
conditionType: 'like',
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value: `.*${value}.*`,
},
},
{
conditionType: 'binary',
operator: '=',
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value,
},
},
],
};
};
const idRegex = /[('"]([0-9a-f]{24})['")]/;
const objectIdTestCondition = () => value => ({
conditionType: 'binary',
operator: '=',
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value: { $oid: value.match(idRegex)[1] },
},
});
const specificPredicateCondition = predicate => () => ({
conditionType: 'specificPredicate',
predicate,
expr: {
exprType: 'placeholder',
},
});
const sqlTemplate = templateSql => {
return {
conditionType: 'rawTemplate',
@ -104,6 +155,8 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
.map(Number)
.desc('number'),
objectid: () => token(P.regexp(/ObjectId\(['"]?[0-9a-f]{24}['"]?\)/)).desc('ObjectId'),
hexstring: () =>
token(P.regexp(/0x(([0-9a-fA-F][0-9a-fA-F])+)/, 1))
.map(x => ({
@ -123,13 +176,22 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
valueTestEq: r => r.value.map(binaryCondition('=')),
hexTestEq: r => r.hexstring.map(binaryCondition('=')),
valueTestStr: r => r.value.map(likeCondition('like', '%#VALUE#%')),
valueTestNum: r => r.number.map(numberTestCondition()),
valueTestObjectId: r => r.objectid.map(objectIdTestCondition()),
notExists: r => r.not.then(r.exists).map(specificPredicateCondition('notExists')),
notEmptyArray: r => r.not.then(r.empty).then(r.array).map(specificPredicateCondition('notEmptyArray')),
emptyArray: r => r.empty.then(r.array).map(specificPredicateCondition('emptyArray')),
exists: () => word('EXISTS').map(specificPredicateCondition('exists')),
comma: () => word(','),
not: () => word('NOT'),
empty: () => word('EMPTY'),
array: () => word('ARRAY'),
notNull: r => r.not.then(r.null).map(unaryCondition('isNotNull')),
null: () => word('NULL').map(unaryCondition('isNull')),
empty: () => word('EMPTY').map(unaryCondition('isEmpty')),
notEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')),
isEmpty: r => r.empty.map(unaryCondition('isEmpty')),
isNotEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')),
true: () => P.regexp(/true/i).map(binaryFixedValueCondition('1')),
false: () => P.regexp(/false/i).map(binaryFixedValueCondition('0')),
trueNum: () => word('1').map(binaryFixedValueCondition('1')),
@ -155,6 +217,7 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
};
const allowedValues = []; // 'string1', 'string2', 'number', 'noQuotedString'];
if (filterBehaviour.allowStringToken) {
allowedValues.push('string1', 'string2', 'noQuotedString');
}
@ -164,6 +227,14 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
const allowedElements = [];
if (filterBehaviour.supportExistsTesting) {
allowedElements.push('exists', 'notExists');
}
if (filterBehaviour.supportArrayTesting) {
allowedElements.push('emptyArray', 'notEmptyArray');
}
if (filterBehaviour.supportNullTesting) {
allowedElements.push('null', 'notNull');
}
@ -181,7 +252,7 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
}
if (filterBehaviour.supportEmpty) {
allowedElements.push('empty', 'notEmpty');
allowedElements.push('isEmpty', 'isNotEmpty');
}
if (filterBehaviour.allowHexString) {
@ -199,6 +270,14 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
}
}
if (filterBehaviour.allowNumberDualTesting) {
allowedElements.push('valueTestNum');
}
if (filterBehaviour.allowObjectIdTesting) {
allowedElements.push('valueTestObjectId');
}
// must be last
if (filterBehaviour.allowStringToken) {
allowedElements.push('valueTestStr');

View File

@ -79,6 +79,11 @@ export interface TestCondition extends UnaryCondition {
conditionType: 'isNull' | 'isNotNull' | 'isEmpty' | 'isNotEmpty';
}
export interface SpecificPredicateCondition extends UnaryCondition {
conditionType: 'specificPredicate';
predicate: string;
}
export interface CompoudCondition {
conditionType: 'and' | 'or';
conditions: Condition[];
@ -135,7 +140,8 @@ export type Condition =
| InCondition
| NotInCondition
| RawTemplateCondition
| AnyColumnPassEvalOnlyCondition;
| AnyColumnPassEvalOnlyCondition
| SpecificPredicateCondition;
export interface Source {
name?: NamedObjectInfo;

View File

@ -48,6 +48,8 @@ export const mongoFilterBehaviour: FilterBehaviour = {
supportExistsTesting: true,
allowStringToken: true,
allowNumberDualTesting: true,
allowObjectIdTesting: true,
};
export const evalFilterBehaviour: FilterBehaviour = {

View File

@ -18,4 +18,6 @@ export interface FilterBehaviour {
allowStringToken?: boolean;
allowNumberToken?: boolean;
allowHexString?: boolean;
allowNumberDualTesting?: boolean;
allowObjectIdTesting?: boolean;
}

View File

@ -270,10 +270,11 @@ const driver = {
return res.databases;
},
async readCollection(pool, options) {
const mongoCondition = convertToMongoCondition(options.condition);
console.log('******************* mongoCondition *****************')
console.log(JSON.stringify(mongoCondition, undefined, 2));
try {
const mongoCondition = convertToMongoCondition(options.condition);
// console.log('******************* mongoCondition *****************');
// console.log(JSON.stringify(mongoCondition, undefined, 2));
const collection = pool.__getDatabase().collection(options.pureName);
if (options.countDocuments) {
const count = await collection.countDocuments(convertObjectId(mongoCondition) || {});

View File

@ -89,6 +89,38 @@ function convertToMongoCondition(filter) {
$options: 'i',
},
};
case 'specificPredicate':
switch (filter.predicate) {
case 'exists':
return {
[convertLeftOperandToMongoColumn(filter.expr)]: {
$exists: true,
},
};
case 'notExists':
return {
[convertLeftOperandToMongoColumn(filter.expr)]: {
$exists: false,
},
};
case 'emptyArray':
return {
[convertLeftOperandToMongoColumn(filter.expr)]: {
$exists: true,
$eq: [],
},
};
case 'notEmptyArray':
return {
[convertLeftOperandToMongoColumn(filter.expr)]: {
$exists: true,
$type: 'array',
$ne: [],
},
};
}
default:
throw new Error(`Unknown condition type ${filter.conditionType}`);
}