Code coverage report for bookshelf/lib/sync.js

Statements: 33.33% (23 / 69)      Branches: 15.63% (5 / 32)      Functions: 17.65% (3 / 17)      Lines: 36.51% (23 / 63)      Ignored: none     

All files » bookshelf/lib/ » sync.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196        1 1             1 2 2 2 2 2 2     1         2 2 2 4   2           1                   1   1     1 1       1   1                                                                                                                                                                                                                                                                               1
// Sync
// ---------------
'use strict';
 
var _ = require('lodash');
var Promise = require('./base/promise');
 
// Sync is the dispatcher for any database queries,
// taking the "syncing" `model` or `collection` being queried, along with
// a hash of options that are used in the various query methods.
// If the `transacting` option is set, the query is assumed to be
// part of a transaction, and this information is passed along to `Knex`.
var Sync = function Sync(syncing, options) {
  options = options || {};
  this.query = syncing.query();
  this.syncing = syncing.resetQuery();
  this.options = options;
  Iif (options.debug) this.query.debug();
  Iif (options.transacting) this.query.transacting(options.transacting);
};
 
_.extend(Sync.prototype, {
 
  // Prefix all keys of the passed in object with the
  // current table name
  prefixFields: function prefixFields(fields) {
    var tableName = this.syncing.tableName;
    var prefixed = {};
    for (var key in fields) {
      prefixed[tableName + '.' + key] = fields[key];
    }
    return prefixed;
  },
 
  // Select the first item from the database - only used by models.
  first: Promise.method(function (attributes) {
 
    var model = this.syncing,
        query = this.query,
        whereAttributes,
        formatted;
 
    // We'll never use an JSON object for a search, because even
    // PostgreSQL, which has JSON type columns, does not support the `=`
    // operator.
    //
    // NOTE: `_.omit` returns an empty object, even if attributes are null.
    whereAttributes = _.omit(attributes, _.isPlainObject);
 
    Eif (!_.isEmpty(whereAttributes)) {
 
      // Format and prefix attributes.
      formatted = this.prefixFields(model.format(whereAttributes));
      query.where(formatted);
    }
 
    // Limit to a single result.
    query.limit(1);
 
    return this.select();
  }),
 
  // Add relational constraints required for either a `count` or `select` query.
  constrain: Promise.method(function () {
    var knex = this.query,
        options = this.options,
        relatedData = this.syncing.relatedData,
        fks = {},
        through;
 
    // Set the query builder on the options, in-case we need to
    // access in the `fetching` event handlers.
    options.query = knex;
 
    // Inject all appropriate select costraints dealing with the relation
    // into the `knex` query builder for the current instance.
    if (relatedData) return Promise['try'](function () {
      if (relatedData.isThrough()) {
        fks[relatedData.key('foreignKey')] = relatedData.parentFk;
        through = new relatedData.throughTarget(fks);
 
        /**
         * Fired before a `fetch` operation. A promise may be returned from the
         * event handler for async behaviour.
         *
         * @event Model#fetching
         * @param   {Model}    model      The model that has been fetched.
         * @param   {string[]} columns    The columns being retrieved by the query.
         * @param   {Object}   options    Options object passed to {@link Model#fetch fetch}.
         * @returns {Promise}
         */
        return through.triggerThen('fetching', through, relatedData.pivotColumns, options).then(function () {
          relatedData.pivotColumns = through.parse(relatedData.pivotColumns);
        });
      }
    });
  }),
 
  // Runs a `count` query on the database, adding any necessary relational
  // constraints. Returns a promise that resolves to an integer count.
  count: Promise.method(function (column) {
    var knex = this.query,
        options = this.options;
 
    return Promise.bind(this).then(function () {
      return this.constrain();
    }).then(function () {
      return this.syncing.triggerThen('counting', this.syncing, options);
    }).then(function () {
      return knex.count((column || '*') + ' as count');
    }).then(function (rows) {
      return rows[0].count;
    });
  }),
 
  // Runs a `select` query on the database, adding any necessary relational
  // constraints, resetting the query when complete. If there are results and
  // eager loaded relations, those are fetched and returned on the model before
  // the promise is resolved. Any `success` handler passed in the
  // options will be called - used by both models & collections.
  select: Promise.method(function () {
    var _this = this;
 
    var knex = this.query,
        options = this.options,
        relatedData = this.syncing.relatedData,
        queryContainsColumns,
        columns;
 
    // Check if any `select` style statements have been called with column
    // specifications. This could include `distinct()` with no arguments, which
    // does not affect inform the columns returned.
    queryContainsColumns = _(knex._statements).where({ grouping: 'columns' }).some('value.length');
 
    return Promise.resolve(this.constrain()).tap(function () {
 
      // If this is a relation, apply the appropriate constraints.
      if (relatedData) {
        relatedData.selectConstraints(knex, options);
      } else {
 
        // Call the function, if one exists, to constrain the eager loaded query.
        if (options._beforeFn) options._beforeFn.call(knex, knex);
 
        if (options.columns) {
 
          // Normalize single column name into array.
          columns = _.isArray(options.columns) ? options.columns : [options.columns];
        } else if (!queryContainsColumns) {
 
          // If columns have already been selected via the `query` method
          // we will use them. Otherwise, select all columns in this table.
          columns = [_.result(_this.syncing, 'tableName') + '.*'];
        }
      }
 
      // Set the query builder on the options, for access in the `fetching`
      // event handlers.
      options.query = knex;
      return _this.syncing.triggerThen('fetching', _this.syncing, columns, options);
    }).then(function () {
      return knex.select(columns);
    });
  }),
 
  // Issues an `insert` command on the query - only used by models.
  insert: Promise.method(function () {
    var syncing = this.syncing;
    return this.query.insert(syncing.format(_.extend(Object.create(null), syncing.attributes)), syncing.idAttribute);
  }),
 
  // Issues an `update` command on the query - only used by models.
  update: Promise.method(function (attrs) {
    var syncing = this.syncing,
        query = this.query;
    if (syncing.id != null) query.where(syncing.idAttribute, syncing.id);
    if (_.where(query._statements, { grouping: 'where' }).length === 0) {
      throw new Error('A model cannot be updated without a "where" clause or an idAttribute.');
    }
    return query.update(syncing.format(_.extend(Object.create(null), attrs)));
  }),
 
  // Issues a `delete` command on the query.
  del: Promise.method(function () {
    var query = this.query,
        syncing = this.syncing;
    if (syncing.id != null) query.where(syncing.idAttribute, syncing.id);
    if (_.where(query._statements, { grouping: 'where' }).length === 0) {
      throw new Error('A model cannot be destroyed without a "where" clause or an idAttribute.');
    }
    return this.query.del();
  })
 
});
 
module.exports = Sync;