server/services/ServiceMethods.js

const errors = require('@feathersjs/errors');
const _ = require('lodash');
const Sequelize = require('sequelize');
const Promise = require('bluebird');
const { logger } = require('../util/logger');
const { getTranslation } = require('../util/helpers');

const { Op } = Sequelize;

/**
 * removeNestedResource
 * remove nested resource based on passed nested resources
 * e.g [1, 2, 3] * [1, 2] => 3 is removed from DB
 * @memberof ServiceMethods
 * @param transaction {Sequelize.Transaction}
 * @param model {Sequelize.Model}
 * @param resource {Object|Array}
 * @param parentID {String|Number}
 * @param foreignKey {String}
 */
const removeNestedResource = ({
  transaction, model, resource = [], parentID, foreignKey,
}) => {
  if (_.isArray(resource)) {
    const idsToUpdate = resource.reduce((acc, el) => (el.id ? acc.concat(el.id) : acc), []);
    return model.destroy({
      transaction,
      where: {
        id: { [Op.notIn]: idsToUpdate },
        [foreignKey]: parentID,
      },
    });
  }
  return model.destroy({
    transaction,
    where: {
      id: { [Op.not]: resource.id },
      [foreignKey]: parentID,
    },
  });
};

/**
 * prepareExports
 * combine previous exports with exports for current element
 * @param globalExports {Object}
 * @param nestedExports {Object}
 * @returns {Function}
 */
const prepareExports = ({ globalExports = {}, nestedExports = {} }) => ({
  data = {}, created = {},
}) => Object.assign(globalExports, Object.keys(nestedExports || {})
  .reduce((acc, el) => Object.assign(acc, {
    [el]: _.has(data, nestedExports[el]) ? _.get(data, nestedExports[el])
      : (created.get && created.get(nestedExports[el])) || null,
  }), {}));


/**
 * prepareImports
 * take imports from global import for current element
 * @param exports {Object}
 * @param imports {Object}
 * @returns {{}}
 */
const prepareImports = ({
  exports = {}, imports = {},
}) => Object.keys(imports || {})
  .reduce((acc, el) => (_.has(exports, el)
    ? Object.assign(acc, { [imports[el]]: exports[el] || null })
    : acc), {});
/**
 * prepareNested
 * prepare nested resource for loading to DB
 * @param nested
 * @param resource
 * @param makeExports
 * @param created
 * @returns {*|{}}
 */
const prepareNested = ({
   nested, resource, makeExports, created,
 }) => {
  const { name, imports } = nested;
  if (_.isArray(resource)) {
    return {
      [name]: resource.reduce((acc, data, index) => {
        if (_.isArray(data[name])) {
          const exports = makeExports({ data, created: created[index] });
          return acc.concat(data[name].reduce((sum, elem) => sum
            .concat(Object.assign(elem, prepareImports({ exports, imports }))), []));
        } else if (_.isObject(data[name])) { // eslint-disable-line
          const exports = makeExports({ data, created: created[index] });
          return acc.concat(Object.assign(data[name], prepareImports({ exports, imports })));
        }
        return acc.concat(data);
      }, []),
    };
  } else if (_.isObject(resource)) { // eslint-disable-line
    const exports = makeExports({ data: resource, created });
    return Object.assign(resource, {
      [name]: Object.assign(resource[name], prepareImports({ exports, imports })),
    });
  }
  return null;
};
/**
 * updateOrCreate
 * update a resource or create new one if it's not present
 * @param transaction {Sequelize.Transaction}
 * @param  model {Sequelize.Model}
 * @param resource {Object}
 * @returns {Promise}
 */
const updateOrCreate = ({ transaction, model, resource }) => {
  if (_.isArray(resource)) {
    return Promise.map((resource || []), (nested) => {
      if (!nested) {
        return Promise.resolve();
      }
      if (nested.id) {
        return model.update(nested, { transaction, where: { id: nested.id } });
      }
      return model.create(nested, { transaction });
    });
  } else if (_.isObject(resource)) { // eslint-disable-line
    if (resource.id) {
      return model.update(resource, { transaction, where: { id: resource.id } });
    }
    return model.create(resource, { transaction });
  }
  logger.log('error', `Trying to update model: ${model.name ? model.name : model } with not correct data: ${resource}`);
  return Promise.resolve(resource);
};
/**
 * processNested
 * process nested fields, update and create models in DB
 * @param nestedFields
 * @param transaction
 * @param data
 * @param id
 * @param exports
 * @returns {*}
 */
const processNested = ({
   nestedFields = [], transaction, data, id, exports = {},
 }) => Promise.map(nestedFields, (field) => {
    const {
      foreignKey, name, model, nestedFields: nested, exports: nestedExports = {}, imports,
    } = field;
    let resource = data[name];
    if (_.isArray(resource)) {
      resource = resource.map((el) => {
        if (_.isObject(el)) {
          return Object.assign(el, prepareImports({ exports, imports }));
        }
        return el;
      });
    } else if (_.isObject(resource)) {
      resource = Object.assign(resource, prepareImports({ exports, imports }));
    }
    let result = Promise.resolve();
    if (foreignKey && id) {
      result = result.then(() => removeNestedResource({
        transaction,
        model,
        resource,
        parentID: id,
        foreignKey,
      })).catch(e => logger.log('error', e && e.message));
    }
    return result.then(() => updateOrCreate({
      transaction,
      model,
      resource,
    }).then((created) => {
      if (nested && nested.length) {
        return Promise.map(nested, (element) => {
          const makeExports = prepareExports({
            nestedExports,
            globalExports: exports,
          });
          const nestedData = prepareNested({
            nested: element,
            resource,
            makeExports,
            created,
          });
          if (!nestedData) {
            return Promise.resolve(resource);
          }
          return processNested({
            nestedFields: nested,
            transaction,
            data: nestedData,
            id,
          });
        });
      }
      return Promise.resolve(resource);
    })).catch(e => logger.log('error', e && e.message));
  });

/**
 * save
 * patch model with nested resource
 * @memberof ServiceMethods
 * @param model {Sequelize.Model}
 * @param params
 * @param app
 * @param nestedFields
 * @param exports
 */
const save = ({
  model, params, app, nestedFields = [], exports = {},
}) => {
  const { id, data, headers } = params;
  const translator = getTranslation(headers);
  const sequelize = app.get('sequelizeClient');
  return sequelize.transaction((transaction) => {
    let root = Promise.resolve();
    if (id) {
      Object.assign(data, { id });
      root = root.then(() => model.update(
        data,
        { transaction, where: { id } },
      )).catch(e => logger.log('error', e && e.message));
    } else {
       root = root.then(() => model.create(data)).catch(e => logger.log('error', e && e.message));
    }
    return root.then((created) => {
      const nextExports = prepareExports({ nestedExports: exports })({ data, created });
      return processNested({
        nestedFields,
        transaction,
        data,
        id,
        exports: nextExports,
      });
    });
  }).then(() => data)
    .catch(() => {
      throw new errors.BadRequest('Invalid data', {
        errors: { [nestedFields.map(el => el.name)]: translator['any.default']() },
      });
    });
};

const methodNotImplemented = (params) => {
  const { headers } = params;
  const translator = getTranslation(headers);
  throw new errors.BadRequest('Invalid data', {
    errors: { path: translator['method.not_exist']() },
  });
};

const polymorphicRemove = ({
   app, ids, model, childModel, foreignKey, polymorphicKey, polymorphicType,
 }) => {
  const sequelize = app.get('sequelizeClient');
  return sequelize.transaction(transaction => Promise.all(ids.reduce((acc, id) => {
    acc.concat(model.destroy({
      transaction,
      where: {
        id,
      },
    }));
    acc.concat(childModel.destroy({
      transaction,
      where: {
        [foreignKey]: id,
        [polymorphicKey]: polymorphicType,
      },
    }));
    return acc;
  })));
};

module.exports = {
  removeNestedResource,
  prepareExports,
  prepareImports,
  prepareNested,
  updateOrCreate,
  processNested,
  save,
  methodNotImplemented,
  polymorphicRemove,
};