enju
Version:
An elasticsearch on node.js written in CoffeeScript.
919 lines (866 loc) • 28.7 kB
JavaScript
(function() {
var Query, QueryCell, QueryOperation, exceptions, properties, q,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
q = require('q');
exceptions = require('./exceptions');
properties = require('./properties');
QueryOperation = (function() {
function QueryOperation() {}
QueryOperation.unequal = 'unequal';
QueryOperation.equal = 'equal';
QueryOperation.less = 'less';
QueryOperation.lessEqual = 'lessEqual';
QueryOperation.greater = 'greater';
QueryOperation.greaterEqual = 'greaterEqual';
QueryOperation.like = 'like';
QueryOperation.unlike = 'unlike';
QueryOperation.contains = 'contains';
QueryOperation.exclude = 'exclude';
QueryOperation.orderASC = 'orderASC';
QueryOperation.orderDESC = 'orderDESC';
QueryOperation.convertOperation = function(value) {
switch (value) {
case '!=':
case QueryOperation.unequal:
return QueryOperation.unequal;
case '==':
case QueryOperation.equal:
return QueryOperation.equal;
case '<':
case QueryOperation.less:
return QueryOperation.less;
case '<=':
case QueryOperation.lessEqual:
return QueryOperation.lessEqual;
case '>':
case QueryOperation.greater:
return QueryOperation.greater;
case '>=':
case QueryOperation.greaterEqual:
return QueryOperation.greaterEqual;
case QueryOperation.like:
return QueryOperation.like;
case QueryOperation.unlike:
return QueryOperation.unlike;
case QueryOperation.contains:
return QueryOperation.contains;
case QueryOperation.exclude:
return QueryOperation.exclude;
case QueryOperation.orderASC:
return QueryOperation.orderASC;
case QueryOperation.orderDESC:
return QueryOperation.orderDESC;
default:
throw new exceptions.OperationError("There is no [" + value + "] operation.");
}
};
return QueryOperation;
})();
QueryCell = (function() {
function QueryCell(args) {
this.dbField = args.dbField, this.operation = args.operation, this.value = args.value, this.isIntersect = args.isIntersect, this.isUnion = args.isUnion;
this.isContainsEmpty = this.operation === QueryOperation.contains && !this.value.length;
}
return QueryCell;
})();
module.exports = Query = (function() {
function Query(documentClass, queryCells) {
if (queryCells == null) {
queryCells = [];
}
/*
@param documentClass {constructor} The document's constructor.
*/
this.documentClass = documentClass;
this.queryCells = queryCells;
}
Query.updateReferenceProperties = function(documents) {
/*
Update reference properties of documents.
@param documents {list<Document>}
@returns {promise}
*/
var dataTable, deferred, document, documentClassName, documentClasses, documentId, funcs, i, items, j, len, len1, property, propertyName, ref, referenceProperties;
deferred = q.defer();
if (!documents || !documents.length) {
deferred.resolve();
return deferred.promise;
}
dataTable = {};
documentClasses = {};
referenceProperties = [];
ref = documents[0].constructor._properties;
for (propertyName in ref) {
property = ref[propertyName];
if (property.constructor !== properties.ReferenceProperty) {
continue;
}
if (!(property.referenceClass.name in dataTable)) {
dataTable[property.referenceClass.name] = {};
documentClasses[property.referenceClass.name] = property.referenceClass;
}
referenceProperties.push(property);
}
for (i = 0, len = documents.length; i < len; i++) {
document = documents[i];
for (j = 0, len1 = referenceProperties.length; j < len1; j++) {
property = referenceProperties[j];
documentId = document[property.propertyName];
if (documentId) {
dataTable[property.referenceClass.name][documentId] = null;
}
}
}
funcs = [];
for (documentClassName in dataTable) {
items = dataTable[documentClassName];
funcs.push((function(documentClassName, items) {
return documentClasses[documentClassName].get(Object.keys(items), false).then(function(referenceDocuments) {
var k, len2, referenceDocument, results;
results = [];
for (k = 0, len2 = referenceDocuments.length; k < len2; k++) {
referenceDocument = referenceDocuments[k];
results.push(dataTable[documentClassName][referenceDocument.id] = referenceDocument);
}
return results;
});
})(documentClassName, items));
}
q.all(funcs)["catch"](function(error) {
return deferred.reject(error);
}).done(function() {
var k, l, len2, len3, resolveDocument;
for (k = 0, len2 = documents.length; k < len2; k++) {
document = documents[k];
for (l = 0, len3 = referenceProperties.length; l < len3; l++) {
property = referenceProperties[l];
resolveDocument = dataTable[property.referenceClass.name][document[property.propertyName]];
if (property.required && !resolveDocument) {
console.log("There are a reference class can't mapping");
continue;
}
document[property.propertyName] = resolveDocument;
}
}
return deferred.resolve();
});
return deferred.promise;
};
Query.prototype.where = function(field, operation) {
/*
It is intersect.
*/
return this.intersect(field, operation);
};
Query.prototype.intersect = function(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}
*/
var allFields, dbField, firstOperation, previousQueryCell, property, propertyName, ref, ref1, ref2, ref3, ref4, refactorQueryCells, subQuery, value;
refactorQueryCells = function() {
/*
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.
*/
var i, index, queryCells, ref;
queryCells = [];
for (index = i = ref = this.queryCells; i >= 0; index = i += -1) {
queryCells.unshift(this.queryCells[index].pop());
if (this.queryCells[index].isIntersect) {
break;
}
}
return this.queryCells.push(queryCells);
};
if (typeof field === 'function') {
subQuery = field(new Query(this.documentClass));
this.queryCells.push(subQuery.queryCells);
} else {
allFields = [];
ref = this.documentClass._properties;
for (propertyName in ref) {
property = ref[propertyName];
allFields.push(propertyName);
if (property.dbField) {
allFields.push(property.dbField);
}
}
if (typeof field === 'string' && (ref1 = field.split('.', 1)[0], indexOf.call(allFields, ref1) < 0)) {
throw new exceptions.SyntaxError(field + " not in " + this.documentClass.name);
}
previousQueryCell = this.queryCells.length ? this.queryCells[this.queryCells.length - 1] : null;
if (previousQueryCell && previousQueryCell.constructor !== Array && previousQueryCell.isUnion) {
refactorQueryCells();
}
firstOperation = null;
value = null;
for (firstOperation in operation) {
value = operation[firstOperation];
break;
}
if (typeof field === 'string') {
dbField = (ref2 = (ref3 = this.documentClass._properties[field]) != null ? ref3.dbField : void 0) != null ? ref2 : field;
} else {
dbField = (ref4 = field.dbField) != null ? ref4 : field.propertyName;
}
this.queryCells.push(new QueryCell({
dbField: dbField,
operation: QueryOperation.convertOperation(firstOperation),
value: value,
isIntersect: true
}));
}
return this;
};
Query.prototype.union = function(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}
*/
var allFields, dbField, firstOperation, property, propertyName, ref, ref1, ref2, ref3, ref4, value;
if (!this.queryCells.length) {
throw new exceptions.SyntaxError('Can not use .union() at the first query.');
}
allFields = [];
ref = this.documentClass._properties;
for (propertyName in ref) {
property = ref[propertyName];
allFields.push(propertyName);
if (property.dbField) {
allFields.push(property.dbField);
}
}
if (typeof field === 'string' && (ref1 = field.split('.', 1)[0], indexOf.call(allFields, ref1) < 0)) {
throw new exceptions.SyntaxError(field + " not in " + this.documentClass.name);
}
firstOperation = null;
value = null;
for (firstOperation in operation) {
value = operation[firstOperation];
break;
}
if (typeof field === 'string') {
dbField = (ref2 = (ref3 = this.documentClass._properties[field]) != null ? ref3.dbField : void 0) != null ? ref2 : field;
} else {
dbField = (ref4 = field.dbField) != null ? ref4 : field.propertyName;
}
this.queryCells.push(new QueryCell({
dbField: dbField,
operation: QueryOperation.convertOperation(firstOperation),
value: value,
isUnion: true
}));
return this;
};
Query.prototype.orderBy = function(field, descending) {
var allFields, dbField, operationCode, property, propertyName, ref, ref1, ref2, ref3, ref4;
if (descending == null) {
descending = false;
}
/*
Append the order query.
@param member {Property|string} The property name of the document.
@param descending {bool} Is sorted by descending?
@returns {Query}
*/
allFields = [];
ref = this.documentClass._properties;
for (propertyName in ref) {
property = ref[propertyName];
allFields.push(propertyName);
if (property.dbField) {
allFields.push(property.dbField);
}
}
if (typeof field === 'string' && (ref1 = field.split('.', 1)[0], indexOf.call(allFields, ref1) < 0)) {
throw new exceptions.SyntaxError(field + " not in " + this.documentClass.name);
}
if (descending) {
operationCode = QueryOperation.orderDESC;
} else {
operationCode = QueryOperation.orderASC;
}
if (typeof field === 'string') {
dbField = (ref2 = (ref3 = this.documentClass._properties[field]) != null ? ref3.dbField : void 0) != null ? ref2 : field;
} else {
dbField = (ref4 = field.dbField) != null ? ref4 : field.propertyName;
}
this.queryCells.push(new QueryCell({
dbField: dbField,
operation: operationCode
}));
return this;
};
Query.prototype.fetch = function(args) {
var deferred, queryObject;
if (args == null) {
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}})
*/
if (args.limit == null) {
args.limit = 1000;
}
if (args.skip == null) {
args.skip = 0;
}
if (args.fetchReference == null) {
args.fetchReference = true;
}
deferred = q.defer();
queryObject = this.compileQueries();
if (queryObject.isContainsEmpty) {
deferred.resolve({
items: [],
total: 0
});
return deferred.promise;
}
this.documentClass._es.search({
index: this.documentClass.getIndexName(),
body: {
query: queryObject.query,
sort: queryObject.sort
},
from: args.skip,
size: args.limit,
fields: ['_source'],
version: true
}, (function(_this) {
return function(error, response) {
var items, total;
if (error) {
deferred.reject(error);
return;
}
items = (function() {
var dbField, hit, i, item, len, property, propertyName, ref, ref1, ref2, result;
result = [];
ref = response.hits.hits;
for (i = 0, len = ref.length; i < len; i++) {
hit = ref[i];
item = {
id: hit._id,
version: hit._version
};
ref1 = _this.documentClass._properties;
for (propertyName in ref1) {
property = ref1[propertyName];
dbField = (ref2 = property.dbField) != null ? ref2 : propertyName;
if (dbField in hit._source) {
item[propertyName] = hit._source[dbField];
}
}
result.push(new _this.documentClass(item));
}
return result;
})();
total = response.hits.total;
if (args.fetchReference) {
return Query.updateReferenceProperties(items).then(function() {
return deferred.resolve({
items: items,
total: total
});
});
} else {
return deferred.resolve({
items: items,
total: total
});
}
};
})(this));
return deferred.promise;
};
Query.prototype.first = function(fetchReference) {
var args, deferred;
if (fetchReference == null) {
fetchReference = true;
}
/*
Fetch the first document by this query.
@param fetchReference {bool}
@returns {promise<Document|null>}
*/
deferred = q.defer();
args = {
limit: 1,
skip: 0,
fetchReference: fetchReference
};
this.fetch(args).then(function(result) {
return deferred.resolve(result.items.length ? result.items[0] : null);
})["catch"](function(error) {
return deferred.reject(error);
});
return deferred.promise;
};
Query.prototype.hasAny = function() {
/*
Are there any documents match with the query?
@returns {promise<bool>}
*/
var deferred, queryObject;
deferred = q.defer();
queryObject = this.compileQueries();
if (queryObject.isContainsEmpty) {
deferred.resolve(false);
return deferred.promise;
}
this.documentClass._es.searchExists({
index: this.documentClass.getIndexName(),
body: {
query: queryObject.query
}
}).then(function(result) {
return deferred.resolve(result.exists ? true : false);
})["catch"](function(error) {
var ref;
if ((error != null ? (ref = error.body) != null ? ref.exists : void 0 : void 0) === false) {
return deferred.resolve(false);
} else {
return deferred.reject(error);
}
});
return deferred.promise;
};
Query.prototype.count = function() {
/*
Count documents by the query.
@returns {promise<number>}
*/
var deferred, queryObject;
deferred = q.defer();
queryObject = this.compileQueries();
this.documentClass._es.count({
index: this.documentClass.getIndexName(),
body: {
query: queryObject.query
},
size: 0
}, function(error, response) {
if (error) {
deferred.reject(error);
return;
}
return deferred.resolve(response.count);
});
return deferred.promise;
};
Query.prototype.sum = function(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>}
*/
var allFields, dbField, deferred, property, propertyName, queryObject, ref, ref1, ref2, ref3, ref4;
allFields = [];
ref = this.documentClass._properties;
for (propertyName in ref) {
property = ref[propertyName];
allFields.push(propertyName);
if (property.dbField) {
allFields.push(property.dbField);
}
}
if (typeof field === 'string' && (ref1 = field.split('.', 1)[0], indexOf.call(allFields, ref1) < 0)) {
throw new exceptions.SyntaxError(field + " not in " + this.documentClass.name);
}
if (typeof field === 'string') {
dbField = (ref2 = (ref3 = this.documentClass._properties[field]) != null ? ref3.dbField : void 0) != null ? ref2 : field;
} else {
dbField = (ref4 = field.dbField) != null ? ref4 : field.propertyName;
}
deferred = q.defer();
queryObject = this.compileQueries();
if (queryObject.isContainsEmpty) {
deferred.resolve(0);
return deferred.promise;
}
this.documentClass._es.search({
index: this.documentClass.getIndexName(),
body: {
query: queryObject.query,
aggs: {
intraday_return: {
sum: {
field: dbField
}
}
}
},
size: 0
}, (function(_this) {
return function(error, response) {
if (error) {
deferred.reject(error);
return;
}
return deferred.resolve(response.aggregations.intraday_return.value);
};
})(this));
return deferred.promise;
};
Query.prototype.compileQueries = function() {
/*
Compile query cells to elasticsearch query object.
@returns {object}
query: {object}
sort: {list}
isContainsEmpty: {bool}
*/
var elasticsearchQuery, i, index, isContainsEmpty, j, len, minimumMatch, obj, obj1, queries, queryCell, ref, ref1, result, sort, subQuery;
queries = [];
sort = [];
isContainsEmpty = false;
ref = this.queryCells;
for (i = 0, len = ref.length; i < len; i++) {
queryCell = ref[i];
if (queryCell.constructor === Array) {
subQuery = new Query(this.documentClass, queryCell);
elasticsearchQuery = subQuery.compileQueries();
if (elasticsearchQuery.isContainsEmpty) {
continue;
}
queries.push(elasticsearchQuery.query);
continue;
}
if (queryCell.isContainsEmpty) {
isContainsEmpty = true;
break;
}
switch (queryCell.operation) {
case QueryOperation.orderASC:
sort.push((
obj = {},
obj["" + queryCell.dbField] = {
order: 'asc',
ignore_unmapped: true,
missing: '_first'
},
obj
));
break;
case QueryOperation.orderDESC:
sort.push((
obj1 = {},
obj1["" + queryCell.dbField] = {
order: 'desc',
ignore_unmapped: true,
missing: '_last'
},
obj1
));
break;
default:
queries.push(this.compileQuery(queryCell));
}
}
result = {
sort: sort
};
if (isContainsEmpty) {
result.isContainsEmpty = true;
} else if (queries.length === 0) {
result.query = {
match_all: {}
};
} else if (queries.length === 1) {
result.query = queries[0];
} else {
result.query = {
bool: {
should: queries
}
};
minimumMatch = true;
for (index = j = ref1 = this.queryCells.length - 1; j >= 0; index = j += -1) {
if (this.queryCells[index].constructor !== Array && this.queryCells[index].isUnion) {
minimumMatch = false;
break;
}
}
if (minimumMatch) {
result.query.bool.minimum_should_match = queries.length;
}
}
return result;
};
Query.prototype.compileQuery = function(queryCell) {
/*
@param queryCell: {QueryCell}
@returns {object}
*/
var bleachRegexCode, obj, obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, value, x;
bleachRegexCode = function(value) {
var i, len, result, table, word;
if (value == null) {
value = '';
}
value = "" + value;
table = '^$*+?{}.[]()\\|/';
result = [];
for (i = 0, len = value.length; i < len; i++) {
word = value[i];
if (indexOf.call(table, word) >= 0) {
result.push("\\" + word);
} else {
result.push(word);
}
}
return result.join('');
};
switch (queryCell.operation) {
case QueryOperation.equal:
if (queryCell.value != null) {
return {
match: (
obj = {},
obj["" + queryCell.dbField] = {
query: queryCell.value,
operator: 'and'
},
obj
)
};
} else {
return {
filtered: {
filter: {
missing: {
field: queryCell.dbField
}
}
}
};
}
break;
case QueryOperation.unequal:
if (queryCell.value != null) {
return {
bool: {
must_not: {
match: (
obj1 = {},
obj1["" + queryCell.dbField] = {
query: queryCell.value,
operator: 'and'
},
obj1
)
}
}
};
} else {
return {
bool: {
must_not: {
filtered: {
filter: {
missing: {
field: queryCell.dbField
}
}
}
}
}
};
}
break;
case QueryOperation.greater:
return {
range: (
obj2 = {},
obj2["" + queryCell.dbField] = {
gt: queryCell.value
},
obj2
)
};
case QueryOperation.greaterEqual:
return {
range: (
obj3 = {},
obj3["" + queryCell.dbField] = {
gte: queryCell.value
},
obj3
)
};
case QueryOperation.less:
return {
range: (
obj4 = {},
obj4["" + queryCell.dbField] = {
lt: queryCell.value
},
obj4
)
};
case QueryOperation.lessEqual:
return {
range: (
obj5 = {},
obj5["" + queryCell.dbField] = {
lte: queryCell.value
},
obj5
)
};
case QueryOperation.contains:
return {
bool: {
should: (function() {
var i, len, obj6, ref, results;
ref = queryCell.value;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
x = ref[i];
results.push({
match: (
obj6 = {},
obj6["" + queryCell.dbField] = {
query: x,
operator: 'and'
},
obj6
)
});
}
return results;
})()
}
};
case QueryOperation.exclude:
return {
bool: {
minimum_should_match: queryCell.value.length,
should: (function() {
var i, len, obj6, ref, results;
ref = queryCell.value;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
x = ref[i];
results.push({
bool: {
must_not: {
match: (
obj6 = {},
obj6["" + queryCell.dbField] = {
query: x,
operator: 'and'
},
obj6
)
}
}
});
}
return results;
})()
}
};
case QueryOperation.like:
value = bleachRegexCode(queryCell.value);
return {
bool: {
should: [
{
match: (
obj6 = {},
obj6["" + queryCell.dbField] = {
query: queryCell.value,
operator: 'and'
},
obj6
)
}, {
regexp: (
obj7 = {},
obj7["" + queryCell.dbField] = ".*" + value + ".*",
obj7
)
}
]
}
};
case QueryOperation.unlike:
value = bleachRegexCode(queryCell.value);
return {
bool: {
minimum_should_match: 2,
should: [
{
bool: {
must_not: {
match: (
obj8 = {},
obj8["" + queryCell.dbField] = {
query: queryCell.value,
operator: 'and'
},
obj8
)
}
}
}, {
bool: {
must_not: {
regexp: (
obj9 = {},
obj9["" + queryCell.dbField] = ".*" + value + ".*",
obj9
)
}
}
}
]
}
};
}
};
return Query;
})();
}).call(this);