我正在客户端Angular 8中构建应用程序,在服务器端使用MongoDB 4 / Mongoose 5构建NodeJS 12。我有一个Angular2 query builder模块生成的查询。 Angular查询构建器对象已发送到服务器。
我有一个converts the Angular query object to MongoDB operations的服务器端控制器功能。这对于为诸如RecordID
和RecordType
之类的顶级属性生成查询非常有效。这也适用于构建嵌套和/或条件。
但是,我还需要支持查询子文档数组(示例架构中的“ Items”数组)。
架构
这是我要查询的示例架构:
{
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"
},
]
}
工作示例
仅顶级属性
这是查询生成器输出的示例,仅在顶级属性上具有和/或条件:
{ "condition": "or", "rules": [ { "field": "RecordID", "operator": "=", "value": 1 }, { "condition": "and", "rules": [ { "field": "RecordType", "operator": "=", "value": "Item" } ] } ] }
这是仅在顶级属性上转换为MongoDB操作之后的查询构建器输出:
{ '$expr': { '$or': [ { '$eq': [ '$RecordID', 1 ] }, { '$and': [ { '$eq': [ '$RecordType', 'Item' ] } ] } ] }}
将角度查询对象转换为mongodb运算符。
这是现有的查询转换功能
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);
问题
该功能仅适用于顶级属性,例如RecordID和RecordType,但是我需要扩展它以支持子文档的Items数组。
显然,要查询子文档嵌套数组中的属性,必须基于this related question使用$elemMatch
运算符。但是,就我而言,$ expr是构建嵌套和/或条件所必需的,因此我不能简单地切换到$elemMatch
。
问题
如何扩展查询转换功能以也支持$ elemMatch来查询子文档数组?有没有办法让$ expr工作?
UI查询生成器
这是带有子文档的嵌套“ Items”数组的UI查询构建器。在此示例中,结果应匹配RecordType等于“ Item”和Items.Title等于“ Example Title 1”或Items.Title包含“ Example”。
这是UI查询构建器生成的输出。注意:field
和operator
属性值是可配置的。
{"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"}]}]}
更新:我可能已经找到一种查询格式,该格式也可以与$elemMatch
的嵌套和/或条件一起使用。我必须删除$expr
运算符,因为$elemMatch
在表达式内部不起作用。我从answer to this similar question获得了灵感。
这是正在运行的查询。下一步是让我了解如何调整查询构建器转换功能以创建查询。
{
"$and": [{
"RecordType": {
"$eq": "Item"
}
},
{
"$or": [{
"RecordID": {
"$eq": 1
}
},
{
"Items": {
"$elemMatch": {
"Title": { "$eq": "Example Title 1" }
}
}
}
]
}
]
}
答案 0 :(得分:0)
经过更多研究,我有一个可行的解决方案。感谢所有提供见解的乐于助人的响应者。
该函数从Angular查询构建器模块中获取查询,并将其转换为MongoDB查询。
角度查询生成器
{
"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查询结果
{
"$and": [{
"RecordType": {
"$eq": "Item"
}
}, {
"$or": [{
"Items.Title": {
"$regex": "book",
"$options": "i"
}
}, {
"Project": {
"$in": ["5d0699380a2958e44503acfb", "5d0699380a2958e44503ad2a", "5d0699380a2958e44503ad18"]
}
}]
}]
}
代码
/**
* 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;
}