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,
};