UNPKG

maxleap-react-native

Version:
934 lines (849 loc) 31.8 kB
'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; }, }); };