project-data.js 9.0 KB

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