FreeAllMedia/dovima

View on GitHub
es6/lib/model/fetch.js

Summary

Maintainability
D
1 day
Test Coverage
import flowsync from "flowsync";
import inflect from "jargon";
import privateData from "incognito";
import ModelFinder from "../modelFinder.js";
import symbols from "./symbols";

//internal private functions
const fetchByAssociations = {
  "hasMany": fetchByHasMany,
  "hasOne": fetchByHasOne,
  "belongsTo": fetchByBelongsTo
};

function fetchByHasOne(associationName, associations, callback) {
  const modelFinder = new ModelFinder(this.database);
  const association = associations[associationName];
  const ModelClass = association.constructor;

  if (association.through) {
    const throughAssociation = associations[association.through];

    if (!this[this.primaryKey]) {
      throw new Error(`'${this.primaryKey}' is not set on ${this.constructor.name}`);
    }

    modelFinder
      .find(throughAssociation.constructor)
      .where(association.foreignId, "=", this[this.primaryKey])
      .limit(1)
      .results((errors, models) => {
        const joinModel = models[0];
        const destinationAssociation = joinModel.associations[associationName];

        const tempModel = new association.constructor();
        modelFinder
          .find(association.constructor)
          .where(tempModel.primaryKey, "=", joinModel[destinationAssociation.foreignId])
          .limit(1)
          .results((associationError, associationModels) => {
            const associationModel = associationModels[0];
            this[associationName] = associationModel;
            callback();
          });
      });
  } else {
    const query = modelFinder
      .find(ModelClass)
      .where(association.foreignKey, "=", this[this.primaryKey]);

    const processWhereCondition = (value) => {
      if (typeof value === "string") {
        const snakeCasedValue = inflect(value).snake.toString();
        return snakeCasedValue;
      } else {
        return value;
      }
    };

    const processedWhere = association.where.map(processWhereCondition);

    query.andWhere(function () {
      this.where(...processedWhere);

      if(Array.isArray(association.andWhere)) {
        association.andWhere.forEach((andWhereItem) => {
          const processedAndWhereItem = andWhereItem.map(processWhereCondition);
          this.andWhere(...processedAndWhereItem);
        });
      }
    });

    query
      .limit(1)
      .results((errors, models) => {
        const model = models[0];
        this[associationName] = model;
        callback();
      });
  }
}

function fetchWhere(modelClass, key, conditionType, ids, target, callback) {
  const modelFinder = new ModelFinder(this.database);
  modelFinder
    .find(modelClass)
    .where(key, conditionType, ids)
    .results((findErrors, resultModels) => {
      resultModels.forEach((model) => {
        target.push(model);
      });
      callback();
    });
}

function fetchByHasMany(associationName, associations, callback) {
  const association = associations[associationName];

  if (association.through) {
    const throughAssociation = associations[association.through];

    throughAssociation.constructor
      .find
      .where(association.foreignId, this[this.primaryKey])
      .results((errors, models) => {
        if(models.length > 0) {
          const foreignAssociationName = association.as || associationName;

          if (!models[0].associations[foreignAssociationName]) {
            throw new Error(`'${foreignAssociationName}' is not a valid association on through model '${throughAssociation.constructor.name}'`);
          }

          const destinationAssociation = models[0].associations[foreignAssociationName];

          let modelIds = [];

          const tempModel = new association.constructor();

          switch(destinationAssociation.type) {
            case "hasOne":
              modelIds = models.map(model => { return model[throughAssociation.foreignId]; });
              fetchWhere.call(this, association.constructor, tempModel.primaryKey, "in", modelIds, this[associationName], callback);
              break;

            case "hasMany":
              modelIds = models.map(model => { return model[model.primaryKey]; });
              fetchWhere.call(this, association.constructor, destinationAssociation.foreignId, "in", modelIds, this[associationName], callback);
              break;

            case "belongsTo":
              modelIds = models.map(model => { return model[destinationAssociation.foreignId]; });
              fetchWhere.call(this, association.constructor, tempModel.primaryKey, "in", modelIds, this[associationName], callback);
              break;
          }

        }
      });
  } else {
    this[associationName].fetch(callback);
  }
}

function fetchByBelongsTo(associationName, associations, callback) {
  const modelFinder = new ModelFinder(this.database);
  const association = associations[associationName];

  if (!this[association.foreignId]) {
    throw new Error(`Cannot fetch '${associationName}' because '${association.foreignId}' is not set on ${this.constructor.name}`);
  }

  modelFinder
    .find(association.constructor)
    .where(this.primaryKey, "=", this[association.foreignId])
    .limit(1)
    .results((errors, models) => {
      const model = models[0];
      this[associationName] = model;
      model[association.foreignName] = this;
      callback();
    });
}

function fetchBy(fields = [this.primaryKey], callback = undefined) {
  const _ = privateData(this);
  if (_.mockFetchRecord) {
    for (let attributeName in _.mockFetchRecord) {
      const mockValue = _.mockFetchRecord[attributeName];
      this[attributeName] = mockValue;
    }
    callback();
  } else {
    fetchFromDatabase.call(this, fields, callback);
  }
}

function fetchFromDatabase(fields = [this.primaryKey], callback = undefined) {
  let database = this.database;
  if (!database) { throw new Error("Cannot fetch without Model.database set."); }

  let chain = database
    .select("*")
    .from(this.tableName);

  fields.forEach((field, index) => {
    if (!this[field]) { throw new Error(`Cannot fetch this model by the '${field}' field because it is not set.`); }

    if(index === 0) {
      chain = chain.where(field, "=", this[field]);
    } else {
      chain = chain.andWhere(field, "=", this[field]);
    }
  }, this);

  const _ = privateData(this);

  if(_.softDelete) {
    chain = chain.whereNull(inflect("deletedAt").snake.toString());
  }

  chain
    .limit(1)
    .results((error, records) => {
      if(records.length === 0) {
        callback(new Error(`There is no ${this.constructor.name} for the given (${fields.join(", ")}).`));
      } else {
        this[symbols.parseAttributesFromFields](records[0]);

        if (_.includeAssociations.length > 0) {
          const associations = this.associations;

          /* We'll be putting all of our Async tasks into this */
          const fetchTasks = [];

          _.includeAssociations.forEach((associationName) => {

            const association = associations[associationName];

            if (!association) {
              throw new Error(`Cannot fetch '${associationName}' because it is not a valid association on ${this.constructor.name}`);
            }

            fetchTasks.push(finished => {
              //call the fetch function for the correct association type
              fetchByAssociations[association.type].call(this, associationName, associations, finished);
            });
          });

          flowsync.parallel(
            fetchTasks,
            () => {
              if (callback) {
                callback(error, this);
              }
            }
          );
        } else {
          if (callback) {
            callback(error, this);
          }
        }
      }
    });
}

//public function
export default function fetch(...options) {
  switch(options.length) {
    case 0:
      fetchBy.call(this);
      break;
    case 1:
      if(typeof options[0] === "function") {
        fetchBy.call(this, [this.primaryKey], options[0]);
      } else if(Array.isArray(options[0])) {
        fetchBy.call(this, options[0]);
      } else {
        fetchBy.call(this, [options[0]]);
      }
      break;
    case 2:
      if(Array.isArray(options[0])) {
        fetchBy.call(this, options[0], options[1]);
      } else {
        fetchBy.call(this, [options[0]], options[1]);
      }
      break;
  }
}