[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