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" }
}
}
}
]
}
]
}

Example Title 1and all the other criteria will be applied separately so the don't have to apply to the same array element. Is there any way to determine which way of filtering you're trying to apply ?const conditions = { "and": "$and", "or": "$or" };Like addingelemMatchEx: conditions = { "and": "$and", "or": "$or", "elemMatch": "$elemMatch" };`Itemappear as separaterulesso previous solution works as expected here. Please keep in mind that you can also store a document like this in your database:{ Items: { Title: "Example Title 1" } }so the library itself should have a way to distinguish between arrays and nested docs.Items.Titleand then you need to loop through your structure and accumulate them into single$elemMatch{ '$eq': [ '$RecordID', 1 ] }rather than{ 'RecordID': { '$eq': 1} }or{ 'RecordID': 1 }?