query.js

const _ = require('underscore');
const debug = require('debug')('leancloud:query');
const AVError = require('./error');
const { _request, request } = require('./request');
const {
  ensureArray,
  transformFetchOptions,
  continueWhile,
} = require('./utils');

const 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._defaultParams = {};
  };

  /**
   * 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 new Error('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 new Error('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 = _request('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,
      };
    });
  };

  /**
   * Return a query with conditions from json.
   * This can be useful to send a query from server side to client side.
   * @since 4.0.0
   * @param {Object} json from {@link AV.Query#toJSON}
   * @return {AV.Query}
   */
  AV.Query.fromJSON = ({
    className,
    where,
    include,
    select,
    includeACL,
    limit,
    skip,
    order,
  }) => {
    if (typeof className !== 'string') {
      throw new TypeError('Invalid Query JSON, className must be a String.');
    }
    const query = new AV.Query(className);
    _.extend(query, {
      _where: where,
      _include: include,
      _select: select,
      _includeACL: includeACL,
      _limit: limit,
      _skip: skip,
      _order: order,
    });
    return query;
  };

  AV.Query._extend = AV._extend;

  _.extend(
    AV.Query.prototype,
    /** @lends AV.Query.prototype */ {
      //hook to iterate result. Added by dennis<xzhuang@avoscloud.com>.
      _processResult: function(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(objectId, options) {
        if (!_.isString(objectId)) {
          throw new Error('objectId must be a string');
        }
        if (objectId === '') {
          return Promise.reject(
            new AVError(AVError.OBJECT_NOT_FOUND, 'Object not found.')
          );
        }

        var obj = this._newObject();
        obj.id = objectId;

        var queryJSON = this._getParams();
        var fetchOptions = {};

        if (queryJSON.keys) fetchOptions.keys = queryJSON.keys;
        if (queryJSON.include) fetchOptions.include = queryJSON.include;
        if (queryJSON.includeACL)
          fetchOptions.includeACL = queryJSON.includeACL;

        return _request(
          'classes',
          this.className,
          objectId,
          'GET',
          transformFetchOptions(fetchOptions),
          options
        ).then(response => {
          if (_.isEmpty(response))
            throw new AVError(AVError.OBJECT_NOT_FOUND, 'Object not found.');
          obj._finishFetch(obj.parse(response), true);
          return obj;
        });
      },

      /**
       * Returns a JSON representation of this query.
       * @return {Object}
       */
      toJSON() {
        const {
          className,
          _where: where,
          _include: include,
          _select: select,
          _includeACL: includeACL,
          _limit: limit,
          _skip: skip,
          _order: order,
        } = this;
        return {
          className,
          where,
          include,
          select,
          includeACL,
          limit,
          skip,
          order,
        };
      },

      _getParams: function() {
        var params = _.extend({}, this._defaultParams, {
          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._includeACL !== undefined) {
          params.returnACL = this._includeACL;
        }
        if (this._limit >= 0) {
          params.limit = this._limit;
        }
        if (this._skip > 0) {
          params.skip = this._skip;
        }
        if (this._order !== undefined) {
          params.order = this._order;
        }

        return params;
      },

      _newObject: function(response) {
        var obj;
        if (response && response.className) {
          obj = new AV.Object(response.className);
        } else {
          obj = new this.objectClass();
        }
        return obj;
      },
      _createRequest(
        params = this._getParams(),
        options,
        path = `/classes/${this.className}`
      ) {
        if (encodeURIComponent(JSON.stringify(params)).length > 2000) {
          const body = {
            requests: [
              {
                method: 'GET',
                path: `/1.1${path}`,
                params,
              },
            ],
          };
          return request({
            path: '/batch',
            method: 'POST',
            data: body,
            authOptions: options,
          }).then(response => {
            const result = response[0];
            if (result.success) {
              return result.success;
            }
            const error = new AVError(
              result.error.code,
              result.error.error || 'Unknown batch error'
            );
            throw error;
          });
        }
        return request({
          method: 'GET',
          path,
          query: params,
          authOptions: options,
        });
      },

      _parseResponse(response) {
        return _.map(response.results, json => {
          var obj = this._newObject(response);
          if (obj._finishFetch) {
            obj._finishFetch(this._processResult(json), true);
          }
          return obj;
        });
      },

      /**
       * 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(options) {
        const request = this._createRequest(undefined, options);
        return request.then(this._parseResponse.bind(this));
      },

      /**
       * Retrieves both AVObjects and total count.
       *
       * @since 4.12.0
       * @param {AuthOptions} options
       * @return {Promise} A tuple contains results and count.
       */
      findAndCount(options) {
        const params = this._getParams();
        params.count = 1;
        const request = this._createRequest(params, options);

        return request.then(response => [
          this._parseResponse(response),
          response.count,
        ]);
      },

      /**
       * scan a Query. masterKey required.
       *
       * @since 2.1.0
       * @param {object} [options]
       * @param {string} [options.orderedBy] specify the key to sort
       * @param {number} [options.batchSize] specify the batch size for each request
       * @param {AuthOptions} [authOptions]
       * @return {AsyncIterator.<AV.Object>}
       * @example const testIterator = {
       *   [Symbol.asyncIterator]() {
       *     return new Query('Test').scan(undefined, { useMasterKey: true });
       *   },
       * };
       * for await (const test of testIterator) {
       *   console.log(test.id);
       * }
       */
      scan({ orderedBy, batchSize } = {}, authOptions) {
        const condition = this._getParams();
        debug('scan %O', condition);
        if (condition.order) {
          console.warn(
            'The order of the query is ignored for Query#scan. Checkout the orderedBy option of Query#scan.'
          );
          delete condition.order;
        }
        if (condition.skip) {
          console.warn(
            'The skip option of the query is ignored for Query#scan.'
          );
          delete condition.skip;
        }
        if (condition.limit) {
          console.warn(
            'The limit option of the query is ignored for Query#scan.'
          );
          delete condition.limit;
        }
        if (orderedBy) condition.scan_key = orderedBy;
        if (batchSize) condition.limit = batchSize;
        let cursor;
        let remainResults = [];
        return {
          next: () => {
            if (remainResults.length) {
              return Promise.resolve({
                done: false,
                value: remainResults.shift(),
              });
            }
            if (cursor === null) {
              return Promise.resolve({ done: true });
            }
            return _request(
              'scan/classes',
              this.className,
              null,
              'GET',
              cursor ? _.extend({}, condition, { cursor }) : condition,
              authOptions
            ).then(response => {
              cursor = response.cursor;
              if (response.results.length) {
                const results = this._parseResponse(response);
                results.forEach(result => remainResults.push(result));
              }
              if (cursor === null && remainResults.length === 0) {
                return { done: true };
              }
              return {
                done: false,
                value: remainResults.shift(),
              };
            });
          },
        };
      },

      /**
       * Delete objects retrieved by this query.
       * @param {AuthOptions} options
       * @return {Promise} A promise that is fulfilled when the save
       *     completes.
       */
      destroyAll: function(options) {
        var self = this;
        return self.find(options).then(function(objects) {
          return AV.Object.destroyAll(objects, options);
        });
      },

      /**
       * 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(options) {
        var params = this._getParams();
        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(options) {
        var self = this;

        var params = this._getParams();
        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(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(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(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(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 {number} value The length value.
       * @return {AV.Query} Returns the query, so you can chain this call.
       */
      sizeEqualTo: function(key, value) {
        this._addCondition(key, '$size', value);
        return this;
      },

      /**
       * 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(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(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(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(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(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(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(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(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(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(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(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(key, query) {
        var queryJSON = query._getParams();
        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(key, query) {
        var queryJSON = query._getParams();
        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(key, queryKey, query) {
        var queryJSON = query._getParams();
        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(key, queryKey, query) {
        var queryJSON = query._getParams();
        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(queries) {
        var queryJSON = _.map(queries, function(q) {
          return q._getParams().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(queries) {
        var queryJSON = _.map(queries, function(q) {
          return q._getParams().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(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(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(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(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(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(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(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(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(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(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(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(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(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(keys) {
        requires(keys, 'undefined is not a valid key');
        _.forEach(arguments, keys => {
          this._include = this._include.concat(ensureArray(keys));
        });
        return this;
      },

      /**
       * Include the ACL.
       * @param {Boolean} [value=true] Whether to include the ACL
       * @return {AV.Query} Returns the query, so you can chain this call.
       */
      includeACL: function(value = true) {
        this._includeACL = value;
        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(keys) {
        requires(keys, 'undefined is not a valid key');
        _.forEach(arguments, keys => {
          this._select = this._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(callback, options = {}) {
        if (this._order || this._skip || this._limit >= 0) {
          var error = new Error(
            'Cannot iterate on a query with sort, skip, or limit.'
          );
          return 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 continueWhile(
          function() {
            return !finished;
          },
          function() {
            return query.find(options).then(function(results) {
              var callbacksDone = 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;
                }
              });
            });
          }
        );
      },

      /**
       * Subscribe the changes of this query.
       *
       * LiveQuery is not included in the default bundle: {@link https://url.leanapp.cn/enable-live-query}.
       *
       * @since 3.0.0
       * @return {AV.LiveQuery} An eventemitter which can be used to get LiveQuery updates;
       */
      subscribe(options) {
        return AV.LiveQuery.init(this, options);
      },
    }
  );

  AV.FriendShipQuery = AV.Query._extend({
    _newObject: function() {
      const UserClass = AV.Object._getSubclass('_User');
      return new UserClass();
    },
    _processResult: function(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;
      }
    },
  });
};

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy