maxleap-react-native
Version:
MaxLeap SDK for ReactNative
934 lines (849 loc) • 31.8 kB
JavaScript
'use strict';
var _ = require('underscore');
// ML.Query is a way to create a list of ML.Objects.
module.exports = function(ML) {
/**
* Creates a new maxleap ML.Query for the given ML.Object subclass.
* @param objectClass -
* An instance of a subclass of ML.Object, or a ML className string.
* @class
*
* <p>ML.Query defines a query that is used to fetch ML.Objects. The
* most common use case is finding all objects that match a query through the
* <code>find</code> method. For example, this sample code fetches all objects
* of class <code>MyClass</code>. It calls a different function depending on
* whether the fetch succeeded or not.
*
* <pre>
* var query = new ML.Query(MyClass);
* query.find({
* success: function(results) {
* // results is an array of ML.Object.
* },
*
* error: function(error) {
* // error is an instance of ML.Error.
* }
* });</pre></p>
*
* <p>A ML.Query can also be used to retrieve a single object whose id is
* known, through the get method. For example, this sample code fetches an
* object of class <code>MyClass</code> and id <code>myId</code>. It calls a
* different function depending on whether the fetch succeeded or not.
*
* <pre>
* var query = new ML.Query(MyClass);
* query.get(myId, {
* success: function(object) {
* // object is an instance of ML.Object.
* },
*
* error: function(object, error) {
* // error is an instance of ML.Error.
* }
* });</pre></p>
*
* <p>A ML.Query can also be used to count the number of objects that match
* the query without retrieving all of those objects. For example, this
* sample code counts the number of objects of the class <code>MyClass</code>
* <pre>
* var query = new ML.Query(MyClass);
* query.count({
* success: function(number) {
* // There are number instances of MyClass.
* },
*
* error: function(error) {
* // error is an instance of ML.Error.
* }
* });</pre></p>
*/
ML.Query = function(objectClass) {
if (_.isString(objectClass)) {
objectClass = ML.Object._getSubclass(objectClass);
}
this.objectClass = objectClass;
this.className = objectClass.prototype.className;
this._where = {};
this._include = [];
this._limit = -1; // negative limit means, do not send a limit
this._skip = 0;
this._extraOptions = {};
};
/**
* Constructs a ML.Query that is the OR of the passed in queries. For
* example:
* <pre>var compoundQuery = ML.Query.or(query1, query2, query3);</pre>
*
* will create a compoundQuery that is an or of the query1, query2, and
* query3.
* @param {...ML.Query} var_args The list of queries to OR.
* @return {ML.Query} The query that is the OR of the passed in queries.
*/
ML.Query.or = function() {
var queries = _.toArray(arguments);
var className = null;
ML._arrayEach(queries, function(q) {
if (_.isNull(className)) {
className = q.className;
}
if (className !== q.className) {
throw "All queries must be for the same class";
}
});
var query = new ML.Query(className);
query._orQuery(queries);
return query;
};
/**
* Constructs a ML.Query that is the AND of the passed in queries. For
* example:
* <pre>var compoundQuery = ML.Query.and(query1, query2, query3);</pre>
*
* will create a compoundQuery that is an 'and' of the query1, query2, and
* query3.
* @param {...ML.Query} var_args The list of queries to AND.
* @return {ML.Query} The query that is the AND of the passed in queries.
*/
ML.Query.and = function() {
var queries = _.toArray(arguments);
var className = null;
ML._arrayEach(queries, function(q) {
if (_.isNull(className)) {
className = q.className;
}
if (className !== q.className) {
throw "All queries must be for the same class";
}
});
var query = new ML.Query(className);
query._andQuery(queries);
return query;
};
/**
* Retrieves a list of MLObjects that satisfy the CQL.
* CQL syntax please see <a href='https://cn.maxleap.com/docs/cql_guide.html'>CQL Guide.</a>
* Either options.success or options.error is called when the find
* completes.
*
* @param {String} cql, A CQL string, see <a href='https://cn.maxleap.com/docs/cql_guide.html'>CQL Guide.</a>
* @param {Array} pvalues, An array contains placeholder values.
* @param {Object} options A Backbone-style options object,it's optional.
* @return {ML.Promise} A promise that is resolved with the results when
* the query completes,it's optional.
*/
ML.Query.doCloudQuery = function(cql, pvalues, options) {
var params = { cql: cql };
if(_.isArray(pvalues)){
params.pvalues = pvalues;
} else {
options = pvalues;
}
var request = ML._request("cloudQuery", null, null, 'GET', params);
return request.then(function(response) {
//query to process results.
var query = new ML.Query(response.className);
var results = _.map(response.results, function(json) {
var obj = query._newObject(response);
obj._finishFetch(query._processResult(json), true);
return obj;
});
return {
results: results,
count: response.count,
className: response.className
};
})._thenRunCallbacks(options);
};
ML.Query._extend = ML._extend;
ML.Query.prototype = {
//hook to iterate result. Added by dennis<xzhuang@maxleap.com>.
_processResult: function(obj){
return obj;
},
/**
* Constructs a ML.Object whose id is already known by fetching data from
* the server. Either options.success or options.error is called when the
* find completes.
*
* @param {} objectId The id of the object to be fetched.
* @param {Object} options A Backbone-style options object.
*/
get: function(objectId, options) {
if(!objectId) {
var errorObject = new ML.Error(ML.Error.OBJECT_NOT_FOUND,
"Object not found.");
return ML.Promise.error(errorObject);
}
var self = this;
self.equalTo('objectId', objectId);
return self.first().then(function(response) {
if (!ML._.isEmpty(response)) {
return response;
}
var errorObject = new ML.Error(ML.Error.OBJECT_NOT_FOUND,
"Object not found.");
return ML.Promise.error(errorObject);
})._thenRunCallbacks(options, null);
},
/**
* Returns a JSON representation of this query.
* @return {Object}
*/
toJSON: function() {
var params = {
where: this._where
};
if (this._include.length > 0) {
params.include = this._include.join(",");
}
if (this._select) {
params.keys = this._select.join(",");
}
if (this._limit >= 0) {
params.limit = this._limit;
}
if (this._skip > 0) {
params.skip = this._skip;
}
if (this._order !== undefined) {
params.order = this._order;
}
ML._objectEach(this._extraOptions, function(v, k) {
params[k] = v;
});
return params;
},
_newObject: function(response){
var obj;
if (response && response.className) {
obj = new ML.Object(response.className);
} else {
obj = new this.objectClass();
}
return obj;
},
_createRequest: function(params){
//后端接收的api是 /2.0/classes/{{classname}}/query
return ML._request("classes", this.className + '/query', null, "POST",
params || this.toJSON());
},
/**
* Retrieves a list of MLObjects that satisfy this query.
* Either options.success or options.error is called when the find
* completes.
*
* @param {Object} options A Backbone-style options object.
* @return {ML.Promise} A promise that is resolved with the results when
* the query completes.
*/
find: function(options) {
var self = this;
var request = this._createRequest();
return request.then(function(response) {
return _.map(response.results, function(json) {
var obj = self._newObject(response);
obj._finishFetch(self._processResult(json), true);
return obj;
});
})._thenRunCallbacks(options);
},
/**
* Delete objects retrieved by this query.
* @param {Object} options Standard options object with success and error
* callbacks.
* @return {ML.Promise} A promise that is fulfilled when the save
* completes.
*/
destroyAll: function(options){
var self = this;
return self.find().then(function(objects){
return ML.Object.destroyAll(objects);
})._thenRunCallbacks(options);
},
/**
* Counts the number of objects that match this query.
* Either options.success or options.error is called when the count
* completes.
*
* @param {Object} options A Backbone-style options object.
* @return {ML.Promise} A promise that is resolved with the count when
* the query completes.
*/
count: function(options) {
var params = this.toJSON();
params.limit = 0;
params.count = 1;
var request = this._createRequest(params);
return request.then(function(response) {
return response.count;
})._thenRunCallbacks(options);
},
/**
* Retrieves at most one ML.Object that satisfies this query.
*
* Either options.success or options.error is called when it completes.
* success is passed the object if there is one. otherwise, undefined.
*
* @param {Object} options A Backbone-style options object.
* @return {ML.Promise} A promise that is resolved with the object when
* the query completes.
*/
first: function(options) {
var self = this;
var params = this.toJSON();
params.limit = 1;
var request = this._createRequest(params);
return request.then(function(response) {
return _.map(response.results, function(json) {
var obj = self._newObject();
obj._finishFetch(self._processResult(json), true);
return obj;
})[0];
})._thenRunCallbacks(options);
},
/**
* Returns a new instance of ML.Collection backed by this query.
* @return {ML.Collection}
*/
collection: function(items, options) {
options = options || {};
return new ML.Collection(items, _.extend(options, {
model: this._objectClass || this.objectClass,
query: this
}));
},
/**
* Sets the number of results to skip before returning any results.
* This is useful for pagination.
* Default is to skip zero results.
* @param {Number} n the number of results to skip.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
skip: function(n) {
this._skip = n;
return this;
},
/**
* Sets the limit of the number of results to return. The default limit is
* 100, with a maximum of 1000 results being returned at a time.
* @param {Number} n the number of results to limit to.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
limit: function(n) {
this._limit = n;
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be equal to the provided value.
* @param {String} key The key to check.
* @param value The value that the ML.Object must contain.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
equalTo: function(key, value) {
this._where[key] = ML._encode(value);
return this;
},
/**
* Helper for condition queries
*/
_addCondition: function(key, condition, value) {
// Check if we already have a condition
if (!this._where[key]) {
this._where[key] = {};
}
this._where[key][condition] = ML._encode(value);
return this;
},
/**
* Add a constraint to the query that requires a particular
* <strong>array</strong> key's length to be equal to the provided value.
* @param {String} key The array key to check.
* @param value The length value.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
sizeEqualTo: function(key, value) {
this._addCondition(key, "$size", value);
},
/**
* Add a constraint to the query that requires a particular key's value to
* be not equal to the provided value.
* @param {String} key The key to check.
* @param value The value that must not be equalled.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
notEqualTo: function(key, value) {
this._addCondition(key, "$ne", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be less than the provided value.
* @param {String} key The key to check.
* @param value The value that provides an upper bound.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
lessThan: function(key, value) {
this._addCondition(key, "$lt", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be greater than the provided value.
* @param {String} key The key to check.
* @param value The value that provides an lower bound.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
greaterThan: function(key, value) {
this._addCondition(key, "$gt", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be less than or equal to the provided value.
* @param {String} key The key to check.
* @param value The value that provides an upper bound.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
lessThanOrEqualTo: function(key, value) {
this._addCondition(key, "$lte", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be greater than or equal to the provided value.
* @param {String} key The key to check.
* @param value The value that provides an lower bound.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
greaterThanOrEqualTo: function(key, value) {
this._addCondition(key, "$gte", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be contained in the provided list of values.
* @param {String} key The key to check.
* @param {Array} values The values that will match.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
containedIn: function(key, values) {
this._addCondition(key, "$in", values);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* not be contained in the provided list of values.
* @param {String} key The key to check.
* @param {Array} values The values that will not match.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
notContainedIn: function(key, values) {
this._addCondition(key, "$nin", values);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* contain each one of the provided list of values.
* @param {String} key The key to check. This key's value must be an array.
* @param {Array} values The values that will match.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
containsAll: function(key, values) {
this._addCondition(key, "$all", values);
return this;
},
/**
* Add a constraint for finding objects that contain the given key.
* @param {String} key The key that should exist.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
exists: function(key) {
this._addCondition(key, "$exists", true);
return this;
},
/**
* Add a constraint for finding objects that do not contain a given key.
* @param {String} key The key that should not exist
* @return {ML.Query} Returns the query, so you can chain this call.
*/
doesNotExist: function(key) {
this._addCondition(key, "$exists", false);
return this;
},
/**
* Add a regular expression constraint for finding string values that match
* the provided regular expression.
* This may be slow for large datasets.
* @param {String} key The key that the string to match is stored in.
* @param {RegExp} regex The regular expression pattern to match.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
matches: function(key, regex, modifiers) {
this._addCondition(key, "$regex", regex);
if (!modifiers) { modifiers = ""; }
// Javascript regex options support mig as inline options but store them
// as properties of the object. We support mi & should migrate them to
// modifiers
if (regex.ignoreCase) { modifiers += 'i'; }
if (regex.multiline) { modifiers += 'm'; }
if (modifiers && modifiers.length) {
this._addCondition(key, "$options", modifiers);
}
return this;
},
/**
* Add a constraint that requires that a key's value matches a ML.Query
* constraint.
* @param {String} key The key that the contains the object to match the
* query.
* @param {ML.Query} query The query that should match.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
matchesQuery: function(key, query) {
var queryJSON = query.toJSON();
queryJSON.className = query.className;
this._addCondition(key, "$inQuery", queryJSON);
return this;
},
/**
* Add a constraint that requires that a key's value not matches a
* ML.Query constraint.
* @param {String} key The key that the contains the object to match the
* query.
* @param {ML.Query} query The query that should not match.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
doesNotMatchQuery: function(key, query) {
var queryJSON = query.toJSON();
queryJSON.className = query.className;
this._addCondition(key, "$notInQuery", queryJSON);
return this;
},
/**
* Add a constraint that requires that a key's value matches a value in
* an object returned by a different ML.Query.
* @param {String} key The key that contains the value that is being
* matched.
* @param {String} queryKey The key in the objects returned by the query to
* match against.
* @param {ML.Query} query The query to run.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
matchesKeyInQuery: function(key, queryKey, query) {
var queryJSON = query.toJSON();
queryJSON.className = query.className;
this._addCondition(key, "$select",
{ key: queryKey, query: queryJSON });
return this;
},
/**
* Add a constraint that requires that a key's value not match a value in
* an object returned by a different ML.Query.
* @param {String} key The key that contains the value that is being
* excluded.
* @param {String} queryKey The key in the objects returned by the query to
* match against.
* @param {ML.Query} query The query to run.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
doesNotMatchKeyInQuery: function(key, queryKey, query) {
var queryJSON = query.toJSON();
queryJSON.className = query.className;
this._addCondition(key, "$dontSelect",
{ key: queryKey, query: queryJSON });
return this;
},
/**
* Add constraint that at least one of the passed in queries matches.
* @param {Array} queries
* @return {ML.Query} Returns the query, so you can chain this call.
*/
_orQuery: function(queries) {
var queryJSON = _.map(queries, function(q) {
return q.toJSON().where;
});
this._where.$or = queryJSON;
return this;
},
/**
* Add constraint that both of the passed in queries matches.
* @param {Array} queries
* @return {ML.Query} Returns the query, so you can chain this call.
*/
_andQuery: function(queries) {
var queryJSON = _.map(queries, function(q) {
return q.toJSON().where;
});
this._where.$and = queryJSON;
return this;
},
/**
* Converts a string into a regex that matches it.
* Surrounding with \Q .. \E does this, we just need to escape \E's in
* the text separately.
*/
_quote: function(s) {
return "\\Q" + s.replace("\\E", "\\E\\\\E\\Q") + "\\E";
},
/**
* Add a constraint for finding string values that contain a provided
* string. This may be slow for large datasets.
* @param {String} key The key that the string to match is stored in.
* @param {String} substring The substring that the value must contain.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
contains: function(key, value) {
this._addCondition(key, "$regex", this._quote(value));
return this;
},
/**
* Add a constraint for finding string values that start with a provided
* string. This query will use the backend index, so it will be fast even
* for large datasets.
* @param {String} key The key that the string to match is stored in.
* @param {String} prefix The substring that the value must start with.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
startsWith: function(key, value) {
this._addCondition(key, "$regex", "^" + this._quote(value));
return this;
},
/**
* Add a constraint for finding string values that end with a provided
* string. This will be slow for large datasets.
* @param {String} key The key that the string to match is stored in.
* @param {String} suffix The substring that the value must end with.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
endsWith: function(key, value) {
this._addCondition(key, "$regex", this._quote(value) + "$");
return this;
},
/**
* Sorts the results in ascending order by the given key.
*
* @param {String} key The key to order by.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
ascending: function(key) {
this._order = key;
return this;
},
/**
* Also sorts the results in ascending order by the given key. The previous sort keys have
* precedence over this key.
*
* @param {String} key The key to order by
* @return {ML.Query} Returns the query so you can chain this call.
*/
addAscending: function(key){
if(this._order)
this._order += ',' + key;
else
this._order = key;
return this;
},
/**
* Sorts the results in descending order by the given key.
*
* @param {String} key The key to order by.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
descending: function(key) {
this._order = "-" + key;
return this;
},
/**
* Also sorts the results in descending order by the given key. The previous sort keys have
* precedence over this key.
*
* @param {String} key The key to order by
* @return {ML.Query} Returns the query so you can chain this call.
*/
addDescending: function(key){
if(this._order)
this._order += ',-' + key;
else
this._order = '-' + key;
return key;
},
/**
* Add a proximity based constraint for finding objects with key point
* values near the point given.
* @param {String} key The key that the ML.GeoPoint is stored in.
* @param {ML.GeoPoint} point The reference ML.GeoPoint that is used.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
near: function(key, point) {
if (!(point instanceof ML.GeoPoint)) {
// Try to cast it to a GeoPoint, so that near("loc", [20,30]) works.
point = new ML.GeoPoint(point);
}
this._addCondition(key, "$nearSphere", point);
return this;
},
/**
* Add a proximity based constraint for finding objects with key point
* values near the point given and within the maximum distance given.
* @param {String} key The key that the ML.GeoPoint is stored in.
* @param {ML.GeoPoint} point The reference ML.GeoPoint that is used.
* @param maxDistance Maximum distance (in radians) of results to return.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
withinRadians: function(key, point, distance) {
this.near(key, point);
this._addCondition(key, "$maxDistance", distance);
return this;
},
/**
* Add a proximity based constraint for finding objects with key point
* values near the point given and within the maximum distance given.
* Radius of earth used is 3958.8 miles.
* @param {String} key The key that the ML.GeoPoint is stored in.
* @param {ML.GeoPoint} point The reference ML.GeoPoint that is used.
* @param {Number} maxDistance Maximum distance (in miles) of results to
* return.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
withinMiles: function(key, point, distance) {
return this.withinRadians(key, point, distance / 3958.8);
},
/**
* Add a proximity based constraint for finding objects with key point
* values near the point given and within the maximum distance given.
* Radius of earth used is 6371.0 kilometers.
* @param {String} key The key that the ML.GeoPoint is stored in.
* @param {ML.GeoPoint} point The reference ML.GeoPoint that is used.
* @param {Number} maxDistance Maximum distance (in kilometers) of results
* to return.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
withinKilometers: function(key, point, distance) {
return this.withinRadians(key, point, distance / 6371.0);
},
/**
* Add a constraint to the query that requires a particular key's
* coordinates be contained within a given rectangular geographic bounding
* box.
* @param {String} key The key to be constrained.
* @param {ML.GeoPoint} southwest
* The lower-left inclusive corner of the box.
* @param {ML.GeoPoint} northeast
* The upper-right inclusive corner of the box.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
withinGeoBox: function(key, southwest, northeast) {
if (!(southwest instanceof ML.GeoPoint)) {
southwest = new ML.GeoPoint(southwest);
}
if (!(northeast instanceof ML.GeoPoint)) {
northeast = new ML.GeoPoint(northeast);
}
this._addCondition(key, '$within', { '$box': [southwest, northeast] });
return this;
},
/**
* Include nested ML.Objects for the provided key. You can use dot
* notation to specify which fields in the included object are also fetch.
* @param {String} key The name of the key to include.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
include: function() {
var self = this;
ML._arrayEach(arguments, function(key) {
if (_.isArray(key)) {
self._include = self._include.concat(key);
} else {
self._include.push(key);
}
});
return this;
},
/**
* Restrict the fields of the returned ML.Objects to include only the
* provided keys. If this is called multiple times, then all of the keys
* specified in each of the calls will be included.
* @param {Array} keys The names of the keys to include.
* @return {ML.Query} Returns the query, so you can chain this call.
*/
select: function() {
var self = this;
this._select = this._select || [];
ML._arrayEach(arguments, function(key) {
if (_.isArray(key)) {
self._select = self._select.concat(key);
} else {
self._select.push(key);
}
});
return this;
},
/**
* Iterates over each result of a query, calling a callback for each one. If
* the callback returns a promise, the iteration will not continue until
* that promise has been fulfilled. If the callback returns a rejected
* promise, then iteration will stop with that error. The items are
* processed in an unspecified order. The query may not have any sort order,
* and may not use limit or skip.
* @param callback {Function} Callback that will be called with each result
* of the query.
* @param options {Object} An optional Backbone-like options object with
* success and error callbacks that will be invoked once the iteration
* has finished.
* @return {ML.Promise} A promise that will be fulfilled once the
* iteration has completed.
*/
each: function(callback, options) {
options = options || {};
if (this._order || this._skip || (this._limit >= 0)) {
var error =
"Cannot iterate on a query with sort, skip, or limit.";
return ML.Promise.error(error)._thenRunCallbacks(options);
}
var promise = new ML.Promise();
var query = new ML.Query(this.objectClass);
// We can override the batch size from the options.
// This is undocumented, but useful for testing.
query._limit = options.batchSize || 100;
query._where = _.clone(this._where);
query._include = _.clone(this._include);
query.ascending('objectId');
var finished = false;
return ML.Promise._continueWhile(function() {
return !finished;
}, function() {
return query.find().then(function(results) {
var callbacksDone = ML.Promise.as();
ML._.each(results, function(result) {
callbacksDone = callbacksDone.then(function() {
return callback(result);
});
});
return callbacksDone.then(function() {
if (results.length >= query._limit) {
query.greaterThan("objectId", results[results.length - 1].id);
} else {
finished = true;
}
});
});
})._thenRunCallbacks(options);
}
};
ML.FriendShipQuery = ML.Query._extend({
_objectClass: ML.User,
_newObject: function(){
return new ML.User();
},
_processResult: function(json){
var user = json[this._friendshipTag];
if(user.__type === 'Pointer' && user.className === '_User'){
delete user.__type;
delete user.className;
}
return user;
},
});
};