model-data-transformer.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import {Service} from '@e22m4u/js-service';
  2. import {cloneDeep} from '../../utils/index.js';
  3. import {isPlainObject} from '../../utils/index.js';
  4. import {transformPromise} from '../../utils/index.js';
  5. import {EmptyValuesService} from '@e22m4u/js-empty-values';
  6. import {InvalidArgumentError} from '../../errors/index.js';
  7. import {ModelDefinitionUtils} from './model-definition-utils.js';
  8. import {PropertyTransformerRegistry} from './properties/index.js';
  9. /**
  10. * Model data transformer.
  11. */
  12. export class ModelDataTransformer extends Service {
  13. /**
  14. * Transform.
  15. *
  16. * @param {string} modelName
  17. * @param {object} modelData
  18. * @param {boolean} isPartial
  19. * @returns {object|Promise<object>}
  20. */
  21. transform(modelName, modelData, isPartial = false) {
  22. if (!isPlainObject(modelData))
  23. throw new InvalidArgumentError(
  24. 'The data of the model %v should be an Object, but %v was given.',
  25. modelName,
  26. modelData,
  27. );
  28. const emptyValuesService = this.getService(EmptyValuesService);
  29. const modelDefinitionUtils = this.getService(ModelDefinitionUtils);
  30. const propDefs =
  31. modelDefinitionUtils.getPropertiesDefinitionInBaseModelHierarchy(
  32. modelName,
  33. );
  34. const propNames = Object.keys(isPartial ? modelData : propDefs);
  35. const transformedData = cloneDeep(modelData);
  36. return propNames.reduce((transformedDataOrPromise, propName) => {
  37. const propDef = propDefs[propName];
  38. if (!propDef) return transformedDataOrPromise;
  39. const propType =
  40. modelDefinitionUtils.getDataTypeFromPropertyDefinition(propDef);
  41. const propValue = modelData[propName];
  42. const isEmpty = emptyValuesService.isEmptyByType(propType, propValue);
  43. if (isEmpty) return transformedDataOrPromise;
  44. const newPropValueOrPromise = this._transformPropertyValue(
  45. modelName,
  46. propName,
  47. propDef,
  48. propValue,
  49. );
  50. return transformPromise(newPropValueOrPromise, newPropValue => {
  51. return transformPromise(transformedDataOrPromise, resolvedData => {
  52. if (newPropValue !== propValue) resolvedData[propName] = newPropValue;
  53. return resolvedData;
  54. });
  55. });
  56. }, transformedData);
  57. }
  58. /**
  59. * Transform property value.
  60. *
  61. * @param {string} modelName
  62. * @param {string} propName
  63. * @param {string|object} propDef
  64. * @param {*} propValue
  65. * @returns {*|Promise<*>}
  66. */
  67. _transformPropertyValue(modelName, propName, propDef, propValue) {
  68. if (typeof propDef === 'string' || propDef.transform == null)
  69. return propValue;
  70. const transformDef = propDef.transform;
  71. const transformerRegistry = this.getService(PropertyTransformerRegistry);
  72. const transformFn = (
  73. value,
  74. transformerOrName,
  75. transformerOptions = undefined,
  76. ) => {
  77. let transformerName, transformerFn;
  78. // если второй аргумент является строкой, то строка
  79. // воспринимается как название зарегистрированного
  80. // трансформера
  81. if (typeof transformerOrName === 'string') {
  82. transformerName = transformerOrName;
  83. transformerFn = transformerRegistry.getTransformer(transformerName);
  84. }
  85. // если второй аргумент является функцией,
  86. // то функция воспринимается как трансформер
  87. else if (typeof transformerOrName === 'function') {
  88. transformerName =
  89. transformerOrName.name && transformerOrName.name !== 'transform'
  90. ? transformerOrName.name
  91. : undefined;
  92. transformerFn = transformerOrName;
  93. }
  94. // если второй аргумент не является строкой
  95. // и функцией, то выбрасывается ошибка
  96. else {
  97. throw new InvalidArgumentError(
  98. 'Transformer must be a non-empty String or ' +
  99. 'a Function, but %v was given.',
  100. transformerOrName,
  101. );
  102. }
  103. const context = {transformerName, modelName, propName};
  104. return transformerFn(value, transformerOptions, context);
  105. };
  106. // если значением опции "transform" является строка,
  107. // то строка воспринимается как название трансформера
  108. if (transformDef && typeof transformDef === 'string') {
  109. return transformFn(propValue, transformDef);
  110. }
  111. // если значением опции "transform" является функция,
  112. // то функция воспринимается как трансформер
  113. else if (transformDef && typeof transformDef === 'function') {
  114. return transformFn(propValue, transformDef);
  115. }
  116. // если значение опции "transform" является массив, то каждый
  117. // элемент массива воспринимается как название трансформера
  118. // или функция-валидатор
  119. else if (Array.isArray(transformDef)) {
  120. return transformDef.reduce((valueOrPromise, transformerOrName) => {
  121. if (
  122. !transformerOrName ||
  123. (typeof transformerOrName !== 'string' &&
  124. typeof transformerOrName !== 'function')
  125. ) {
  126. throw new InvalidArgumentError(
  127. 'The provided option "transform" for the property %v ' +
  128. 'in the model %v has an Array value that should contain ' +
  129. 'transformer names or transformer functions, but %v was given.',
  130. propName,
  131. modelName,
  132. transformerOrName,
  133. );
  134. }
  135. return transformPromise(valueOrPromise, value => {
  136. return transformFn(value, transformerOrName);
  137. });
  138. }, propValue);
  139. }
  140. // если значение опции "transform" является объектом,
  141. // то ключи объекта воспринимаются как названия трансформеров,
  142. // а их значения аргументами
  143. else if (transformDef !== null && typeof transformDef === 'object') {
  144. return Object.keys(transformDef).reduce(
  145. (valueOrPromise, transformerName) => {
  146. const transformerOptions = transformDef[transformerName];
  147. return transformPromise(valueOrPromise, value => {
  148. return transformFn(value, transformerName, transformerOptions);
  149. });
  150. },
  151. propValue,
  152. );
  153. }
  154. // если значение опции "transform" не является строкой,
  155. // функцией и массивом, то выбрасывается ошибка
  156. else {
  157. throw new InvalidArgumentError(
  158. 'The provided option "transform" for the property %v in the model %v ' +
  159. 'should be either a transformer name, a transformer function, an array ' +
  160. 'of transformer names or functions, or an object mapping transformer ' +
  161. 'names to their arguments, but %v was given.',
  162. propName,
  163. modelName,
  164. transformDef,
  165. );
  166. }
  167. }
  168. }