<commit_before>import has from 'lodash/has';
import get from 'lodash/get';
import escapeRegexp from 'escape-string-regexp';
import { ObjectID as objectId } from 'mongodb';

class GQLQueryParser {
  static NODE_TYPES = {
    FIELD: Symbol('field'),
    RELATION: Symbol('relation'),
    LOGICAL_QUERY: Symbol('nestedQuery'),
  };

  constructor(module) {
    this.module = module;
    this.operators = {};
    this.nodeVisitors = [];

    this.configure();
  }

  configure() {
    this.addOperator('not', value => ({ $ne: value }));
    this.addOperator('in', value => ({ $in: value }));
    this.addOperator('notIn', value => ({ $nin: value }));
    this.addOperator(['lt', 'lowerThan'], value => ({ $lt: value }));
    this.addOperator(['lte', 'lowerThanOrEqual'], value => ({ $lte: value }));
    this.addOperator(['gt', 'greaterThan'], value => ({ $gt: value }));
    this.addOperator(['gte', 'greaterThanOrEqual'], value => ({ $gte: value }));
    this.addOperator('between', value => ({ $gt: value[0], $lt: value[1] }));
    this.addOperator(['betweene', 'betweenOrEqual'], value => ({ $gte: value[0], $lte: value[1] }));
    this.addOperator('contains', value => ({
      $regex: `.*${escapeRegexp(value)}.*`,
      $options: 'i',
    }));
    this.addOperator('notContains', value => ({
      $not: {
        $regex: `.*${escapeRegexp(value)}.*`,
        $options: 'i',
      },
    }));
    this.addOperator('regex', value => ({
      $regex: value,
      $options: 'i',
    }));
    this.addOperator('startsWith', value => ({
      $regex: `^${escapeRegexp(value)}.*`,
      $options: 'i',
    }));
    this.addOperator('endsWith', value => ({
      $regex: `.*${escapeRegexp(value)}$`,
      $options: 'i',
    }));

    this.nodeVisitors.push(node => {
      if (
        node.type === this.constructor.NODE_TYPES.FIELD &&
        (node.field === 'id' || node.fieldName === 'id')
      ) {
        Object.assign(node, {
          value: Array.isArray(node.value) ? node.value.map(objectId) : objectId(node.value),
          fieldName: '_id',
        });
      }
    });
  }

  get relations() {
    return this.module.relations;
  }

  addOperator(nameOrNames, transfomer) {
    const names = Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames];
    names.forEach(name => {
      this.operators[name] = transfomer;
    });
  }

  parseQuery(query, collectionName) {
    return Object.keys(query).reduce((acc, field) => {
      const value = query[field];
      const node = {
        collectionName,
        field,
        value,
        operator: null,
        parent: null,
        children: null,
      };

      acc.push(node);

      if (['and', 'or', 'nor'].includes(field.toLowerCase())) {
        node.type = this.constructor.NODE_TYPES.LOGICAL_QUERY;
        node.children = value.map(subQuery => this.parseQuery(subQuery, collectionName));
        return acc;
      }

      if (Object.keys(this.relations[collectionName] || {}).includes(field)) {
        node.type = this.constructor.NODE_TYPES.RELATION;
        node.children = this.parseQuery(
          value,
          this.relations[collectionName][field].collectionName,
        );
        return acc;
      }

      node.type = this.constructor.NODE_TYPES.FIELD;
      const match = field.match(new RegExp(`^(.*)_(${Object.keys(this.operators).join('|')})$`));

      if (match) {
        node.fieldName = match[1];
        node.operator = match[2];
      } else {
        node.fieldName = field;
      }

      return acc;
    }, []);
  }

  branchToMongo(branch, namespace = '') {
    return branch.reduce((acc, node) => {
      if (node.type === this.constructor.NODE_TYPES.FIELD) {
        Object.assign(acc, {
          [`${namespace}${node.fieldName}`]: node.operator
            ? this.operators[node.operator](node.value)
            : node.value,
        });
      }

      if (node.type === this.constructor.NODE_TYPES.LOGICAL_QUERY) {
        Object.assign(acc, {
          [`$${node.field.toLowerCase()}`]: node.children.map(children =>
            this.branchToMongo(children, namespace),
          ),
        });
      }

      if (node.type === this.constructor.NODE_TYPES.RELATION) {
        Object.assign(acc, {
          [node.field]: this.branchToMongo(node.children, ''),
        });
      }

      return acc;
    }, {});
  }

  visitBranch = (branch, nodeVisitors, parent) => {
    branch.forEach(node => {
      if (node.type === this.constructor.NODE_TYPES.FIELD) {
        nodeVisitors.forEach(nodeVisitor => nodeVisitor(node, branch, parent));
      } else if (node.type === this.constructor.NODE_TYPES.LOGICAL_QUERY) {
        node.children.forEach(childNodes => this.visitBranch(childNodes, nodeVisitors, node));
      } else if (node.type === this.constructor.NODE_TYPES.RELATION) {
        this.visitBranch(node.children, nodeVisitors, node);
      }
    });
  };

  toPipeline(
    { where = {}, skip, after, first, last, pivot, orderBy = [] } = {},
    { repository, pipeline = repository.createPipeline(), nodeVisitors = [] },
  ) {
    const tree = this.parseQuery(where, repository.collectionName);

    this.visitBranch(tree, [...this.nodeVisitors, ...nodeVisitors]);

    const matchersBranch = tree.filter(node => node.type !== this.constructor.NODE_TYPES.RELATION);<commit_msg>[oors-mongodb] GQLQueryParser allows node visitors to visit relation and logical query nodes<commit_after>import has from 'lodash/has';
import get from 'lodash/get';
import escapeRegexp from 'escape-string-regexp';
import { ObjectID as objectId } from 'mongodb';

class GQLQueryParser {
  static NODE_TYPES = {
    FIELD: Symbol('field'),
    RELATION: Symbol('relation'),
    LOGICAL_QUERY: Symbol('nestedQuery'),
  };

  constructor(module) {
    this.module = module;
    this.operators = {};
    this.nodeVisitors = [];

    this.configure();
  }

  configure() {
    this.addOperator('not', value => ({ $ne: value }));
    this.addOperator('in', value => ({ $in: value }));
    this.addOperator('notIn', value => ({ $nin: value }));
    this.addOperator(['lt', 'lowerThan'], value => ({ $lt: value }));
    this.addOperator(['lte', 'lowerThanOrEqual'], value => ({ $lte: value }));
    this.addOperator(['gt', 'greaterThan'], value => ({ $gt: value }));
    this.addOperator(['gte', 'greaterThanOrEqual'], value => ({ $gte: value }));
    this.addOperator('between', value => ({ $gt: value[0], $lt: value[1] }));
    this.addOperator(['betweene', 'betweenOrEqual'], value => ({ $gte: value[0], $lte: value[1] }));
    this.addOperator('contains', value => ({
      $regex: `.*${escapeRegexp(value)}.*`,
      $options: 'i',
    }));
    this.addOperator('notContains', value => ({
      $not: {
        $regex: `.*${escapeRegexp(value)}.*`,
        $options: 'i',
      },
    }));
    this.addOperator('regex', value => ({
      $regex: value,
      $options: 'i',
    }));
    this.addOperator('startsWith', value => ({
      $regex: `^${escapeRegexp(value)}.*`,
      $options: 'i',
    }));
    this.addOperator('endsWith', value => ({
      $regex: `.*${escapeRegexp(value)}$`,
      $options: 'i',
    }));

    this.nodeVisitors.push(node => {
      if (
        node.type === this.constructor.NODE_TYPES.FIELD &&
        (node.field === 'id' || node.fieldName === 'id')
      ) {
        Object.assign(node, {
          value: Array.isArray(node.value) ? node.value.map(objectId) : objectId(node.value),
          fieldName: '_id',
        });
      }
    });
  }

  get relations() {
    return this.module.relations;
  }

  addOperator(nameOrNames, transfomer) {
    const names = Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames];
    names.forEach(name => {
      this.operators[name] = transfomer;
    });
  }

  parseQuery(query, collectionName) {
    return Object.keys(query).reduce((acc, field) => {
      const value = query[field];
      const node = {
        collectionName,
        field,
        value,
        operator: null,
        parent: null,
        children: null,
      };

      acc.push(node);

      if (['and', 'or', 'nor'].includes(field.toLowerCase())) {
        node.type = this.constructor.NODE_TYPES.LOGICAL_QUERY;
        node.children = value.map(subQuery => this.parseQuery(subQuery, collectionName));
        return acc;
      }

      if (Object.keys(this.relations[collectionName] || {}).includes(field)) {
        node.type = this.constructor.NODE_TYPES.RELATION;
        node.children = this.parseQuery(
          value,
          this.relations[collectionName][field].collectionName,
        );
        return acc;
      }

      node.type = this.constructor.NODE_TYPES.FIELD;
      const match = field.match(new RegExp(`^(.*)_(${Object.keys(this.operators).join('|')})$`));

      if (match) {
        node.fieldName = match[1];
        node.operator = match[2];
      } else {
        node.fieldName = field;
      }

      return acc;
    }, []);
  }

  branchToMongo(branch, namespace = '') {
    return branch.reduce((acc, node) => {
      if (node.type === this.constructor.NODE_TYPES.FIELD) {
        Object.assign(acc, {
          [`${namespace}${node.fieldName}`]: node.operator
            ? this.operators[node.operator](node.value)
            : node.value,
        });
      }

      if (node.type === this.constructor.NODE_TYPES.LOGICAL_QUERY) {
        Object.assign(acc, {
          [`$${node.field.toLowerCase()}`]: node.children.map(children =>
            this.branchToMongo(children, namespace),
          ),
        });
      }

      if (node.type === this.constructor.NODE_TYPES.RELATION) {
        Object.assign(acc, {
          [node.field]: this.branchToMongo(node.children, ''),
        });
      }

      return acc;
    }, {});
  }

  visitBranch = (branch, nodeVisitors, parent) => {
    branch.forEach(node => {
      nodeVisitors.forEach(nodeVisitor => nodeVisitor(node, branch, parent));

      if (node.type === this.constructor.NODE_TYPES.LOGICAL_QUERY) {
        node.children.forEach(childNodes => this.visitBranch(childNodes, nodeVisitors, node));
      }

      if (node.type === this.constructor.NODE_TYPES.RELATION) {
        this.visitBranch(node.children, nodeVisitors, node);
      }
    });
  };

  toPipeline(
    { where = {}, skip, after, first, last, pivot, orderBy = [] } = {},
    { repository, pipeline = repository.createPipeline(), nodeVisitors = [] },
  ) {
    const tree = this.parseQuery(where, repository.collectionName);

    this.visitBranch(tree, [...this.nodeVisitors, ...nodeVisitors]);

    const matchersBranch = tree.filter(node => node.type !== this.constructor.NODE_TYPES.RELATION);
{'fraction_non_alphanumeric': 0.1793857169775768, 'fraction_numerical': 0.0011305822498586771, 'mean_word_length': 2.6973180076628354, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 53, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1782877', 'n_tokens_mistral': 3333, 'n_tokens_neox': 3344, 'n_words': 565}