project-data.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import {InvalidArgumentError} from '@e22m4u/js-format';
  2. import {validateProjectionSchema} from './validate-projection-schema.js';
  3. /**
  4. * Project data.
  5. *
  6. * @param {object|Function|string} schema
  7. * @param {object|object[]|*} data
  8. * @param {object} [options]
  9. * @returns {*}
  10. */
  11. export function projectData(schema, data, options) {
  12. // options
  13. if (options !== undefined) {
  14. if (!options || typeof options !== 'object' || Array.isArray(options)) {
  15. throw new InvalidArgumentError(
  16. 'Projection options must be an Object, but %v was given.',
  17. options,
  18. );
  19. }
  20. // options.strict
  21. if (options.strict !== undefined && typeof options.strict !== 'boolean') {
  22. throw new InvalidArgumentError(
  23. 'Projection option "strict" must be a Boolean, but %v was given.',
  24. options.strict,
  25. );
  26. }
  27. // options.scope
  28. if (
  29. options.scope !== undefined &&
  30. (options.scope === '' || typeof options.scope !== 'string')
  31. ) {
  32. throw new InvalidArgumentError(
  33. 'Projection option "scope" must be a non-empty String, ' +
  34. 'but %v was given.',
  35. options.scope,
  36. );
  37. }
  38. // options.nameResolver
  39. if (
  40. options.nameResolver !== undefined &&
  41. typeof options.nameResolver !== 'function'
  42. ) {
  43. throw new InvalidArgumentError(
  44. 'Projection option "nameResolver" must be a Function, ' +
  45. 'but %v was given.',
  46. options.nameResolver,
  47. );
  48. }
  49. // options.factoryArgs
  50. if (
  51. options.factoryArgs !== undefined &&
  52. !Array.isArray(options.factoryArgs)
  53. ) {
  54. throw new InvalidArgumentError(
  55. 'Projection option "factoryArgs" must be an Array, ' +
  56. 'but %v was given.',
  57. options.factoryArgs,
  58. );
  59. }
  60. }
  61. // если схема является фабрикой,
  62. // то извлекается фабричное значение
  63. if (typeof schema === 'function') {
  64. const factoryArgs = (options && options.factoryArgs) || [];
  65. schema = schema(...factoryArgs);
  66. if (
  67. !schema ||
  68. (typeof schema !== 'object' && typeof schema !== 'string') ||
  69. Array.isArray(schema)
  70. ) {
  71. throw new InvalidArgumentError(
  72. 'Schema factory must return an Object ' +
  73. 'or a non-empty String, but %v was given.',
  74. schema,
  75. );
  76. }
  77. }
  78. // если схема является строкой,
  79. // то выполняется разрешение имени
  80. if (typeof schema === 'string') {
  81. // если разрешающая функция не определена,
  82. // то выбрасывается ошибка
  83. if (!options || !options.nameResolver) {
  84. throw new InvalidArgumentError(
  85. 'Projection option "nameResolver" is required to resolve %v name.',
  86. schema,
  87. );
  88. }
  89. schema = options.nameResolver(schema);
  90. // если результат разрешающей функции не является
  91. // объектом, то выбрасывается ошибка
  92. if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
  93. throw new InvalidArgumentError(
  94. 'Name resolver must return an Object, but %v was given.',
  95. schema,
  96. );
  97. }
  98. }
  99. // после нормализации схемы в объект,
  100. // выполняется поверхностная проверка
  101. validateProjectionSchema(schema, true);
  102. // если данные не являются объектом или массивом,
  103. // то значение возвращается без изменений
  104. if (data == null || typeof data !== 'object') {
  105. return data;
  106. }
  107. // если данные являются массивом,
  108. // то схема применяется к каждому элементу
  109. if (Array.isArray(data)) {
  110. return data.map(item => projectData(schema, item, options));
  111. }
  112. // если данные являются объектом,
  113. // то проекция создается согласно схеме
  114. const result = {};
  115. const strict = Boolean(options && options.strict);
  116. const scope = (options && options.scope) || undefined;
  117. // в обычном режиме итерация выполняется по ключам исходного
  118. // объекта, а в строгом режиме по ключам, описанным в схеме
  119. // (исключая ключи прототипа Object.keys(x))
  120. const propNames = Object.keys(strict ? schema : data);
  121. propNames.forEach(propName => {
  122. // если свойство отсутствует в исходных
  123. // данных, то свойство игнорируется
  124. if (!(propName in data)) return;
  125. const propOptions = schema[propName];
  126. // проверка доступности свойства для данной
  127. // области проекции (если определена)
  128. if (_shouldSelect(propOptions, strict, scope)) {
  129. const value = data[propName];
  130. // если определена вложенная схема,
  131. // то проекция применяется рекурсивно
  132. if (
  133. propOptions &&
  134. typeof propOptions === 'object' &&
  135. propOptions.schema
  136. ) {
  137. result[propName] = projectData(propOptions.schema, value, options);
  138. }
  139. // иначе значение присваивается
  140. // свойству без изменений
  141. else {
  142. result[propName] = value;
  143. }
  144. }
  145. });
  146. return result;
  147. }
  148. /**
  149. * Should select (internal).
  150. *
  151. * Определяет, следует ли включать свойство в результат.
  152. *
  153. * Приоритет:
  154. * 1. Правило для области.
  155. * 2. Общее правило.
  156. * 4. Режим проекции.
  157. *
  158. * @param {object|boolean} propOptions
  159. * @param {boolean|undefined} strict
  160. * @param {string|undefined} scope
  161. * @returns {boolean}
  162. */
  163. function _shouldSelect(propOptions, strict, scope) {
  164. // если настройки свойства являются логическим значением,
  165. // то значение используется как индикатор видимости
  166. if (typeof propOptions === 'boolean') {
  167. return propOptions;
  168. }
  169. // если настройки свойства являются объектом,
  170. // то проверяется правило области и общее правило
  171. if (
  172. propOptions &&
  173. typeof propOptions === 'object' &&
  174. !Array.isArray(propOptions)
  175. ) {
  176. // если определена область проекции,
  177. // то выполняется проверка правила области
  178. if (
  179. scope &&
  180. typeof scope === 'string' &&
  181. propOptions.scopes &&
  182. typeof propOptions.scopes === 'object' &&
  183. propOptions.scopes[scope] !== undefined
  184. ) {
  185. const scopeOptions = propOptions.scopes[scope];
  186. // если настройки активной области проекции
  187. // являются логическим значением, то значение
  188. // возвращается в качестве результата
  189. if (typeof scopeOptions === 'boolean') {
  190. return scopeOptions;
  191. }
  192. // если настройки активной области проекции
  193. // являются объектом и содержат опцию select,
  194. // то значение опции возвращается как результат
  195. if (
  196. scopeOptions &&
  197. typeof scopeOptions === 'object' &&
  198. !Array.isArray(scopeOptions) &&
  199. typeof scopeOptions.select === 'boolean'
  200. ) {
  201. return scopeOptions.select;
  202. }
  203. }
  204. // если правило видимости для активной области
  205. // проекции не определено, то проверяется наличие
  206. // общего правила
  207. if (typeof propOptions.select === 'boolean') {
  208. return propOptions.select;
  209. }
  210. }
  211. // если правила видимости не определены
  212. // то результат будет зависеть от режима
  213. return !strict;
  214. }