UNPKG

leancloud-storage

Version:
917 lines (831 loc) 31.8 kB
'use strict'; var _ = require('underscore'); var AVError = require('./error'); var AVRequest = require('./request').request; var _require = require('./utils'), ensureArray = _require.ensureArray; var requires = function requires(value, message) { if (value === undefined) { throw new Error(message); } }; // AV.Query is a way to create a list of AV.Objects. module.exports = function (AV) { /** * Creates a new AV.Query for the given AV.Object subclass. * @param {Class|String} objectClass An instance of a subclass of AV.Object, or a AV className string. * @class * * <p>AV.Query defines a query that is used to fetch AV.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 AV.Query(MyClass); * query.find().then(function(results) { * // results is an array of AV.Object. * }, function(error) { * // error is an instance of AVError. * });</pre></p> * * <p>An AV.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 AV.Query(MyClass); * query.get(myId).then(function(object) { * // object is an instance of AV.Object. * }, function(error) { * // error is an instance of AVError. * });</pre></p> * * <p>An AV.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 AV.Query(MyClass); * query.count().then(function(number) { * // There are number instances of MyClass. * }, function(error) { * // error is an instance of AVError. * });</pre></p> */ AV.Query = function (objectClass) { if (_.isString(objectClass)) { objectClass = AV.Object._getSubclass(objectClass); } this.objectClass = objectClass; this.className = objectClass.prototype.className; this._where = {}; this._include = []; this._select = []; this._limit = -1; // negative limit means, do not send a limit this._skip = 0; this._extraOptions = {}; }; /** * Constructs a AV.Query that is the OR of the passed in queries. For * example: * <pre>var compoundQuery = AV.Query.or(query1, query2, query3);</pre> * * will create a compoundQuery that is an or of the query1, query2, and * query3. * @param {...AV.Query} var_args The list of queries to OR. * @return {AV.Query} The query that is the OR of the passed in queries. */ AV.Query.or = function () { var queries = _.toArray(arguments); var className = null; AV._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 AV.Query(className); query._orQuery(queries); return query; }; /** * Constructs a AV.Query that is the AND of the passed in queries. For * example: * <pre>var compoundQuery = AV.Query.and(query1, query2, query3);</pre> * * will create a compoundQuery that is an 'and' of the query1, query2, and * query3. * @param {...AV.Query} var_args The list of queries to AND. * @return {AV.Query} The query that is the AND of the passed in queries. */ AV.Query.and = function () { var queries = _.toArray(arguments); var className = null; AV._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 AV.Query(className); query._andQuery(queries); return query; }; /** * Retrieves a list of AVObjects that satisfy the CQL. * CQL syntax please see {@link https://leancloud.cn/docs/cql_guide.html CQL Guide}. * * @param {String} cql A CQL string, see {@link https://leancloud.cn/docs/cql_guide.html CQL Guide}. * @param {Array} pvalues An array contains placeholder values. * @param {AuthOptions} options * @return {Promise} A promise that is resolved with the results when * the query completes. */ AV.Query.doCloudQuery = function (cql, pvalues, options) { var params = { cql: cql }; if (_.isArray(pvalues)) { params.pvalues = pvalues; } else { options = pvalues; } var request = AVRequest('cloudQuery', null, null, 'GET', params, options); return request.then(function (response) { //query to process results. var query = new AV.Query(response.className); var results = _.map(response.results, function (json) { var obj = query._newObject(response); if (obj._finishFetch) { obj._finishFetch(query._processResult(json), true); } return obj; }); return { results: results, count: response.count, className: response.className }; }); }; AV.Query._extend = AV._extend; AV.Query.prototype = { //hook to iterate result. Added by dennis<xzhuang@avoscloud.com>. _processResult: function _processResult(obj) { return obj; }, /** * Constructs an AV.Object whose id is already known by fetching data from * the server. * * @param {String} objectId The id of the object to be fetched. * @param {AuthOptions} options * @return {Promise.<AV.Object>} */ get: function get(objectId, options) { if (!objectId) { var errorObject = new AVError(AVError.OBJECT_NOT_FOUND, "Object not found."); throw errorObject; } var self = this; var obj = self._newObject(); obj.id = objectId; var queryJSON = self.toJSON(); var fetchOptions = {}; if (queryJSON.keys) fetchOptions.keys = queryJSON.keys; if (queryJSON.include) fetchOptions.include = queryJSON.include; return obj.fetch(fetchOptions, options); }, /** * Returns a JSON representation of this query. * @return {Object} */ toJSON: function toJSON() { var params = { where: this._where }; if (this._include.length > 0) { params.include = this._include.join(","); } if (this._select.length > 0) { 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; } AV._objectEach(this._extraOptions, function (v, k) { params[k] = v; }); return params; }, _newObject: function _newObject(response) { var obj; if (response && response.className) { obj = new AV.Object(response.className); } else { obj = new this.objectClass(); } return obj; }, _createRequest: function _createRequest(params, options) { return AVRequest('classes', this.className, null, "GET", params || this.toJSON(), options); }, /** * Retrieves a list of AVObjects that satisfy this query. * * @param {AuthOptions} options * @return {Promise} A promise that is resolved with the results when * the query completes. */ find: function find(options) { var self = this; var request = this._createRequest(null, options); return request.then(function (response) { return _.map(response.results, function (json) { var obj = self._newObject(response); if (obj._finishFetch) { obj._finishFetch(self._processResult(json), true); } return obj; }); }); }, /** * Delete objects retrieved by this query. * @param {AuthOptions} options * @return {Promise} A promise that is fulfilled when the save * completes. */ destroyAll: function destroyAll(options) { var self = this; return self.find(options).then(function (objects) { return AV.Object.destroyAll(objects); }); }, /** * Counts the number of objects that match this query. * * @param {AuthOptions} options * @return {Promise} A promise that is resolved with the count when * the query completes. */ count: function count(options) { var params = this.toJSON(); params.limit = 0; params.count = 1; var request = this._createRequest(params, options); return request.then(function (response) { return response.count; }); }, /** * Retrieves at most one AV.Object that satisfies this query. * * @param {AuthOptions} options * @return {Promise} A promise that is resolved with the object when * the query completes. */ first: function first(options) { var self = this; var params = this.toJSON(); params.limit = 1; var request = this._createRequest(params, options); return request.then(function (response) { return _.map(response.results, function (json) { var obj = self._newObject(); if (obj._finishFetch) { obj._finishFetch(self._processResult(json), true); } return obj; })[0]; }); }, /** * 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 {AV.Query} Returns the query, so you can chain this call. */ skip: function skip(n) { requires(n, 'undefined is not a valid skip value'); 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 {AV.Query} Returns the query, so you can chain this call. */ limit: function limit(n) { requires(n, 'undefined is not a valid limit value'); 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 AV.Object must contain. * @return {AV.Query} Returns the query, so you can chain this call. */ equalTo: function equalTo(key, value) { requires(key, 'undefined is not a valid key'); requires(value, 'undefined is not a valid value'); this._where[key] = AV._encode(value); return this; }, /** * Helper for condition queries * @private */ _addCondition: function _addCondition(key, condition, value) { requires(key, 'undefined is not a valid condition key'); requires(condition, 'undefined is not a valid condition'); requires(value, 'undefined is not a valid condition value'); // Check if we already have a condition if (!this._where[key]) { this._where[key] = {}; } this._where[key][condition] = AV._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 {AV.Query} Returns the query, so you can chain this call. */ sizeEqualTo: function sizeEqualTo(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 {AV.Query} Returns the query, so you can chain this call. */ notEqualTo: function notEqualTo(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 {AV.Query} Returns the query, so you can chain this call. */ lessThan: function lessThan(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 {AV.Query} Returns the query, so you can chain this call. */ greaterThan: function greaterThan(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 {AV.Query} Returns the query, so you can chain this call. */ lessThanOrEqualTo: function lessThanOrEqualTo(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 {AV.Query} Returns the query, so you can chain this call. */ greaterThanOrEqualTo: function greaterThanOrEqualTo(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 {AV.Query} Returns the query, so you can chain this call. */ containedIn: function containedIn(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 {AV.Query} Returns the query, so you can chain this call. */ notContainedIn: function notContainedIn(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 {AV.Query} Returns the query, so you can chain this call. */ containsAll: function containsAll(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 {AV.Query} Returns the query, so you can chain this call. */ exists: function exists(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 {AV.Query} Returns the query, so you can chain this call. */ doesNotExist: function doesNotExist(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 {AV.Query} Returns the query, so you can chain this call. */ matches: function matches(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 AV.Query * constraint. * @param {String} key The key that the contains the object to match the * query. * @param {AV.Query} query The query that should match. * @return {AV.Query} Returns the query, so you can chain this call. */ matchesQuery: function matchesQuery(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 * AV.Query constraint. * @param {String} key The key that the contains the object to match the * query. * @param {AV.Query} query The query that should not match. * @return {AV.Query} Returns the query, so you can chain this call. */ doesNotMatchQuery: function doesNotMatchQuery(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 AV.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 {AV.Query} query The query to run. * @return {AV.Query} Returns the query, so you can chain this call. */ matchesKeyInQuery: function matchesKeyInQuery(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 AV.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 {AV.Query} query The query to run. * @return {AV.Query} Returns the query, so you can chain this call. */ doesNotMatchKeyInQuery: function doesNotMatchKeyInQuery(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 {AV.Query} Returns the query, so you can chain this call. * @private */ _orQuery: function _orQuery(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 {AV.Query} Returns the query, so you can chain this call. * @private */ _andQuery: function _andQuery(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. * @private */ _quote: function _quote(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 {AV.Query} Returns the query, so you can chain this call. */ contains: function contains(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 {AV.Query} Returns the query, so you can chain this call. */ startsWith: function startsWith(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 {AV.Query} Returns the query, so you can chain this call. */ endsWith: function endsWith(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 {AV.Query} Returns the query, so you can chain this call. */ ascending: function ascending(key) { requires(key, 'undefined is not a valid 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 {AV.Query} Returns the query so you can chain this call. */ addAscending: function addAscending(key) { requires(key, 'undefined is not a valid 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 {AV.Query} Returns the query, so you can chain this call. */ descending: function descending(key) { requires(key, 'undefined is not a valid 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 {AV.Query} Returns the query so you can chain this call. */ addDescending: function addDescending(key) { requires(key, 'undefined is not a valid key'); if (this._order) this._order += ',-' + key;else this._order = '-' + key; return this; }, /** * Add a proximity based constraint for finding objects with key point * values near the point given. * @param {String} key The key that the AV.GeoPoint is stored in. * @param {AV.GeoPoint} point The reference AV.GeoPoint that is used. * @return {AV.Query} Returns the query, so you can chain this call. */ near: function near(key, point) { if (!(point instanceof AV.GeoPoint)) { // Try to cast it to a GeoPoint, so that near("loc", [20,30]) works. point = new AV.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 AV.GeoPoint is stored in. * @param {AV.GeoPoint} point The reference AV.GeoPoint that is used. * @param maxDistance Maximum distance (in radians) of results to return. * @return {AV.Query} Returns the query, so you can chain this call. */ withinRadians: function withinRadians(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 AV.GeoPoint is stored in. * @param {AV.GeoPoint} point The reference AV.GeoPoint that is used. * @param {Number} maxDistance Maximum distance (in miles) of results to * return. * @return {AV.Query} Returns the query, so you can chain this call. */ withinMiles: function withinMiles(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 AV.GeoPoint is stored in. * @param {AV.GeoPoint} point The reference AV.GeoPoint that is used. * @param {Number} maxDistance Maximum distance (in kilometers) of results * to return. * @return {AV.Query} Returns the query, so you can chain this call. */ withinKilometers: function withinKilometers(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 {AV.GeoPoint} southwest * The lower-left inclusive corner of the box. * @param {AV.GeoPoint} northeast * The upper-right inclusive corner of the box. * @return {AV.Query} Returns the query, so you can chain this call. */ withinGeoBox: function withinGeoBox(key, southwest, northeast) { if (!(southwest instanceof AV.GeoPoint)) { southwest = new AV.GeoPoint(southwest); } if (!(northeast instanceof AV.GeoPoint)) { northeast = new AV.GeoPoint(northeast); } this._addCondition(key, '$within', { '$box': [southwest, northeast] }); return this; }, /** * Include nested AV.Objects for the provided key. You can use dot * notation to specify which fields in the included object are also fetch. * @param {String[]} keys The name of the key to include. * @return {AV.Query} Returns the query, so you can chain this call. */ include: function include(keys) { var _this = this; requires(keys, 'undefined is not a valid key'); _(arguments).forEach(function (keys) { _this._include = _this._include.concat(ensureArray(keys)); }); return this; }, /** * Restrict the fields of the returned AV.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 {String[]} keys The names of the keys to include. * @return {AV.Query} Returns the query, so you can chain this call. */ select: function select(keys) { var _this2 = this; requires(keys, 'undefined is not a valid key'); _(arguments).forEach(function (keys) { _this2._select = _this2._select.concat(ensureArray(keys)); }); 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. * @return {Promise} A promise that will be fulfilled once the * iteration has completed. */ each: function each(callback) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (this._order || this._skip || this._limit >= 0) { var error = new Error("Cannot iterate on a query with sort, skip, or limit."); return AV.Promise.reject(error); } var query = new AV.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 AV.Promise._continueWhile(function () { return !finished; }, function () { return query.find(options).then(function (results) { var callbacksDone = AV.Promise.resolve(); _.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; } }); }); }); } }; AV.FriendShipQuery = AV.Query._extend({ _objectClass: AV.User, _newObject: function _newObject() { return new AV.User(); }, _processResult: function _processResult(json) { if (json && json[this._friendshipTag]) { var user = json[this._friendshipTag]; if (user.__type === 'Pointer' && user.className === '_User') { delete user.__type; delete user.className; } return user; } else { return null; } } }); };