Source: GenericDao.js

'use strict';

require('insulin').factory('ndm_GenericDao', GenericDaoProducer);

function GenericDaoProducer(deferred, NotFoundError, DuplicateError, InsertValidator,
  UpdateValidator, DeleteValidator, ndm_Column, ndm_assert) {
  /**
   * Generic data-access object for simple CRUD operations.
   */
  class GenericDao {
    /**
     * Initialize the DAO.  Note that the db is expected to be connected
     * before any of this class's methods are used.
     * @param {DataContext} dataContext - A DataContext instance that is used
     * to run queries.
     * @param {string} tableName - The name of the table that this DAO operates on.
     */
    constructor(dataContext, tableName) {
      this.dc    = dataContext;
      this.table = this.dc.database.getTableByName(tableName);
    }

    // <<private>>

    /**
     * Private implementation details for retrieve.
     * @private
     * @see retrieve
     */
    _retrieve(where, params={}) {
      const tblMapping = this.table.mapTo;
      const query      = this.dc.from(this.table.name);

      if (where)
        query.where(where, params);

      return query
        .select()
        .execute()
        .then(res => res[tblMapping]);
    }

    /**
     * Private implementation details for retrieveSingle.
     * @private
     * @see retrieveSingle
     */
    _retrieveSingle(where, params, onNotFound) {
      return this
        ._retrieve(where, params)
        .then(res => {
          if (res.length === 0) {
            const err = new NotFoundError('Resource not found.');

            if (onNotFound)
              return deferred.reject(onNotFound(err));
            return deferred.reject(err);
          }

          return res[0];
        });
    }

    /**
     * Private implementation details for retrieveByID.
     * @private
     * @see retrieveByID
     */
    _retrieveByID(id) {
      const pkName     = this.table.primaryKey[0].name;
      const fqcn       = `${this.table.mapTo}.${pkName}`;
      const where      = {$eq: {[fqcn]: `:${pkName}`}};
      const params     = {[pkName]: id};
      const onNotFound = () => new NotFoundError(`Invalid ${pkName}.`);

      return this._retrieveSingle(where, params, onNotFound);
    }

    /**
     * Private implementation details for isUnique.
     * @private
     * @see isUnique
     */
    _isUnique(where, params, onDupe) {
      return this
        ._retrieve(where, params)
        .then(dupe => {
          if (dupe.length === 0)
            return deferred.resolve(true);

          // This is the id of the duplicate record.
          const pkMapping = this.table.primaryKey[0].mapTo;
          const id        = dupe[0][pkMapping];
          const err       = new DuplicateError('Duplicate resource.', null, id);

          if (onDupe)
            return deferred.reject(onDupe(err));
          return deferred.reject(err);
        });
    }

    /**
     * Private implementation details for createIf.
     * @private
     * @see createIf
     */
    _createIf(resource, condition) {
      const tblMapping = this.table.mapTo;

      return new InsertValidator(resource, tblMapping, this.dc.database)
        .validate()
        .then(() => condition(resource))
        .then(() => this.dc.insert({[tblMapping]: resource}).execute())
        .then(() => resource);
    }

    /**
     * Private implementation details for create().
     * @private
     * @param resource See create().
     */
    _create(resource) {
      // Same as createIf with a no-op condition.
      return this._createIf(resource, () => deferred.resolve());
    }

    /**
     * Private implementation details for updateIf.
     * @private
     * @see updateIf
     */
    _updateIf(resource, condition) {
      const tblMapping = this.table.mapTo;

      return new UpdateValidator(resource, tblMapping, this.dc.database)
        .validate()
        .then(() => condition(resource))
        .then(() => this.dc.update({[tblMapping]: resource}).execute())
        .then(function(updRes) {
          return updRes.affectedRows === 1 ? resource : deferred.reject(
            new NotFoundError('Resource not found.'));
        });
    }

    /**
     * Private implementation details for update().
     * @private
     * @param resource See update().
     */
    _update(resource) {
      // Same as updateIf with a no-op condition.
      return this._updateIf(resource, () => deferred.resolve());
    }

    /**
     * Private implementation details for delete().
     * @private
     * @see delete
     */
    _delete(resource) {
      const tblMapping = this.table.mapTo;

      return new DeleteValidator(resource, tblMapping, this.dc.database)
        .validate()
        .then(() => this.dc.delete({[tblMapping]: resource}).execute())
        .then(function(delRes) {
          return delRes.affectedRows === 1 ? resource : deferred.reject(
            new NotFoundError('Resource not found.'));
        });
    }

    /**
     * Private implementation details for replace().
     * @private
     * @see replace
     */
    _replace(pTblName, pID, resources) {
      const pTbl        = this.dc.database.getTableByName(pTblName);
      const pTblMapping = pTbl.mapTo;
      const pPKMapping  = pTbl.primaryKey[0].mapTo;
      const parent      = {[pPKMapping]: pID};

      const tblName     = this.table.name;
      const tblMapping  = this.table.mapTo;
      const pkMapping   = this.table.primaryKey[0].mapTo;

      const fks         = this.dc.database.relStore.getRelationships(tblName, pTblName, true);

      let fkName, fkMapping, fqFKName;

      ndm_assert(fks.length === 1,
        'Replace can only be performed if there is exactly one relationship ' +
        'between the parent and child tables.');

      fkName    = fks[0].column;
      fkMapping = this.table.getColumnByName(fkName).mapTo;
      fqFKName  = ndm_Column.createFQColName(tblName, fkName);

      // 1) Validate the parentID.
      // 2) Set/overwrite the parentID on each resource, and remove any resouce
      //    identifiers.
      // 3) Validate each resource.
      // 4) Delete the old resources.
      // 5) Insert the new resources.
      return new DeleteValidator(parent, pTblMapping, this.dc.database)
        .validate()
        .then(() => {
          resources.forEach(r => {
            r[fkMapping] = pID;
            delete r[pkMapping];
          });

          return deferred.map(resources, resource => 
            new InsertValidator(resource, tblMapping, this.dc.database).validate());
        })
        .then(() => {
          return this.dc
            .from(this.table.name)
            .where({'$eq': {[fqFKName]: `:${pPKMapping}`}}, parent)
            .delete()
            .execute();
        })
        .then(() => {
          return this.dc
            .insert({[tblMapping]: resources})
            .execute();
        })
        .then(resources => resources[tblMapping]);
    }

    // <<public>>

    /**
     * @callback GenericDao~errorCallback
     * @param {Error} err - An Error instance.
     * @returns {Error} A promise that produces a customized Error
     * instance.
     */

    /**
     * @callback GenericDao~conditionCallback
     * @param {Object} resource - The resource object.
     * @returns {Promise<bool>} A promise that is resolved if the condition is
     * met, or otherwise rejected.
     */

    /**
     * Retrieve an array of resources.
     * @param {Object} where - An optional where condition.
     * @param {Object} params - Query parameters for the where condition.
     * @returns {Promise<Object[]>} A promise that is resolved with the results
     * as an array.
     */
    retrieve(where, params) {
      return this._retrieve(where, params);
    }

    /**
     * Retrieve a single resource as an object.
     * @param {Object} where - An optional where condition.
     * @param {Object} params - Query parameters for the where condition.
     * @param {GenericDao~errorCallback} onNotFound - An optional function that
     * produces an Error when a resource is not found.
     * @returns {Promise<Object>} A promise that is resolved with the first
     * matching resource.  If there are no matches found, then the promise is
     * rejected with a NotFoundError instance.
     */
    retrieveSingle(where, params, onNotFound) {
      return this._retrieveSingle(where, params, onNotFound);
    }

    /**
     * Retrieve a single resource by ID.  Specialized version of retrieveSingle.
     * @param {any} id - The unique identifier of the resource.
     */
    retrieveByID(id) {
      return this._retrieveByID(id);
    }

    /**
     * Helper function to check that something is unique.  This is useful
     * before creating or updating a record.
     * @param {Object} where - An optional where condition.
     * @param {Object} params - Query parameters for the where condition.
     * @param {GenericDao~errorCallback} onDupe - An option function that is
     * called when a duplicate is found.  If the resource is not found and this
     * function is defined, the resource is rejected with the result of this
     * function.
     * @returns {Promise<bool>} A promise that is resolved if the resource is
     * unique (that is, if no records are found).  If the resource is not
     * unique then the promise is rejected with a DuplicateError instance.
     */
    isUnique(where, params, onDupe) {
      return this._isUnique(where, params, onDupe);
    }

    /**
     * Create a resource if a condition resolves successfully.  Note
     * that prior to invoking the condition the resource is validated
     * using an InsertValidator.
     * @param {Object} resource - A model to create.
     * @param {GenericDao~conditionCallback} condition - A function that
     * returns a promise.  If the promise is resolved then the model is
     * created.  resource is passed to condition.
     * @returns {Promise<Object>} A promise that is:
     * 1) Resolved with the model if the model is valid and the
     *    condition is resolved.  The model will be updated with
     *    the new ID if possible.
     * 2) Rejected with a ValidationErrorList if the model is invalid.
     * 3) Rejected with condition's promise if condition is rejected.
     */
    createIf(resource, condition) {
      return this._createIf(resource, condition);
    }

    /**
     * Generic create method that validates a model using an InsertValidator
     * and then inserts the model.
     * @param {Object} resource - A model to create.
     * @returns {Promise<Object>} Same as {@link createIf}.
     */
    create(resource) {
      return this._create(resource);
    }

    /**
     * Update a resource if a condition resolves successfully.  Note
     * that prior to invoking the condition the resource is validated
     * using an UpdateValidator.
     * @param {Object} resource - A model to update by ID.
     * @param {GenericDao~conditionCallback} condition - A function that
     * returns a promise.  If the promise is resolved then the model is
     * created.  resource is passed to condition.
     * @returns {Promise<Object>} A promise that is:
     * 1) Resolved with the model if the model is valid and the
     *    condition is resolved.
     * 2) Rejected with a ValidationErrorList if the model is invalid.
     * 3) Rejected with condition's promise if condition is rejected.
     */
    updateIf(resource, condition) {
      return this._updateIf(resource, condition);
    }

    /**
     * Generic update method that validates a model using an UpdateValidator
     * and then updates it by ID.
     * @param {Object} resource - A model to update by ID.
     * @returns {Promise<Object>} Same as {@link updateIf}.
     */
    update(resource) {
      return this._update(resource);
    }

    /**
     * Generic delete method that validates a model using a DeleteValidator
     * and then deletes it by ID.
     * @param {Object} resource - A model to delete by ID.
     * @returns {Promise<Object>} A promise that is:
     * 1) Resolved with the model if the model is valid and deleted.
     * 2) Rejected with a ValidationErrorList if the model is invalid.
     * 3) Rejected with a NotFoundError instance if no records are affected
     *    by the delete attempt.
     */
    delete(resource) {
      return this._delete(resource);
    }

    /**
     * Replace (remove and recreate) all the resources associated with a parent
     * table.
     * @param {string} parentTableName - The name of the parent table.
     * @param {any} parentID - The identifier of the parent resource.
     * @param {Object[]} resources - An array of resources which will be
     * created.
     * @returns {Promise<Object[]>} The array of resources, each updated with
     * their new identifier and parentID.  The parent and the resources are
     * validated, so the returned promise shall be rejected if a validation
     * error occurs.
     */
    replace(parentTableName, parentID, resources) {
      return this._replace(parentTableName, parentID, resources);
    }
  }

  return GenericDao;
}