search.js

const _ = require('underscore');
const AVRequest = require('./request')._request;

module.exports = function(AV) {
  /**
   * A builder to generate sort string for app searching.For example:
   * @class
   * @since 0.5.1
   * @example
   *   var builder = new AV.SearchSortBuilder();
   *   builder.ascending('key1').descending('key2','max');
   *   var query = new AV.SearchQuery('Player');
   *   query.sortBy(builder);
   *   query.find().then();
   */
  AV.SearchSortBuilder = function() {
    this._sortFields = [];
  };

  _.extend(
    AV.SearchSortBuilder.prototype,
    /** @lends AV.SearchSortBuilder.prototype */ {
      _addField: function(key, order, mode, missing) {
        var field = {};
        field[key] = {
          order: order || 'asc',
          mode: mode || 'avg',
          missing: '_' + (missing || 'last'),
        };
        this._sortFields.push(field);
        return this;
      },

      /**
       * Sorts the results in ascending order by the given key and options.
       *
       * @param {String} key The key to order by.
       * @param {String} mode The sort mode, default is 'avg', you can choose
       *                  'max' or 'min' too.
       * @param {String} missing The missing key behaviour, default is 'last',
       *                  you can choose 'first' too.
       * @return {AV.SearchSortBuilder} Returns the builder, so you can chain this call.
       */
      ascending: function(key, mode, missing) {
        return this._addField(key, 'asc', mode, missing);
      },

      /**
       * Sorts the results in descending order by the given key and options.
       *
       * @param {String} key The key to order by.
       * @param {String} mode The sort mode, default is 'avg', you can choose
       *                  'max' or 'min' too.
       * @param {String} missing The missing key behaviour, default is 'last',
       *                  you can choose 'first' too.
       * @return {AV.SearchSortBuilder} Returns the builder, so you can chain this call.
       */
      descending: function(key, mode, missing) {
        return this._addField(key, 'desc', mode, missing);
      },

      /**
       * 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.
       * @param {Object} options The other options such as mode,order, unit etc.
       * @return {AV.SearchSortBuilder} Returns the builder, so you can chain this call.
       */
      whereNear: function(key, point, options) {
        options = options || {};
        var field = {};
        var geo = {
          lat: point.latitude,
          lon: point.longitude,
        };
        var m = {
          order: options.order || 'asc',
          mode: options.mode || 'avg',
          unit: options.unit || 'km',
        };
        m[key] = geo;
        field['_geo_distance'] = m;

        this._sortFields.push(field);
        return this;
      },

      /**
       * Build a sort string by configuration.
       * @return {String} the sort string.
       */
      build: function() {
        return JSON.stringify(AV._encode(this._sortFields));
      },
    }
  );

  /**
   * App searching query.Use just like AV.Query:
   *
   * Visit <a href='https://leancloud.cn/docs/app_search_guide.html'>App Searching Guide</a>
   * for more details.
   * @class
   * @since 0.5.1
   * @example
   *   var query = new AV.SearchQuery('Player');
   *   query.queryString('*');
   *   query.find().then(function(results) {
   *     console.log('Found %d objects', query.hits());
   *     //Process results
   *   });
   */
  AV.SearchQuery = AV.Query._extend(
    /** @lends AV.SearchQuery.prototype */ {
      _sid: null,
      _hits: 0,
      _queryString: null,
      _highlights: null,
      _sortBuilder: null,
      _clazz: null,

      constructor: function(className) {
        if (className) {
          this._clazz = className;
        } else {
          className = '__INVALID_CLASS';
        }
        AV.Query.call(this, className);
      },

      _createRequest: function(params, options) {
        return AVRequest(
          'search/select',
          null,
          null,
          'GET',
          params || this._getParams(),
          options
        );
      },

      /**
       * Sets the sid of app searching query.Default is null.
       * @param {String} sid  Scroll id for searching.
       * @return {AV.SearchQuery} Returns the query, so you can chain this call.
       */
      sid: function(sid) {
        this._sid = sid;
        return this;
      },

      /**
       * Sets the query string of app searching.
       * @param {String} q  The query string.
       * @return {AV.SearchQuery} Returns the query, so you can chain this call.
       */
      queryString: function(q) {
        this._queryString = q;
        return this;
      },

      /**
       * Sets the highlight fields. Such as
       * <pre><code>
       *   query.highlights('title');
       *   //or pass an array.
       *   query.highlights(['title', 'content'])
       * </code></pre>
       * @param {String|String[]} highlights a list of fields.
       * @return {AV.SearchQuery} Returns the query, so you can chain this call.
       */
      highlights: function(highlights) {
        var objects;
        if (highlights && _.isString(highlights)) {
          objects = _.toArray(arguments);
        } else {
          objects = highlights;
        }
        this._highlights = objects;
        return this;
      },

      /**
       * Sets the sort builder for this query.
       * @see AV.SearchSortBuilder
       * @param { AV.SearchSortBuilder} builder The sort builder.
       * @return {AV.SearchQuery} Returns the query, so you can chain this call.
       *
       */
      sortBy: function(builder) {
        this._sortBuilder = builder;
        return this;
      },

      /**
       * Returns the number of objects that match this query.
       * @return {Number}
       */
      hits: function() {
        if (!this._hits) {
          this._hits = 0;
        }
        return this._hits;
      },

      _processResult: function(json) {
        delete json['className'];
        delete json['_app_url'];
        delete json['_deeplink'];
        return json;
      },

      /**
       * Returns true when there are more documents can be retrieved by this
       * query instance, you can call find function to get more results.
       * @see AV.SearchQuery#find
       * @return {Boolean}
       */
      hasMore: function() {
        return !this._hitEnd;
      },

      /**
       * Reset current query instance state(such as sid, hits etc) except params
       * for a new searching. After resetting, hasMore() will return true.
       */
      reset: function() {
        this._hitEnd = false;
        this._sid = null;
        this._hits = 0;
      },

      /**
       * Retrieves a list of AVObjects that satisfy this query.
       * Either options.success or options.error is called when the find
       * completes.
       *
       * @see AV.Query#find
       * @param {AuthOptions} options
       * @return {Promise} A promise that is resolved with the results when
       * the query completes.
       */
      find: function(options) {
        var self = this;

        var request = this._createRequest(undefined, options);

        return request.then(function(response) {
          //update sid for next querying.
          if (response.sid) {
            self._oldSid = self._sid;
            self._sid = response.sid;
          } else {
            self._sid = null;
            self._hitEnd = true;
          }
          self._hits = response.hits || 0;

          return _.map(response.results, function(json) {
            if (json.className) {
              response.className = json.className;
            }
            var obj = self._newObject(response);
            obj.appURL = json['_app_url'];
            obj._finishFetch(self._processResult(json), true);
            return obj;
          });
        });
      },

      _getParams: function() {
        var params = AV.SearchQuery.__super__._getParams.call(this);
        delete params.where;
        if (this._clazz) {
          params.clazz = this.className;
        }
        if (this._sid) {
          params.sid = this._sid;
        }
        if (!this._queryString) {
          throw new Error('Please set query string.');
        } else {
          params.q = this._queryString;
        }
        if (this._highlights) {
          params.highlights = this._highlights.join(',');
        }
        if (this._sortBuilder && params.order) {
          throw new Error('sort and order can not be set at same time.');
        }
        if (this._sortBuilder) {
          params.sort = this._sortBuilder.build();
        }

        return params;
      },
    }
  );
};

/**
 * Sorts the results in ascending order by the given key.
 *
 * @method AV.SearchQuery#ascending
 * @param {String} key The key to order by.
 * @return {AV.SearchQuery} Returns the query, so you can chain this call.
 */
/**
 * Also sorts the results in ascending order by the given key. The previous sort keys have
 * precedence over this key.
 *
 * @method AV.SearchQuery#addAscending
 * @param {String} key The key to order by
 * @return {AV.SearchQuery} Returns the query so you can chain this call.
 */
/**
 * Sorts the results in descending order by the given key.
 *
 * @method AV.SearchQuery#descending
 * @param {String} key The key to order by.
 * @return {AV.SearchQuery} Returns the query, so you can chain this call.
 */
/**
 * Also sorts the results in descending order by the given key. The previous sort keys have
 * precedence over this key.
 *
 * @method AV.SearchQuery#addDescending
 * @param {String} key The key to order by
 * @return {AV.SearchQuery} Returns the query so you can chain this call.
 */
/**
 * Include nested AV.Objects for the provided key.  You can use dot
 * notation to specify which fields in the included object are also fetch.
 * @method AV.SearchQuery#include
 * @param {String[]} keys The name of the key to include.
 * @return {AV.SearchQuery} Returns the query, so you can chain this call.
 */
/**
 * Sets the number of results to skip before returning any results.
 * This is useful for pagination.
 * Default is to skip zero results.
 * @method AV.SearchQuery#skip
 * @param {Number} n the number of results to skip.
 * @return {AV.SearchQuery} Returns the query, so you can chain this call.
 */
/**
 * 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.
 * @method AV.SearchQuery#limit
 * @param {Number} n the number of results to limit to.
 * @return {AV.SearchQuery} Returns the query, so you can chain this call.
 */