server/services/ServiceMethods.js

  1. const errors = require('@feathersjs/errors');
  2. const _ = require('lodash');
  3. const Sequelize = require('sequelize');
  4. const Promise = require('bluebird');
  5. const { logger } = require('../util/logger');
  6. const { getTranslation } = require('../util/helpers');
  7. const { Op } = Sequelize;
  8. /**
  9. * removeNestedResource
  10. * remove nested resource based on passed nested resources
  11. * e.g [1, 2, 3] * [1, 2] => 3 is removed from DB
  12. * @memberof ServiceMethods
  13. * @param transaction {Sequelize.Transaction}
  14. * @param model {Sequelize.Model}
  15. * @param resource {Object|Array}
  16. * @param parentID {String|Number}
  17. * @param foreignKey {String}
  18. */
  19. const removeNestedResource = ({
  20. transaction, model, resource = [], parentID, foreignKey,
  21. }) => {
  22. if (_.isArray(resource)) {
  23. const idsToUpdate = resource.reduce((acc, el) => (el.id ? acc.concat(el.id) : acc), []);
  24. return model.destroy({
  25. transaction,
  26. where: {
  27. id: { [Op.notIn]: idsToUpdate },
  28. [foreignKey]: parentID,
  29. },
  30. });
  31. }
  32. return model.destroy({
  33. transaction,
  34. where: {
  35. id: { [Op.not]: resource.id },
  36. [foreignKey]: parentID,
  37. },
  38. });
  39. };
  40. /**
  41. * prepareExports
  42. * combine previous exports with exports for current element
  43. * @param globalExports {Object}
  44. * @param nestedExports {Object}
  45. * @returns {Function}
  46. */
  47. const prepareExports = ({ globalExports = {}, nestedExports = {} }) => ({
  48. data = {}, created = {},
  49. }) => Object.assign(globalExports, Object.keys(nestedExports || {})
  50. .reduce((acc, el) => Object.assign(acc, {
  51. [el]: _.has(data, nestedExports[el]) ? _.get(data, nestedExports[el])
  52. : (created.get && created.get(nestedExports[el])) || null,
  53. }), {}));
  54. /**
  55. * prepareImports
  56. * take imports from global import for current element
  57. * @param exports {Object}
  58. * @param imports {Object}
  59. * @returns {{}}
  60. */
  61. const prepareImports = ({
  62. exports = {}, imports = {},
  63. }) => Object.keys(imports || {})
  64. .reduce((acc, el) => (_.has(exports, el)
  65. ? Object.assign(acc, { [imports[el]]: exports[el] || null })
  66. : acc), {});
  67. /**
  68. * prepareNested
  69. * prepare nested resource for loading to DB
  70. * @param nested
  71. * @param resource
  72. * @param makeExports
  73. * @param created
  74. * @returns {*|{}}
  75. */
  76. const prepareNested = ({
  77. nested, resource, makeExports, created,
  78. }) => {
  79. const { name, imports } = nested;
  80. if (_.isArray(resource)) {
  81. return {
  82. [name]: resource.reduce((acc, data, index) => {
  83. if (_.isArray(data[name])) {
  84. const exports = makeExports({ data, created: created[index] });
  85. return acc.concat(data[name].reduce((sum, elem) => sum
  86. .concat(Object.assign(elem, prepareImports({ exports, imports }))), []));
  87. } else if (_.isObject(data[name])) { // eslint-disable-line
  88. const exports = makeExports({ data, created: created[index] });
  89. return acc.concat(Object.assign(data[name], prepareImports({ exports, imports })));
  90. }
  91. return acc.concat(data);
  92. }, []),
  93. };
  94. } else if (_.isObject(resource)) { // eslint-disable-line
  95. const exports = makeExports({ data: resource, created });
  96. return Object.assign(resource, {
  97. [name]: Object.assign(resource[name], prepareImports({ exports, imports })),
  98. });
  99. }
  100. return null;
  101. };
  102. /**
  103. * updateOrCreate
  104. * update a resource or create new one if it's not present
  105. * @param transaction {Sequelize.Transaction}
  106. * @param model {Sequelize.Model}
  107. * @param resource {Object}
  108. * @returns {Promise}
  109. */
  110. const updateOrCreate = ({ transaction, model, resource }) => {
  111. if (_.isArray(resource)) {
  112. return Promise.map((resource || []), (nested) => {
  113. if (!nested) {
  114. return Promise.resolve();
  115. }
  116. if (nested.id) {
  117. return model.update(nested, { transaction, where: { id: nested.id } });
  118. }
  119. return model.create(nested, { transaction });
  120. });
  121. } else if (_.isObject(resource)) { // eslint-disable-line
  122. if (resource.id) {
  123. return model.update(resource, { transaction, where: { id: resource.id } });
  124. }
  125. return model.create(resource, { transaction });
  126. }
  127. logger.log('error', `Trying to update model: ${model.name ? model.name : model } with not correct data: ${resource}`);
  128. return Promise.resolve(resource);
  129. };
  130. /**
  131. * processNested
  132. * process nested fields, update and create models in DB
  133. * @param nestedFields
  134. * @param transaction
  135. * @param data
  136. * @param id
  137. * @param exports
  138. * @returns {*}
  139. */
  140. const processNested = ({
  141. nestedFields = [], transaction, data, id, exports = {},
  142. }) => Promise.map(nestedFields, (field) => {
  143. const {
  144. foreignKey, name, model, nestedFields: nested, exports: nestedExports = {}, imports,
  145. } = field;
  146. let resource = data[name];
  147. if (_.isArray(resource)) {
  148. resource = resource.map((el) => {
  149. if (_.isObject(el)) {
  150. return Object.assign(el, prepareImports({ exports, imports }));
  151. }
  152. return el;
  153. });
  154. } else if (_.isObject(resource)) {
  155. resource = Object.assign(resource, prepareImports({ exports, imports }));
  156. }
  157. let result = Promise.resolve();
  158. if (foreignKey && id) {
  159. result = result.then(() => removeNestedResource({
  160. transaction,
  161. model,
  162. resource,
  163. parentID: id,
  164. foreignKey,
  165. })).catch(e => logger.log('error', e && e.message));
  166. }
  167. return result.then(() => updateOrCreate({
  168. transaction,
  169. model,
  170. resource,
  171. }).then((created) => {
  172. if (nested && nested.length) {
  173. return Promise.map(nested, (element) => {
  174. const makeExports = prepareExports({
  175. nestedExports,
  176. globalExports: exports,
  177. });
  178. const nestedData = prepareNested({
  179. nested: element,
  180. resource,
  181. makeExports,
  182. created,
  183. });
  184. if (!nestedData) {
  185. return Promise.resolve(resource);
  186. }
  187. return processNested({
  188. nestedFields: nested,
  189. transaction,
  190. data: nestedData,
  191. id,
  192. });
  193. });
  194. }
  195. return Promise.resolve(resource);
  196. })).catch(e => logger.log('error', e && e.message));
  197. });
  198. /**
  199. * save
  200. * patch model with nested resource
  201. * @memberof ServiceMethods
  202. * @param model {Sequelize.Model}
  203. * @param params
  204. * @param app
  205. * @param nestedFields
  206. * @param exports
  207. */
  208. const save = ({
  209. model, params, app, nestedFields = [], exports = {},
  210. }) => {
  211. const { id, data, headers } = params;
  212. const translator = getTranslation(headers);
  213. const sequelize = app.get('sequelizeClient');
  214. return sequelize.transaction((transaction) => {
  215. let root = Promise.resolve();
  216. if (id) {
  217. Object.assign(data, { id });
  218. root = root.then(() => model.update(
  219. data,
  220. { transaction, where: { id } },
  221. )).catch(e => logger.log('error', e && e.message));
  222. } else {
  223. root = root.then(() => model.create(data)).catch(e => logger.log('error', e && e.message));
  224. }
  225. return root.then((created) => {
  226. const nextExports = prepareExports({ nestedExports: exports })({ data, created });
  227. return processNested({
  228. nestedFields,
  229. transaction,
  230. data,
  231. id,
  232. exports: nextExports,
  233. });
  234. });
  235. }).then(() => data)
  236. .catch(() => {
  237. throw new errors.BadRequest('Invalid data', {
  238. errors: { [nestedFields.map(el => el.name)]: translator['any.default']() },
  239. });
  240. });
  241. };
  242. const methodNotImplemented = (params) => {
  243. const { headers } = params;
  244. const translator = getTranslation(headers);
  245. throw new errors.BadRequest('Invalid data', {
  246. errors: { path: translator['method.not_exist']() },
  247. });
  248. };
  249. const polymorphicRemove = ({
  250. app, ids, model, childModel, foreignKey, polymorphicKey, polymorphicType,
  251. }) => {
  252. const sequelize = app.get('sequelizeClient');
  253. return sequelize.transaction(transaction => Promise.all(ids.reduce((acc, id) => {
  254. acc.concat(model.destroy({
  255. transaction,
  256. where: {
  257. id,
  258. },
  259. }));
  260. acc.concat(childModel.destroy({
  261. transaction,
  262. where: {
  263. [foreignKey]: id,
  264. [polymorphicKey]: polymorphicType,
  265. },
  266. }));
  267. return acc;
  268. })));
  269. };
  270. module.exports = {
  271. removeNestedResource,
  272. prepareExports,
  273. prepareImports,
  274. prepareNested,
  275. updateOrCreate,
  276. processNested,
  277. save,
  278. methodNotImplemented,
  279. polymorphicRemove,
  280. };