jmdobry/reheat

View on GitHub
lib/collection/static/findAll.js

Summary

Maintainability
F
1 wk
Test Coverage
/* jshint loopfunc:true */
module.exports = function (container, Promise, utils, errors) {

  var errorPrefix = 'Collection.findAll(predicate[, options][, cb]): ';
  var IllegalArgumentError = errors.IllegalArgumentError;
  var RuntimeError = errors.RuntimeError;
  var UnhandledError = errors.UnhandledError;

  function _findAll(predicate, options) {
    var Collection = this;
    var models = container.get('models');
    var Model;

    if (this.model) {
      Model = this.model;
    } else if (this.collection) {
      Model = this;
      Collection = Model.collection;
    }

    var query = Model.r.table(Model.tableName);

    return Promise.resolve().bind(this).then(function () {

      if (!('withDeleted' in options)) {
        options.withDeleted = false;
      }

      if (!utils.isEmpty(predicate.where)) {
        query = query.filter(predicate.where);
      }

      if (Model.softDelete && !options.withDeleted) {
        query = query.filter({ deleted: null });
      }

      var newModels = {},
        merge = {};

      if (options.with && options.with.length) {
        query = query.map(function (doc) {
          if (Model.relations.belongsTo) {
            utils.forOwn(Model.relations.belongsTo, function (relation, modelName) {
              if (!models[modelName]) {
                throw new RuntimeError(Model.name + ' Model defined belongsTo relationship to nonexistent ' + modelName + ' Model!');
              } else if (utils.contains(options.with, modelName)) {
                var localField = relation.localField,
                  localKey = relation.localKey;

                merge[localField] = Model.r.table(models[modelName].tableName).get(doc(localKey).default(''));

                newModels[localField] = {
                  modelName: modelName,
                  relation: 'belongsTo'
                };
              }
            });
          }

          if (Model.relations.hasMany) {
            utils.forOwn(Model.relations.hasMany, function (relation, modelName) {
              if (!models[modelName]) {
                throw new RuntimeError(Model.name + ' Model defined hasMany relationship to nonexistent ' + modelName + ' Model!');
              } else if (utils.contains(options.with, modelName)) {
                var localField = relation.localField,
                  foreignKey = relation.foreignKey;

                merge[localField] = Model.r.table(models[modelName].tableName).getAll(doc(Model.idAttribute).default(''), { index: foreignKey }).coerceTo('ARRAY');

                newModels[localField] = {
                  modelName: modelName,
                  relation: 'hasMany'
                };
              }
            });
          }

          if (Model.relations.hasOne) {
            utils.forOwn(Model.relations.hasOne, function (relation, modelName) {
              if (!models[modelName]) {
                throw new RuntimeError(Model.name + ' Model defined hasOne relationship to nonexistent ' + modelName + ' Model!');
              } else if (utils.contains(options.with, modelName)) {
                var localField = relation.localField;

                merge[localField] = Model.r.table(models[modelName].tableName);

                if (relation.localKey) {
                  merge[localField] = merge[localField].get(relation.localKey);
                } else {
                  var foreignKey = relation.foreignKey;
                  merge[localField] = merge[localField].getAll(doc(Model.idAttribute).default(''), { index: foreignKey }).coerceTo('ARRAY');
                }

                newModels[localField] = {
                  modelName: modelName,
                  relation: 'hasOne'
                };
              }
            });
          }

          return doc.merge(merge);
        });
      }

      if (predicate.orderBy) {
        if (utils.isString(predicate.orderBy)) {
          predicate.orderBy = [
            [predicate.orderBy, 'asc']
          ];
        }
        for (var i = 0; i < predicate.orderBy.length; i++) {
          if (utils.isString(predicate.orderBy[i])) {
            predicate.orderBy[i] = [predicate.orderBy[i], 'asc'];
          }
          query = utils.upperCase(predicate.orderBy[i][1]) === 'DESC' ? query.orderBy(Model.r.desc(predicate.orderBy[i][0])) : query.orderBy(predicate.orderBy[i][0]);
        }
      }

      if (predicate.skip) {
        query = query.skip(predicate.skip);
      }

      if (predicate.limit) {
        query = query.limit(predicate.limit);
      }

      if (predicate.pluck) {
        if (utils.isString(predicate.pluck)) {
          query = query.pluck(predicate.pluck);
        } else {
          query = query.pluck.apply(query, predicate.pluck);
        }
      }

      var profile;

      return Model.connection.run(query, options)
        .then(function (cursor) {
          if (options.profile) {
            profile = cursor.profile;
            cursor = cursor.value;
          }
          if (cursor && typeof cursor.toArray === 'function') {
            return cursor.toArray();
          } else {
            return cursor;
          }
        })
        .then(function (documents) {
          if (!options.raw) {
            var length = documents.length;
            for (var i = 0; i < length; i++) {
              var doc = documents[i];
              utils.forOwn(doc, function (localValue, localKey) {
                if (localKey in newModels) {
                  if (utils.isObject(localValue)) {
                    doc[localKey] = new models[newModels[localKey].modelName](doc[localKey]);
                  } else if (utils.isArray(localValue)) {
                    if (newModels[localKey].relation === 'hasOne' && localValue.length) {
                      doc[localKey] = new models[newModels[localKey].modelName](localValue[0]);
                    } else {
                      doc[localKey] = new models[newModels[localKey].modelName].collection(localValue);
                    }
                  }
                }
              });
              documents[i] = new Model(documents[i]);
            }
          }
          var collection = new Collection(documents);
          if (options.profile) {
            collection.queries = [profile];
          }
          return collection;
        })
        .finally(function () {
          newModels = models = merge = null;
        });
    });
  }

  /**
   * @doc method
   * @id Collection.static_methods:findAll
   * @name findAll
   * @description
   * Filter `Collection.model.tableName` by the given predicate. Return a collection of instances of Colleciton.model or
   * the raw data if `options.raw === true`.
   *
   * See [r#filter](http://rethinkdb.com/api/javascript/#filter).
   *
   * ## Signature:
   * ```js
   * Collection.findAll(predicate[, options][, cb])
   * ```
   *
   * ## Examples:
   *
   * ### Promise-style:
   * ```js
   *  Posts.findAll({
     *      where: {
     *          author: 'John Anderson'
     *      }
     *  }).then(function (posts) {
     *      res.send(200, posts.toJSON());
     *  })
   *  .catch(reheat.support.IllegalArgumentError, function (err) {
     *      res.send(400, err.errors);
     *  })
   *  .catch(reheat.support.UnhandledError, function (err) {
     *      res.send(500, err.message);
     *  })
   *  .error(function (err) {
     *      res.send(500, err.message);
     *  });
   * ```
   *
   * ### Node-style:
   * ```js
   *  Posts.findAll({
     *      where: {
     *          author: 'John Anderson'
     *      }
     *  }, function (err, posts) {
     *      if (err) {
     *          if (err instanceof reheat.support.IllegalArgumentError) {
     *              res.send(400, err.errors);
     *          } else {
     *              res.send(500, err.message);
     *          }
     *      } else {
     *          res.send(200, posts.toJSON());
     *      }
     *  });
   * ```
   *
   * ## Throws/Rejects with
   *
   * - `{IllegalArgumentError}`
   * - `{UnhandledError}`
   *
   * @param {object} predicate Filtering criteria. Properties:
   *
   * - `{object=}` - `where` - Where clause.
   * - `{string|array.<string>=}` - `orderBy` - OrderBy clause.
   * - `{number=}` - `limit` - Limit clause.
   * - `{number=}` - `skip` - Skip clause.
   * - `{string|array=}` - `pluck` - Pluck clause. Only applies if `options.raw` === true.
   *
   * @param {object=} options Optional configuration. Properties:
   *
   * - `{boolean=false}` - `raw`- If `true`, return the raw data instead of instances of Model.
   * - `{boolean=false}` - `withDeleted`- If `true`, return "softDeleted" items as well.
   * - `{boolean=false}` - `profile`- If `true` the query profile will be appended to `instance.queries`.
   * - `{array=[]}` - `with`- Array of strings corresponding to the Model names of relations to retrieve and merge into
   * the result.
   *
   * @param {function=} cb Optional callback function for Node-style usage. Signature: `cb(err, collection)`. Arguments:
   *
   * - `{IllegalArgumentError|UnhandledError}` - `err` - `null` if no error occurs.
   * - `{Collection}` - `collection` - If no error occurs, an instance of this Collection filled with instances of this
   * Collection's Model.
   * @returns {Promise} Promise.
   */
  function findAll(predicate, options, cb) {
    if (utils.isFunction(options)) {
      cb = options;
      options = {};
    }
    options = options || {};

    if (cb && !utils.isFunction(cb)) {
      throw new IllegalArgumentError(errorPrefix + 'cb: Must be a function!', { actual: typeof cb, expected: 'function' });
    }

    return Promise.resolve().bind(this).then(function sanitize() {

      if (!utils.isObject(predicate)) {
        throw new IllegalArgumentError(errorPrefix + 'predicate: Must be an object!', { actual: typeof predicate, expected: 'object' });
      } else if (!utils.isObject(options)) {
        throw new IllegalArgumentError(errorPrefix + 'options: Must be an object!', { actual: typeof options, expected: 'object' });
      }

      predicate.where = predicate.where || {};

      if (utils.isString(predicate.where)) {
        try {
          predicate.where = JSON.parse(predicate.where);
        } catch (err) {
          throw new UnhandledError(err);
        }
      } else if (!utils.isObject(predicate.where)) {
        throw new IllegalArgumentError(errorPrefix + 'predicate.where: Must be a string or an object!', { where: { actual: typeof predicate.where, expected: 'string|object' } });
      }

      if (predicate.limit) {
        var limit = parseInt(predicate.limit, 10);
        if (isNaN(limit)) {
          throw new IllegalArgumentError(errorPrefix + 'predicate.limit: Must be a number!', { limit: { actual: typeof predicate.limit, expected: 'number' } });
        } else {
          predicate.limit = limit;
        }
      }

      if (predicate.skip) {
        var skip = parseInt(predicate.skip, 10);
        if (isNaN(skip)) {
          throw new IllegalArgumentError(errorPrefix + 'predicate.skip: Must be a number!', { skip: { actual: typeof predicate.skip, expected: 'number' } });
        } else {
          predicate.skip = skip;
        }
      }

      if (predicate.pluck && options.raw) {
        if (!utils.isString(predicate.pluck) && !utils.isArray(predicate.pluck)) {
          throw new IllegalArgumentError(errorPrefix + 'predicate.pluck: Must be a string or an array!', { pluck: { actual: typeof predicate.pluck, expected: 'string|array' } });
        }
      } else {
        delete predicate.pluck;
      }

      return _findAll.apply(this, [predicate, options, cb]);
    }).nodeify(cb);
  }

  return findAll;
};