UNPKG

enju

Version:

An elasticsearch on node.js written in CoffeeScript.

608 lines (556 loc) 22.2 kB
q = require 'q' exceptions = require './exceptions' properties = require './properties' class QueryOperation @unequal = 'unequal' @equal = 'equal' @less = 'less' @lessEqual = 'lessEqual' @greater = 'greater' @greaterEqual = 'greaterEqual' @like = 'like' # only for string @unlike = 'unlike' # only for string @contains = 'contains' # it is mean `in` @exclude = 'exclude' @orderASC = 'orderASC' @orderDESC = 'orderDESC' @convertOperation = (value) -> switch value when '!=', QueryOperation.unequal QueryOperation.unequal when '==', QueryOperation.equal QueryOperation.equal when '<', QueryOperation.less QueryOperation.less when '<=', QueryOperation.lessEqual QueryOperation.lessEqual when '>', QueryOperation.greater QueryOperation.greater when '>=', QueryOperation.greaterEqual QueryOperation.greaterEqual when QueryOperation.like QueryOperation.like when QueryOperation.unlike QueryOperation.unlike when QueryOperation.contains QueryOperation.contains when QueryOperation.exclude QueryOperation.exclude when QueryOperation.orderASC QueryOperation.orderASC when QueryOperation.orderDESC QueryOperation.orderDESC else throw new exceptions.OperationError("There is no [#{value}] operation.") class QueryCell constructor: (args) -> {@dbField, @operation, @value, @isIntersect, @isUnion} = args # if there is a query like .where('field', contains: []) it will be true. @isContainsEmpty = @operation is QueryOperation.contains and not @value.length module.exports = class Query constructor: (documentClass, queryCells=[]) -> ### @param documentClass {constructor} The document's constructor. ### @documentClass = documentClass @queryCells = queryCells # ----------------------------------------------------- # class methods # ----------------------------------------------------- @updateReferenceProperties = (documents) -> ### Update reference properties of documents. @param documents {list<Document>} @returns {promise} ### deferred = q.defer() if not documents or not documents.length deferred.resolve() return deferred.promise dataTable = {} # {documentClassName: {documentId: {Document}}} documentClasses = {} # {documentClassName: documentClass} referenceProperties = [] # all reference properties in documents # scan what kind of documents should be fetched for propertyName, property of documents[0].constructor._properties if property.constructor isnt properties.ReferenceProperty continue if property.referenceClass.name not of dataTable dataTable[property.referenceClass.name] = {} documentClasses[property.referenceClass.name] = property.referenceClass referenceProperties.push property # scan what id of documents should be fetched for document in documents for property in referenceProperties # loop all reference properties in the document documentId = document[property.propertyName] if documentId dataTable[property.referenceClass.name][documentId] = null # fetch documents funcs = [] for documentClassName, items of dataTable funcs.push do (documentClassName, items) -> documentClasses[documentClassName].get(Object.keys(items), no).then (referenceDocuments) -> for referenceDocument in referenceDocuments dataTable[documentClassName][referenceDocument.id] = referenceDocument q.all(funcs) .catch (error) -> deferred.reject error .done -> # update reference properties of documents for document in documents for property in referenceProperties # loop all reference properties in the document resolveDocument = dataTable[property.referenceClass.name][document[property.propertyName]] if property.required and not resolveDocument console.log("There are a reference class can't mapping") continue document[property.propertyName] = resolveDocument deferred.resolve() deferred.promise # ----------------------------------------------------- # public methods # ----------------------------------------------------- where: (field, operation) -> ### It is intersect. ### @intersect field, operation intersect: (field, operation) -> ### Append a query as intersect. @param field {Property|string|function} Property: The property of the document. string: The property name of the document. function: The sub query. @param operation {object} key: [ '!=', 'unequal' '==', 'equal' '<', 'less' '<=', 'lessEqual' '>', 'greater', '>=', 'greaterEqual' 'like' 'unlike' 'contains' 'exclude' ] @returns {Query} ### refactorQueryCells = -> ### If the last query cell is union query cell, then we append the intersect query cell. We should let previous query cells be the sub query, the append this query cell. ### queryCells = [] for index in [@queryCells..0] by -1 queryCells.unshift @queryCells[index].pop() break if @queryCells[index].isIntersect @queryCells.push queryCells if typeof(field) is 'function' # .where (query) -> subQuery = field(new Query(@documentClass)) @queryCells.push subQuery.queryCells else # .where Document.name, '==': 'Enju' allFields = [] for propertyName, property of @documentClass._properties allFields.push propertyName allFields.push(property.dbField) if property.dbField if typeof(field) is 'string' and field.split('.', 1)[0] not in allFields throw new exceptions.SyntaxError("#{field} not in #{@documentClass.name}") previousQueryCell = if @queryCells.length then @queryCells[@queryCells.length - 1] else null if previousQueryCell and previousQueryCell.constructor isnt Array and previousQueryCell.isUnion refactorQueryCells() firstOperation = null value = null for firstOperation, value of operation break if typeof(field) is 'string' dbField = @documentClass._properties[field]?.dbField ? field else dbField = field.dbField ? field.propertyName @queryCells.push new QueryCell dbField: dbField operation: QueryOperation.convertOperation firstOperation value: value isIntersect: yes @ union: (field, operation) -> ### Append a query as intersect. @param field {Property|string} Property: The property of the document. string: The property name of the document. @param operation {object} key: [ '!=', 'unequal' '==', 'equal' '<', 'less' '<=', 'lessEqual' '>', 'greater', '>=', 'greaterEqual' 'like' 'unlike' 'contains' 'exclude' ] @returns {Query} ### if not @queryCells.length throw new exceptions.SyntaxError('Can not use .union() at the first query.') allFields = [] for propertyName, property of @documentClass._properties allFields.push propertyName allFields.push(property.dbField) if property.dbField if typeof(field) is 'string' and field.split('.', 1)[0] not in allFields throw new exceptions.SyntaxError("#{field} not in #{@documentClass.name}") firstOperation = null value = null for firstOperation, value of operation break if typeof(field) is 'string' dbField = @documentClass._properties[field]?.dbField ? field else dbField = field.dbField ? field.propertyName @queryCells.push new QueryCell dbField: dbField operation: QueryOperation.convertOperation firstOperation value: value isUnion: yes @ orderBy: (field, descending=no) -> ### Append the order query. @param member {Property|string} The property name of the document. @param descending {bool} Is sorted by descending? @returns {Query} ### allFields = [] for propertyName, property of @documentClass._properties allFields.push propertyName allFields.push(property.dbField) if property.dbField if typeof(field) is 'string' and field.split('.', 1)[0] not in allFields throw new exceptions.SyntaxError("#{field} not in #{@documentClass.name}") if descending operationCode = QueryOperation.orderDESC else operationCode = QueryOperation.orderASC if typeof(field) is 'string' dbField = @documentClass._properties[field]?.dbField ? field else dbField = field.dbField ? field.propertyName @queryCells.push new QueryCell dbField: dbField operation: operationCode @ fetch: (args={}) -> ### Fetch documents by this query. @param args {object} limit: {number} The size of the pagination. (The limit of the result items.) default is 1000 skip: {number} The offset of the pagination. (Skip x items.) default is 0 fetchReference: {bool} Fetch documents of reference properties. default is true. @returns {promise<object>} ({items: {Document}, total: {number}}) ### args.limit ?= 1000 args.skip ?= 0 args.fetchReference ?= yes deferred = q.defer() queryObject = @compileQueries() if queryObject.isContainsEmpty deferred.resolve items: [] total: 0 return deferred.promise @documentClass._es.search index: @documentClass.getIndexName() body: query: queryObject.query sort: queryObject.sort from: args.skip size: args.limit fields: ['_source'] version: yes , (error, response) => if error deferred.reject error return items = do => result = [] for hit in response.hits.hits item = id: hit._id version: hit._version for propertyName, property of @documentClass._properties dbField = property.dbField ? propertyName if dbField of hit._source item[propertyName] = hit._source[dbField] result.push new @documentClass(item) result total = response.hits.total if args.fetchReference Query.updateReferenceProperties(items).then -> deferred.resolve items: items total: total else deferred.resolve items: items total: total deferred.promise first: (fetchReference=yes) -> ### Fetch the first document by this query. @param fetchReference {bool} @returns {promise<Document|null>} ### deferred = q.defer() args = limit: 1 skip: 0 fetchReference: fetchReference @fetch(args) .then (result) -> deferred.resolve if result.items.length then result.items[0] else null .catch (error) -> deferred.reject error deferred.promise hasAny: -> ### Are there any documents match with the query? @returns {promise<bool>} ### deferred = q.defer() queryObject = @compileQueries() if queryObject.isContainsEmpty deferred.resolve no return deferred.promise @documentClass._es.searchExists index: @documentClass.getIndexName() body: query: queryObject.query .then (result) -> deferred.resolve if result.exists then yes else no .catch (error) -> if error?.body?.exists is no deferred.resolve no else deferred.reject error deferred.promise count: -> ### Count documents by the query. @returns {promise<number>} ### deferred = q.defer() queryObject = @compileQueries() @documentClass._es.count index: @documentClass.getIndexName() body: query: queryObject.query size: 0 , (error, response) -> if error deferred.reject error return deferred.resolve response.count deferred.promise sum: (field) -> ### Sum the field of documents by the query. https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html @param field {Property|string} The property name of the document. @returns {promise<number>} ### allFields = [] for propertyName, property of @documentClass._properties allFields.push propertyName allFields.push(property.dbField) if property.dbField if typeof(field) is 'string' and field.split('.', 1)[0] not in allFields throw new exceptions.SyntaxError("#{field} not in #{@documentClass.name}") if typeof(field) is 'string' dbField = @documentClass._properties[field]?.dbField ? field else dbField = field.dbField ? field.propertyName deferred = q.defer() queryObject = @compileQueries() if queryObject.isContainsEmpty deferred.resolve 0 return deferred.promise @documentClass._es.search index: @documentClass.getIndexName() body: query: queryObject.query aggs: intraday_return: sum: field: dbField size: 0 , (error, response) => if error deferred.reject error return deferred.resolve response.aggregations.intraday_return.value deferred.promise # ----------------------------------------------------- # private methods # ----------------------------------------------------- compileQueries: -> ### Compile query cells to elasticsearch query object. @returns {object} query: {object} sort: {list} isContainsEmpty: {bool} ### queries = [] sort = [] isContainsEmpty = no for queryCell in @queryCells if queryCell.constructor is Array # there are sub queries at this query subQuery = new Query(@documentClass, queryCell) elasticsearchQuery = subQuery.compileQueries() if elasticsearchQuery.isContainsEmpty continue queries.push elasticsearchQuery.query continue # compile query cell to elasticsearch query object and append into queries if queryCell.isContainsEmpty isContainsEmpty = yes break switch queryCell.operation when QueryOperation.orderASC sort.push "#{queryCell.dbField}": order: 'asc' ignore_unmapped: yes missing: '_first' when QueryOperation.orderDESC sort.push "#{queryCell.dbField}": order: 'desc' ignore_unmapped: yes missing: '_last' else queries.push @compileQuery queryCell result = sort: sort # append queries if isContainsEmpty result.isContainsEmpty = yes else if queries.length is 0 result.query = match_all: {} else if queries.length is 1 result.query = queries[0] else result.query = bool: should: queries minimumMatch = yes for index in [@queryCells.length - 1..0] by -1 if @queryCells[index].constructor isnt Array and @queryCells[index].isUnion minimumMatch = no break result.query.bool.minimum_should_match = queries.length if minimumMatch result compileQuery: (queryCell) -> ### @param queryCell: {QueryCell} @returns {object} ### bleachRegexCode = (value='') -> value = "#{value}" table = '^$*+?{}.[]()\\|/' result = [] for word in value if word in table result.push "\\#{word}" else result.push word result.join '' switch queryCell.operation when QueryOperation.equal if queryCell.value? match: "#{queryCell.dbField}": query: queryCell.value operator: 'and' else filtered: filter: missing: field: queryCell.dbField when QueryOperation.unequal if queryCell.value? bool: must_not: match: "#{queryCell.dbField}": query: queryCell.value operator: 'and' else bool: must_not: filtered: filter: missing: field: queryCell.dbField when QueryOperation.greater range: "#{queryCell.dbField}": gt: queryCell.value when QueryOperation.greaterEqual range: "#{queryCell.dbField}": gte: queryCell.value when QueryOperation.less range: "#{queryCell.dbField}": lt: queryCell.value when QueryOperation.lessEqual range: "#{queryCell.dbField}": lte: queryCell.value when QueryOperation.contains bool: should: ({match: {"#{queryCell.dbField}": {query: x, operator: 'and'}}} for x in queryCell.value) when QueryOperation.exclude bool: minimum_should_match: queryCell.value.length should: ({bool: {must_not: {match: {"#{queryCell.dbField}": {query: x, operator: 'and'}}}}} for x in queryCell.value) when QueryOperation.like value = bleachRegexCode queryCell.value bool: should: [ { match: "#{queryCell.dbField}": query: queryCell.value operator: 'and' } { regexp: "#{queryCell.dbField}": ".*#{value}.*" } ] when QueryOperation.unlike value = bleachRegexCode queryCell.value bool: minimum_should_match: 2 should: [ { bool: must_not: match: "#{queryCell.dbField}": query: queryCell.value operator: 'and' } { bool: must_not: regexp: "#{queryCell.dbField}": ".*#{value}.*" } ]