wbyoung/maguey

View on GitHub
lib/adapters/base.js

Summary

Maintainability
A
0 mins
Test Coverage
'use strict';

var _ = require('lodash');
var pool = require('generic-pool');
var Promise = require('bluebird');
var Class = require('corazon/class');
var property = require('corazon/property');
var Grammar = require('../dialect/grammar');
var Translator = require('../dialect/translator');
var Phrasing = require('../dialect/phrasing');
var Procedures = require('../dialect/procedures');

/**
 * @class Adapter
 * @classdesc
 *
 * The base Adapter class is the extension point for custom database adapters.
 * As a user of Azul, you typically won't use this, but if you're looking to
 * add support for a custom database, you should start here.
 */
var Adapter = Class.extend(/** @lends Adapter# */ {

  /**
   * Create an adapter.
   *
   * @public
   * @constructor
   */
  init: function(connection) {
    var cls = this.__identity__;
    this._super();
    this._connection = connection;
    this._grammar = cls.Grammar.create();
    this._translator = cls.Translator.create();
    this._phrasing = cls.Phrasing.create(this._grammar, this._translator);
    this._procedures = cls.Procedures.create(this._grammar, this._phrasing);
  },
});

Adapter.reopenClass(/** @lends Adapter */ {

  /**
   * The grammar class to use for this adapter. Subclasses can override this
   * property to customize the grammar in use.
   *
   * @public
   * @type {Grammar}
   */
  Grammar: Grammar,

  /**
   * The translator class to use for this adapter. Subclasses can override this
   * property to customize the translator in use.
   *
   * @public
   * @type {Translator}
   */
  Translator: Translator,

  /**
   * The phrasing class to use for this adapter. Subclasses can override this
   * property to customize the phrasing in use.
   *
   * @public
   * @type {Phrasing}
   */
  Phrasing: Phrasing,


  /**
   * The procedures class to use for this adapter. Subclasses can override this
   * property to customize the procedures in use.
   *
   * @public
   * @type {Procedures}
   */
  Procedures: Procedures,

});

/**
 * Access to the {@link Translator} instance.
 *
 * @name Adapter#translator
 * @public
 * @type {Translator}
 * @readonly
 */
Adapter.reopen({ translator: property() });

/**
 * Access to the {@link Phrasing} instance.
 *
 * @name Adapter#phrasing
 * @public
 * @type {Phrasing}
 * @readonly
 */
Adapter.reopen({ phrasing: property() });

/**
 * Access to the {@link Procedures} instance.
 *
 * @name Adapter#phrasing
 * @public
 * @type {Procedures}
 * @readonly
 */
Adapter.reopen({ procedures: property() });

Adapter.reopen(/** @lends Adapter# */ {

  /**
   * Create a connection.
   *
   * The resulting _client_ object can be any object. It will be created
   * through  a resource pool to avoid creating too many connections. The
   * _client_ object will also be passed to {@link #_disconnect} and
   * {@link #_execute} for subclass use.
   *
   * Subclasses must implement this method.
   *
   * @protected
   * @return {Promise} Promise to resolve with a _client_ object when the
   * connection has been established.
   */
  _connect: Promise.method(function() {
    throw new Error('The `_connect` method must be implemented by subclass');
  }),

  /**
   * Destroy a connection.
   *
   * The `client` object is the object that was created from a
   * {@link #_connect}.
   *
   * Subclasses must implement this method.
   *
   * @protected
   * @param {Object} client The _client_ object.
   * @return {Promise} Promise to resolve when connection has been closed.
   */
  _disconnect: Promise.method(function(/*client*/) {
    throw new Error('The `_disconnect` method must be implemented by subclass');
  }),

  /**
   * The format for normalized responses after executing a raw SQL query in a
   * database.
   *
   * @public
   * @typedef {Object} Adapter~ExecutionResult
   * @property {Array.<Object>} client The client that executed the query.
   * @property {Array.<Object>} rows The rows returned by the query.
   * @property {Array.<String>} fields The filed names returned by the query
   * (in order).
   */

  /**
   * Execute a query.
   *
   * @param {String} sql The raw sql string.
   * @param {Array} args The positional sql args.
   * @param {Object} [options] Options.
   * @param {Object} [options.client] Client to use rather than acquiring one.
   * @return {Promise} A promise that will resolve/reject to an
   * {@link Adapter~ExecutionResult}.
   */
  execute: Promise.method(function(sql, args, options) {
    var self = this;
    var opts = options || {};
    return Promise.bind({}).then(function() {
      return opts.client ||
        (this.acquired = true, self.pool.acquireAsync());
    })
    .then(function(client) { this.client = client; })
    .then(function() {
      return self._execute(this.client, sql, args);
    })
    .finally(function() {
      return this.acquired && self.pool.release(this.client);
    });
  }),

  /**
   * Execute a query.
   *
   * The resolved result should be normalized by the subclass to contain the
   * properties defined by {@link Adapter~ExecutionResult}.
   *
   * The `client` object is the object that was created from a
   * {@link #_connect}.
   *
   * Subclasses must implement this method.
   *
   * @param {Object} client The _client_ object.
   * @param {String} sql The raw sql string.
   * @param {Array} args The positional sql args.
   * @return {Promise} A promise that will resolve/reject based on the results
   * of the query.
   */
  _execute: Promise.method(function(/*client, sql, args*/) {
    throw new Error('The `_execute` method must be implemented by subclass');
  }),

  /**
   * Disconnect all connections.
   *
   * @return {Promise} A promise indicating that the database has been
   * disconnected.
   */
  disconnectAll: Promise.method(function() {
    return new Promise(function(resolve) {
      this.pool.drain(function() {
        this.pool.destroyAllNow();
        resolve();
      }.bind(this));
    }.bind(this));
  }),

  /**
   * Access to pool that manages creation/destruction of connection and keeps
   * track of objects representing details of those connections. The details
   * objects may vary by adapter.
   *
   * @name Adapter#pool
   * @private
   * @type {Object}
   * @readonly
   */
  pool: property(function() {
    if (this._pool) { return this._pool; }

    var self = this;

    var create = function(callback) { // jscs:ignore jsDoc
      // an arbitrary value, usually a _client_, (and will be referred to as
      // _client_ throughout this code) will be passed through from the result
      // of the adapter's `_connect` to the success function. this will result
      // in the pool resource vending that _client_ when new acquisitions are
      // made.
      var success = _.partial(callback, null);
      var failure = callback;
      self._connect().then(success, failure);
    };

    var destroy = function(client) { // jscs:ignore jsDoc
      // it is expected that when resources are released from the pool the
      // `release` call will be provided with the _client_, the same object that
      // was returned from the `_connect` method. so here, it gets passed along
      // to `_disconnect` so the `_disconnect` gets the same _client_ object
      // that `_connect` returned.
      self._disconnect(client);
    };

    self._pool = Promise.promisifyAll(pool.Pool({
      name: 'connection',
      create: create,
      destroy: destroy,
      max: 10,
      min: 2,
      idleTimeoutMillis: 30000,
    }));

    return self._pool;
  }),
});

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