fix(classes.doc (convertFilterForMongoDb)): Improve filter conversion: handle logical operators, merge operator objects, add nested filter tests and docs, and fix test script

This commit is contained in:
2025-08-18 20:24:16 +00:00
parent 8b37ebc8f9
commit 0dbaa1bc5d
6 changed files with 462 additions and 24 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartdata',
version: '5.16.3',
version: '5.16.4',
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
}

View File

@@ -199,21 +199,51 @@ export const convertFilterForMongoDb = (filterArg: { [key: string]: any }) => {
throw new Error('$where operator is not allowed for security reasons');
}
// Special case: detect MongoDB operators and pass them through directly
const topLevelOperators = ['$and', '$or', '$nor', '$not', '$text', '$regex'];
// Handle logical operators recursively
const logicalOperators = ['$and', '$or', '$nor', '$not'];
const processedFilter: { [key: string]: any } = {};
for (const key of Object.keys(filterArg)) {
if (topLevelOperators.includes(key)) {
return filterArg; // Return the filter as-is for MongoDB operators
if (logicalOperators.includes(key)) {
if (key === '$not') {
processedFilter[key] = convertFilterForMongoDb(filterArg[key]);
} else if (Array.isArray(filterArg[key])) {
processedFilter[key] = filterArg[key].map((subFilter: any) => convertFilterForMongoDb(subFilter));
}
}
}
// If only logical operators, return them
const hasOnlyLogicalOperators = Object.keys(filterArg).every(key => logicalOperators.includes(key));
if (hasOnlyLogicalOperators) {
return processedFilter;
}
// Original conversion logic for non-MongoDB query objects
const convertedFilter: { [key: string]: any } = {};
// Helper to merge operator objects
const mergeIntoConverted = (path: string, value: any) => {
const existing = convertedFilter[path];
if (!existing) {
convertedFilter[path] = value;
} else if (
typeof existing === 'object' && !Array.isArray(existing) &&
typeof value === 'object' && !Array.isArray(value) &&
(Object.keys(existing).some(k => k.startsWith('$')) || Object.keys(value).some(k => k.startsWith('$')))
) {
// Both have operators, merge them
convertedFilter[path] = { ...existing, ...value };
} else {
// Otherwise later wins
convertedFilter[path] = value;
}
};
const convertFilterArgument = (keyPathArg2: string, filterArg2: any) => {
if (Array.isArray(filterArg2)) {
// Arrays are typically used as values for operators like $in or as direct equality matches
convertedFilter[keyPathArg2] = filterArg2;
mergeIntoConverted(keyPathArg2, filterArg2);
return;
} else if (typeof filterArg2 === 'object' && filterArg2 !== null) {
// Check if this is an object with MongoDB operators
@@ -264,8 +294,8 @@ export const convertFilterForMongoDb = (filterArg: { [key: string]: any }) => {
throw new Error('$size operator requires a numeric value');
}
// Pass the operator object through
convertedFilter[keyPathArg2] = filterArg2;
// Use merge helper to handle duplicate paths
mergeIntoConverted(keyPathArg2, filterArg2);
return;
}
@@ -282,13 +312,20 @@ export const convertFilterForMongoDb = (filterArg: { [key: string]: any }) => {
}
} else {
// Primitive values
convertedFilter[keyPathArg2] = filterArg2;
mergeIntoConverted(keyPathArg2, filterArg2);
}
};
for (const key of Object.keys(filterArg)) {
convertFilterArgument(key, filterArg[key]);
// Skip logical operators, they were already processed
if (!logicalOperators.includes(key)) {
convertFilterArgument(key, filterArg[key]);
}
}
// Add back processed logical operators
Object.assign(convertedFilter, processedFilter);
return convertedFilter;
};