I am building an application in Angular 8 on the client side and NodeJS 12 with MongoDB 4 / Mongoose 5 on the server side. I have a query generated by the Angular2 query builder module. The Angular query builder object is sent to the server.
I have a server-side controller function that converts the Angular query object to MongoDB operations . This is working perfectly for generating queries for top-level properties such as RecordID
and RecordType
. This is also working for building nested and/or conditions.
However, I need to also support querying an array of subdocuments (the "Items" array in the example schema).
Schema
Here is the example schema I am trying to query:
{
RecordID: 123,
RecordType: "Item",
Items: [
{
Title: "Example Title 1",
Description: "A description 1"
},
{
Title: "Example 2",
Description: "A description 2"
},
{
Title: "A title 3",
Description: "A description 3"
},
]
}
Working example
Top-level properties only
Here's an example of the query builder output with and/or conditions on top-level properties only:
{ "condition": "or", "rules": [ { "field": "RecordID", "operator": "=", "value": 1 }, { "condition": "and", "rules": [ { "field": "RecordType", "operator": "=", "value": "Item" } ] } ] }
Here's the query builder output after it has been converted to MongoDB operations on top-level properties only:
{ '$expr': { '$or': [ { '$eq': [ '$RecordID', 1 ] }, { '$and': [ { '$eq': [ '$RecordType', 'Item' ] } ] } ] }}
that converts the angular query object to mongodb operators.
Here is the existing query conversion function that
const conditions = { "and": "$and", "or": "$or" };
const operators = { "=": "$eq", "!=": "$ne", "<": "$lt", "<=": "$lte", ">": "$gt", ">=": "$gte" };
const mapRule = rule => ({
[operators[rule.operator]]: [ "$"+rule.field, rule.value ]
});
const mapRuleSet = ruleSet => {
return {
[conditions[ruleSet.condition]]: ruleSet.rules.map(
rule => rule.operator ? mapRule(rule) : mapRuleSet(rule)
)
}
};
let mongoDbQuery = { $expr: mapRuleSet(q) };
console.log(mongoDbQuery);
Issue
The function works for top-level properties only such as RecordID and RecordType, but I need to extend it to support the Items array of subdocuments .
Apparently, to query properties in nested arrays of subdocuments, the $elemMatch
operator must be used, based on this related question . However, in my case, the $expr is necessary to build the nested and/or conditions so I can't simply switch to $elemMatch
.
QUESTION
How can I extend the query conversion function to also support $elemMatch to query arrays of subdocuments? Is there a way to get the $expr to work?
UI query builder
Here is the UI query builder with the nested "Items" array of subdocuments. In this example, the results should match RecordType equals "Item" AND Items.Title equals "Example Title 1" OR Items.Title contains "Example".
Here is the output generated by the UI query builder. Note: The field
and operator
property values are configurable.
{"condition":"and","rules":[{"field":"RecordType","operator":"=","value":"Item"},{"condition":"or","rules":[{"field":"Items.Title","operator":"=","value":"Example Title 1"},{"field":"Items.Title","operator":"contains","value":"Example"}]}]}
UPDATE: I may have found a query format that works with the nested and/or conditions with the $elemMatch
as well. I had to remove the $expr
operator since $elemMatch
does not work inside of expressions. I took inspiration from the answer to this similar question .
This is the query that is working. The next step will be for me to figure out how to adjust the query builder conversion function to create the query.
{
"$and": [{
"RecordType": {
"$eq": "Item"
}
},
{
"$or": [{
"RecordID": {
"$eq": 1
}
},
{
"Items": {
"$elemMatch": {
"Title": { "$eq": "Example Title 1" }
}
}
}
]
}
]
}
After more research I have a working solution. Thanks to all of the helpful responders who provided insight.
The function takes a query from the Angular query builder module and converts it to a MongoDB query.
Angular query builder
{
"condition": "and",
"rules": [{
"field": "RecordType",
"operator": "=",
"value": "Item"
}, {
"condition": "or",
"rules": [{
"field": "Items.Title",
"operator": "contains",
"value": "book"
}, {
"field": "Project",
"operator": "in",
"value": ["5d0699380a2958e44503acfb", "5d0699380a2958e44503ad2a", "5d0699380a2958e44503ad18"]
}]
}]
}
MongoDB query result
{
"$and": [{
"RecordType": {
"$eq": "Item"
}
}, {
"$or": [{
"Items.Title": {
"$regex": "book",
"$options": "i"
}
}, {
"Project": {
"$in": ["5d0699380a2958e44503acfb", "5d0699380a2958e44503ad2a", "5d0699380a2958e44503ad18"]
}
}]
}]
}
Code
/**
* Convert a query object generated by UI to MongoDB query
* @param query a query builder object generated by Angular2QueryBuilder module
* @param model the model for the schema to query
* return a MongoDB query
*
*/
apiCtrl.convertQuery = async (query, model) => {
if (!query || !model) {
return {};
}
const conditions = { "and": "$and", "or": "$or" };
const operators = {
"=": "$eq",
"!=": "$ne",
"<": "$lt",
"<=": "$lte",
">": "$gt",
">=": "gte",
"in": "$in",
"not in": "$nin",
"contains": "$regex"
};
// Get Mongoose schema type instance of a field
const getSchemaType = (field) => {
return model.schema.paths[field] ? model.schema.paths[field].instance : false;
}
// Map each rule to a MongoDB query
const mapRule = (rule) => {
let field = rule.field;
let value = rule.value;
if (!value) {
value = null;
}
// Get schema type of current field
const schemaType = getSchemaType(rule.field);
// Check if schema type of current field is ObjectId
if (schemaType === 'ObjectID' && value) {
// Convert string value to MongoDB ObjectId
if (Array.isArray(value)) {
value.map(val => new ObjectId(val));
} else {
value = new ObjectId(value);
}
// Check if schema type of current field is Date
} else if (schemaType === 'Date' && value) {
// Convert string value to ISO date
console.log(value);
value = new Date(value);
}
console.log(schemaType);
console.log(value);
// Set operator
const operator = operators[rule.operator] ? operators[rule.operator] : '$eq';
// Create a MongoDB query
let mongoDBQuery;
// Check if operator is $regex
if (operator === '$regex') {
// Set case insensitive option
mongoDBQuery = {
[field]: {
[operator]: value,
'$options': 'i'
}
};
} else {
mongoDBQuery = { [field]: { [operator]: value } };
}
return mongoDBQuery;
}
const mapRuleSet = (ruleSet) => {
if (ruleSet.rules.length < 1) {
return;
}
// Iterate Rule Set conditions recursively to build database query
return {
[conditions[ruleSet.condition]]: ruleSet.rules.map(
rule => rule.operator ? mapRule(rule) : mapRuleSet(rule)
)
}
};
let mongoDbQuery = mapRuleSet(query);
return mongoDbQuery;
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.