project-data.js 8.7 KB

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