wbyoung/maguey

View on GitHub
lib/query/base.js

Summary

Maintainability
A
0 mins
Test Coverage
'use strict';

var _ = require('lodash');
var util = require('util');
var Promise = require('bluebird');
var Class = require('corazon/class');
var Transform = require('./mixins/transform');
var EventEmitter = require('events').EventEmitter;
var property = require('corazon/property');

/**
 * An indication that this is a new query that matches the specific type of
 * query that this was chained from. This is used throughout the documentation
 * as a return value when the specific type of query that is returned is not
 * able to be specified (for instance, in mixins).
 *
 * @typedef {BaseQuery} ChainedQuery
 */

/**
 * @class BaseQuery
 * @classdesc
 *
 * Queries are the building block of Azul's database abstraction layer. They
 * are immutable, chainable objects. Each operation that you perform on a query
 * will return a duplicated query rather than the original. The duplicated
 * query will be configured as requested.
 *
 * Generally, you will not create queries directly. Instead, you will receive
 * a query object via one of many convenience methods.
 */
var BaseQuery = Class.extend(/** @lends BaseQuery# */ {

  /**
   * Create a base query.
   *
   * @protected
   * @constructor BaseQuery
   * @mixes Transform
   * @param {Adapter} adapter The adapter to use when using the query.
   */
  init: function(adapter) {
    this._super();
    this._adapter = adapter;
  },

  /**
   * Construct a new specific type of query and pass off arguments to the
   * `_create` method of that query.
   *
   * This method does a bit of work to try to ensure that the newly created
   * query is set up properly. It first creates a new instance of the given
   * type. Then it determines any mixins that the new type uses that the old
   * type did not & calls the `init` function on each of those. It then
   * performs a `_take` to copy anything from the existing query to the spawned
   * version. Finally, the new object's `_create` method is called to complete
   * the spawn.
   *
   * The `_take` implementation is that of the original object & not of the
   * spawned object, so some extra properties may end up on the resulting
   * object that do not apply to it.
   *
   * Note that during the process of calling mixin `init` methods, the method
   * is intentionally called in a way where `_super` calls will have no effect.
   * It will simply perform the initialization for that individual mixin.
   *
   * @method
   * @private
   * @param {Class} type The query type to use.
   * @param {Arguments} args The arguments to pass off.
   * @return {ChainedQuery} A new query of the given type.
   */
  _spawn: function(type, args) {
    var currentMixins = this.__identity__.__mixins__;
    var newMixins = _.difference(type.__mixins__, currentMixins);
    var query = type.new();
    newMixins.forEach(function(mixin) {
      if (mixin.hasOwnProperty('init')) {
        mixin.init.call(query);
      }
    });
    this.__class__.prototype._take.call(query, this);
    type.__class__.prototype._create.apply(query, args);

    this.emit('spawn', query);

    return query;
  },

  /**
   * Override point for initializing spawned queries.
   *
   * @method
   * @private
   */
  _create: function() {},

  /**
   * This method duplicates a query. Queries are immutable objects. All query
   * methods should return copies of the query rather than mutating any internal
   * state.
   *
   * This method is implemented by subclasses to complete duplication of an
   * object. Be sure to call `_super()`. Subclasses should duplicate and
   * reassign all properties that are considered mutable.
   *
   * @method
   * @protected
   * @return {BaseQuery} The duplicated query.
   */
  _dup: function() {
    var dup = this.__identity__.new();
    dup._take(this);

    this.emit('dup', dup);

    return dup;
  },

  /**
   * Duplication implementation.
   *
   * @method
   * @protected
   * @see {@link BaseQuery#_take}
   */
  _take: function(orig) {
    this._super(orig);
    this._adapter = orig._adapter;
  },

  /**
   * Clone a query.
   *
   * In most cases, you will want to take advantage of the fact that queries
   * cache their results. In some cases, however, you may want to re-execute
   * the exact same query. You can clone the query and use
   * {@link BaseQuery#then} or {@link BaseQuery#execute} to re-execute the
   * query:
   *
   *     query.clone().then(\/*...*\/);
   *
   * @return {BaseQuery} The duplicated query.
   */
  clone: function() {
    return this._dup();
  },

  /**
   * Get the statement (SQL & args) for a query.
   *
   * This method simply returns the statement for the query, but can be
   * overridden in sub-classes to provide a statement that is customized on a
   * per-adapter basis when possible.
   *
   * @public
   * @type {Statement}
   * @readonly
   */
  statement: property(function() {
    return this._statement();
  }),

  /**
   * Override point for statement generation.
   *
   * @method
   * @protected
   * @see {@link BaseQuery#statement}
   */
  _statement: function() {
    throw new Error('BaseQuery cannot be used directly.');
  },

  /**
   * The SQL of the query's statement. A shortcut for `statement.sql`.
   *
   * @public
   * @type {String}
   * @readonly
   * @see {@link BaseQuery#statement}
   */
  sql: property(function() {
    return this.statement.sql;
  }),

  /**
   * The args of the query's statement. A shortcut for `statement.args`.
   *
   * @public
   * @type {Array}
   * @readonly
   * @see {@link BaseQuery#statement}
   */
  args: property(function() {
    return this.statement.args;
  }),

  /**
   * Override point for query execution.
   *
   * Subclasses should not change the result value.
   *
   * @method
   * @public
   * @param {Object} client The client with which to execute.
   * @return {Promise}
   */
  _execute: function(client) {
    var statement = this.statement;
    return this._adapter.execute(statement.sql, statement.args, {
      client: client,
    })
    .catch(function(e) {
      throw _.extend(e, {
        message: util.format('%s on "%s"', e.message, statement.sql),
        sql: statement.sql,
        args: statement.args,
        query: this,
      });
    }.bind(this));
  },

  /**
   * Override point for getting client.
   *
   * @method
   * @public
   * @return {Promise}
   */
  _client: Promise.method(function() {
  }),


  /**
   * Override point for processing of query execution result.
   *
   * If a subclass changes the resolved value here, it will change the
   * query's final result as well. The values emitted & cached will be taken
   * from the override.
   *
   * @method
   * @public
   * @return {Promise}
   */
  _process: Promise.method(function(result) {
    return result;
  }),

  /**
   * Execute the query.
   *
   * @return {Promise} A promise that will resolve when execution is complete.
   */
  execute: Promise.method(function() {
    if (!this._promise) {
      var client = this._client.bind(this);
      var execute = this._execute.bind(this);
      var process = this._process.bind(this);
      var emitRaw = this.emit.bind(this, 'rawResult');
      var emitResult = this.emit.bind(this, 'result');
      var emitError = function(e) {
        this.emit('error', e); throw e;
      }.bind(this);

      this.emit('execute');
      this._promise = Promise.resolve()
        .then(client)
        .then(execute)
        .tap(emitRaw)
        .then(process)
        .tap(emitResult)
        .catch(emitError);
    }
    return this._promise;
  }),

  /**
   * {@link BaseQuery} is a _thenable_ object.
   *
   * @param {Function} fulfilledHandler
   * @param {Function} rejectedHandler
   * @return {Promise}
   */
  then: function(fulfilledHandler, rejectedHandler) {
    return this.execute().then(fulfilledHandler, rejectedHandler);
  },
});

BaseQuery.reopen(Transform); // transform methods override base query methods

BaseQuery.reopen(EventEmitter.prototype);

module.exports = BaseQuery.reopenClass({ __name__: 'BaseQuery' });