将查询构建器条件转换为MongoDB操作,包括子文档的嵌套数组

时间:2019-07-29 18:13:48

标签: node.js mongodb nosql

我正在客户端Angular 8中构建应用程序,在服务器端使用MongoDB 4 / Mongoose 5构建NodeJS 12。我有一个Angular2 query builder模块生成的查询。 Angular查询构建器对象已发送到服务器。

我有一个converts the Angular query object to MongoDB operations的服务器端控制器功能。这对于为诸如RecordIDRecordType之类的顶级属性生成查询非常有效。这也适用于构建嵌套和/或条件。

但是,我还需要支持查询子文档数组(示例架构中的“ 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”。

Angular query builder with nested array of objects

这是UI查询构建器生成的输出。注意:fieldoperator属性值是可配置的。

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

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;

}