elastic-query-parser

1.3.4 • Public • Published

[BETA] ELASTIC-QUERY-PARSER

Parse query to elasticsearch query DSL

Usage

With elasticsearch schema

const OrderElasticSchema = {
  type : 'object',
  properties : {
    shop_id  : { type : 'long' },
    id       : { type : 'long' },
    customer : {
      type : 'object',
      properties : {
        id    : { type : 'long'   },
        email : { type : 'string' },
        name  : { type : 'string' },
        phone : { type : 'string' }
      }
    },
    line_items : {
      type : 'nested',
      properties : {
        id       : { type : 'long'   },
        barcode  : { type : 'string' },
        quantity : { type : 'long'   }
      }
    },
    created_at    : { type : 'date'    },
    updated_at    : { type : 'date'    },
    status        : { type : 'string'  },
    private_field : { type : 'long'    },
    order_number  : { type : 'string'  },
    location_id   : { type : 'integer' },
    is_deleted    : { type : 'boolean' }
  }
};

You can create a parser

const parse = Parser({
  schema    : OrderElasticSchema,
  required  : ['shop_id'],
  blackList : ['private_field'],
  whiteList : ['*'],
  alias     : {
    barcode : 'line_items.barcode'
  },
  defaults  : {
    page       : 1,
    limit      : 20,
    sort       : 'created_at_asc',
    is_deleted : false
  },
  deniedValues : ['', null, undefined],
  custom : {
    keyword : (value) => { return { "query" : { "must" : [{ "match" : {
      "fields" : ["customer.name", "customer.phone", "customer.email"],
      "query"  : value
  }}]}} 
  }}
});

That parse query object to elasticsearch filter

it ('should parse query to elasticsearch query DSL successfully', async () => {
 
  let query = {
    'shop_id'              : '100000001',
    'created_at_gte'       : '2019-04-01T03:15:00.000Z',
    'created_at_lte'       : '2019-04-30T03:15:00.000Z',
    'updated_at_from_date' : '2019-04-01T03:15:00.000Z',
    'updated_at_to_date'   : '2019-04-30T03:15:00.000Z',
    'customer.id'          : '10000',
    'customer.name_like'   : 'hoang',
    'barcode'              : 'HEO',
    'status_ne'            : 'NEW',
    'location_id_in'       : '1000,2000',
    'keyword'              : '0969728159',
    // pagination
    'page'                 : '2', 
    'limit'                : '20',
    'sort'                 : 'created_at_asc,id_desc',
    'fields'               : '-customer',
  };
 
  let { errors, page, queryDSL, fields, skip, limit, sort } = parse(query);
 
  let expectedQueryDSL = {
    "query": {
      "bool": {
        "must" : [
          {
            "query_string": {
              "fields": ["customer.name"],
              "query": "hoang",
              "minimum_should_match": "100%",
              "analyzer": "search_whitespace"
            }
          },
          {
            "match": {
              "fields": ["customer.name", "customer.phone", "customer.email"],
              "query": "0969728159"
            }
          }
        ],
        "filter": {
          "bool": {
            "must": [
              {
                "term": {
                  "is_deleted": false
                }
              },
              {
                "term": {
                  "shop_id": 100000001
                }
              },
              {
                "range": {
                  "created_at": {
                    "gte": "2019-04-01T03:15:00.000Z"
                  }
                }
              },
              {
                "range": {
                  "created_at": {
                    "lte": "2019-04-30T03:15:00.000Z"
                  }
                }
              },
              {
                "range": {
                  "updated_at": {
                    "gte": new Date("2019-03-31T17:00:00.000Z")
                  }
                }
              },
              {
                "range": {
                  "updated_at": {
                    "lte": new Date("2019-04-30T16:59:59.999Z")
                  }
                }
              },
              {
                "term": {
                  "customer.id": 10000
                }
              },
              {
                "nested": {
                  "path": "line_items",
                  "filter": {
                    "term": {
                      "line_items.barcode": "HEO"
                    }
                  }
                }
              },
              {
                "terms": {
                  "location_id": [
                    1000,
                    2000
                  ]
                }
              }
            ],
            "must_not": [
              {
                "term": {
                  "status": "NEW"
                }
              }
            ]
          }
        }
      }
    }
  };
 
  assert.equal(errors, null);
  assert.deepEqual(queryDSL, expectedQueryDSL);
  assert.deepEqual(fields, { private_field : 0, customer : 0 });
  assert.equal(page, 2);
  assert.equal(skip, 20);
  assert.equal(limit, 20);
  assert.deepEqual(sort, ['created_at:asc', 'id:desc', '_score:desc']);
});

And prevent wrong query

it ('should parse query to elasticsearch query DSL fail when mis required field and use wrong operator', () => {
 
  let query = {
    'customer.name_gte' : 'hoang',
    'private_field_gt'  : '10',
    'unknown_field_lt'  : '0'
  };
 
  let { errors, filter } = parse(query);
 
  let expectedErrors = [
    {
      code     : 'ERR_WRONG_OPERATOR',
      field    : 'customer.name',
      type     : 'string',
      operator : 'gte',
      message  : `Can't use operator gte on customer.name has type string`
    },
    {
      code    : 'ERR_UNAVAILABLE_FIELD',
      field   : 'private_field',
      message : `Can't search on field private_field`
    },
    {
      code    : 'ERR_INVALID_FIELD',
      field   : 'unknown_field',
      message : `Invalid field unknown_field`
    },
    {
      code    : 'ERR_REQUIRED',
      field   : 'shop_id',
      message : 'shop_id is required'
    }
  ];
 
  assert.deepEqual(errors, expectedErrors);
});

Support permission on operators : equal, ne, in, nin

it ('should return not permission error with operator equal', () => {
 
  let query = {
    shop_id        : 1000001,
    location_id    : '2000',
  };
 
  let { errors, filter } = parse(query, { 
    permission : { 
      location_id : [1000, 3000],
    } 
  });
 
  assert.deepEqual(errors, [{
    code    : 'ERR_NOT_PERMISSION',
    field   : 'location_id',
    value   : 2000,
    message : `Can't see item has location_id = 2000`
  }]);
 
});
it ('should return not permission error with operator in', () => {
 
  let query = {
    shop_id        : 1000001,
    location_id_in : '1000,2000',
  };
 
  let { errors, filter } = parse(query, { 
    permission : { 
      location_id : [1000, 3000],
    } 
  });
 
  assert.deepEqual(errors, [{
    code    : 'ERR_NOT_PERMISSION',
    field   : 'location_id',
    value   : 2000,
    message : `Can't see item has location_id = 2000`
  }]);
 
});
it ('should auto assign field has permission to filter that not exists in query', () => {
 
  let query = {
    shop_id        : 100000001
  };
 
  let { errors, queryDSL } = parse(query, { 
    permission : { 
      location_id : [1000, 3000],
    } 
  });
 
  assert.deepEqual(queryDSL, {
    "query" : {
      "bool" : {
        "filter" : {
          "bool": {
            "must": [
              {
                "term" : {
                  "is_deleted" : false
                }
              },
              {
                "term": {
                  "shop_id": 100000001
                }
              },
              {
                "terms" : {
                  "location_id" : [1000, 3000]
                }
              }
            ]
          }
        }
      }
    }
  })
});

Support aggregations

it ('should generate simple elasticsearch aggregation and flatten function', () => {
  let xQuery = {
    shop_id         : 100000001,
    created_at_gte : "2019-04-01",
    created_at_lte : "2019-05-30",
    $group : [
      { month : { 
        date_histogram : { 
          field     : 'created_at', 
          interval  : 'month', 
          format    : 'MM/yyyy',
        } 
      }},
      { location : 'location_id' }
    ],
    $metrics  : [
      { total   : { count : '*'           }},
      { revenue : { sum   : 'total_price' }}
    ]
  };
 
  let { queryDSL, flatten } = parse(xQuery);
 
  assert.deepEqual(queryDSL, {
    "query": {
      "bool": {
        "filter": {
          "bool" : {
            "must": [
              {
                "term" : {
                  "is_deleted" : false
                }
              },
              {
                "term": {
                  "shop_id": 100000001
                }
              },
              {
                "range": {
                  "created_at": {
                    "gte": "2019-04-01",
                  }
                },
              },
              {
                "range": {
                  "created_at": {
                    "lte": "2019-05-30",
                  }
                }
              }
            ]
          }
        }
      }
    },
    "size": 0,
    "aggs": {
      "month": {
        "date_histogram": {
          "field": "created_at",
          "interval": "month",
          "time_zone": "+07:00",
          "format": "MM/yyyy",
          "order": {
            "_key": "desc"
          }
        },
        "aggs": {
          "location": {
            "terms": {
              "field": "location_id"
            },
            "aggs": {
              "revenue" : {
                "sum" : {
                  "field" : "total_price"
                }
              }
            }
          }
        }
      }
    }
  });
 
  let simple_es_res = {
    "took": 11,
    "timed_out": false,
    "_shards": {
      "total": 5,
      "successful": 5,
      "failed": 0
    },
    "hits": {
      "total": 135,
      "max_score": 0,
      "hits": []
    },
    "aggregations": {
      "month": {
        "buckets": [
          {
            "key_as_string": "05/2019",
            "key": 1556643600000,
            "doc_count": 17,
            "location": {
              "doc_count_error_upper_bound": 0,
              "sum_other_doc_count": 0,
              "buckets": [
                {
                  "key": "482663",
                  "doc_count": 17,
                  "revenue": {
                    "value": 12100000
                  }
                }
              ]
            }
          },
          {
            "key_as_string": "04/2019",
            "key": 1554051600000,
            "doc_count": 118,
            "location": {
              "doc_count_error_upper_bound": 0,
              "sum_other_doc_count": 0,
              "buckets": [
                {
                  "key": "482663",
                  "doc_count": 105,
                  "revenue": {
                    "value": 226919200
                  }
                },
                {
                  "key": "483179",
                  "doc_count": 8,
                  "revenue": {
                    "value": 4794000
                  }
                },
                {
                  "key": "482668",
                  "doc_count": 5,
                  "revenue": {
                    "value": 9100000
                  }
                }
              ]
            }
          }
        ]
      }
    }
  };
 
  let expected_flatten_items = [
    { month : '05/2019', location : '482663', total : 17 , revenue : 12100000  },
    { month : '04/2019', location : '482663', total : 105, revenue : 226919200 },
    { month : '04/2019', location : '483179', total : 8  , revenue : 4794000   },
    { month : '04/2019', location : '482668', total : 5  , revenue : 9100000   },
  ];
 
  let expected_flatten_rows = [
    ['05/2019','482663', 17 , 12100000 ],
    ['04/2019','482663', 105, 226919200],
    ['04/2019','483179', 8  , 4794000  ],
    ['04/2019','482668', 5  , 9100000  ],
  ];
 
  let { items, cols, rows } = flatten(simple_es_res, { genRows : true });
 
  assert.deepEqual(cols, ['month', 'location', 'total', 'revenue']);
  assert.deepEqual(items, expected_flatten_items);
  assert.deepEqual(rows, expected_flatten_rows);
});

Generate tracing info

it ('should generate tracing info', () => {
 
  let query = {
    'shop_id'              : '100000001',
    'created_at_gte'       : '2019-04-01T03:15:00.000Z',
    'created_at_lte'       : '2019-04-30T03:15:00.000Z',
    'updated_at_from_date' : '2019-04-01T03:15:00.000Z',
    'updated_at_to_date'   : '2019-04-30T03:15:00.000Z',
    'customer.id'          : '10000',
    'customer.name_like'   : 'hoang',
    'barcode'              : 'HEO',
    'status_ne'            : 'NEW',
    'location_id_in'       : '1000,2000',
    'tags_all'             : 'food,sea',
    'keyword'              : '0969728159',
  };
 
  let { trace } = parse(query, { isTrace : true });
 
  assert.deepEqual(trace, {
    'is_deleted'           : { field : 'is_deleted'        , alias : undefined, operator : ''          , raw_value : false                      , type : 'boolean', value : false                                , filter : { must : { term : { is_deleted : false } } } },
    'shop_id'              : { field : 'shop_id'           , alias : undefined, operator : ''          , raw_value : '100000001'                , type : 'long'   , value :  100000001                           , filter : { must : { term : { shop_id : 100000001 } } } },
    'created_at_gte'       : { field : 'created_at'        , alias : undefined, operator : 'gte'       , raw_value : '2019-04-01T03:15:00.000Z' , type : 'date'   , value : '2019-04-01T03:15:00.000Z'           , filter : { must : { range : { created_at : { gte : '2019-04-01T03:15:00.000Z' } }}} },
    'created_at_lte'       : { field : 'created_at'        , alias : undefined, operator : 'lte'       , raw_value : '2019-04-30T03:15:00.000Z' , type : 'date'   , value : '2019-04-30T03:15:00.000Z'           , filter : { must : { range : { created_at : { lte : '2019-04-30T03:15:00.000Z' } }}} },
    'updated_at_from_date' : { field : 'updated_at'        , alias : undefined, operator : 'from_date' , raw_value : '2019-04-01T03:15:00.000Z' , type : 'date'   , value : new Date("2019-03-31T17:00:00.000Z") , filter : { must : { range : { updated_at : { gte : new Date('2019-03-31T17:00:00.000Z') } }}} },
    'updated_at_to_date'   : { field : 'updated_at'        , alias : undefined, operator : 'to_date'   , raw_value : '2019-04-30T03:15:00.000Z' , type : 'date'   , value : new Date("2019-04-30T16:59:59.999Z") , filter : { must : { range : { updated_at : { lte : new Date('2019-04-30T16:59:59.999Z') } }}} },
    'customer.id'          : { field : 'customer.id'       , alias : undefined, operator : ''          , raw_value : '10000'                    , type : 'long'   , value : 10000                                , filter : { must : { term : { 'customer.id' : 10000 }}} },
    'customer.name_like'   : { field : 'customer.name'     , alias : undefined, operator : 'like'      , raw_value : 'hoang'                    , type : 'string' , value : 'hoang'                              , query  : { must : { query_string : { fields : ["customer.name"], query : "hoang", minimum_should_match : "100%", analyzer : "search_whitespace" }}}},
    'barcode'              : { field : 'line_items.barcode', alias : 'barcode', operator : ''          , raw_value : 'HEO'                      , type : 'string' , value : 'HEO'                                , filter : { must : { term : { 'line_items.barcode' : 'HEO' } }} },
    'status_ne'            : { field : 'status'            , alias : undefined, operator : 'ne'        , raw_value : 'NEW'                      , type : 'string' , value : 'NEW'                                , filter : { must_not : { term : { status : 'NEW' }}} },
    'location_id_in'       : { field : 'location_id'       , alias : undefined, operator : 'in'        , raw_value : '1000,2000'                , type : 'integer', value : [1000,2000]                          , filter : { must : { terms : { location_id : [1000, 2000] } }} },
    'tags_all'             : { field : 'tags'              , alias : undefined, operator : 'all'       , raw_value : 'food,sea'                 , type : 'string' , value : ['food', 'sea']                      , filter : { must : [ { term : { tags : 'food' }}, { term : { tags : 'sea' }}] } },
    'keyword'              : { field : undefined           , alias : undefined, operator : 'custom'    , raw_value : '0969728159'               , type : undefined, value : '0969728159'                         , query  : { must : [{ match : { fields : ['customer.name', 'customer.phone', 'customer.email'], query : '0969728159' }}]} },
  });
});

Full API documents is coming soon ...

Testing

npm test

Readme

Keywords

Package Sidebar

Install

npm i elastic-query-parser

Weekly Downloads

1

Version

1.3.4

License

MIT

Unpacked Size

87.7 kB

Total Files

19

Last publish

Collaborators

  • _hoang_