project-data.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import {InvalidArgumentError} from '@e22m4u/js-format';
  2. import {validateProjectionSchema} from './validate-projection-schema.js';
  3. /**
  4. * Project data.
  5. *
  6. * @param {object|Function} schemaOrFactory
  7. * @param {object} data
  8. * @param {object|undefined} options
  9. * @returns {*}
  10. */
  11. export function projectData(schemaOrFactory, data, options = undefined) {
  12. // schemaOrFactory
  13. if (
  14. !schemaOrFactory ||
  15. (typeof schemaOrFactory !== 'object' &&
  16. typeof schemaOrFactory !== 'function') ||
  17. Array.isArray(schemaOrFactory)
  18. ) {
  19. throw new InvalidArgumentError(
  20. 'Projection schema must be an Object or a Function ' +
  21. 'that returns a schema object, but %v was given.',
  22. schemaOrFactory,
  23. );
  24. }
  25. // options
  26. if (options !== undefined) {
  27. if (!options || typeof options !== 'object' || Array.isArray(options)) {
  28. throw new InvalidArgumentError(
  29. 'Parameter "options" must be an Object, but %v was given.',
  30. options,
  31. );
  32. }
  33. // options.strict
  34. if (options.strict !== undefined && typeof options.strict !== 'boolean') {
  35. throw new InvalidArgumentError(
  36. 'Option "strict" must be a Boolean, but %v was given.',
  37. options.strict,
  38. );
  39. }
  40. // options.scope
  41. if (
  42. options.scope !== undefined &&
  43. (!options.scope || typeof options.scope !== 'string')
  44. ) {
  45. throw new InvalidArgumentError(
  46. 'Option "scope" must be a non-empty String, but %v was given.',
  47. options.scope,
  48. );
  49. }
  50. }
  51. const strict = Boolean(options && options.strict);
  52. const scope = (options && options.scope) || undefined;
  53. // если вместо схемы передана фабрика,
  54. // то извлекается фабричное значение
  55. let schema = schemaOrFactory;
  56. if (typeof schemaOrFactory === 'function') {
  57. schema = schemaOrFactory();
  58. // если не удалось извлечь схему проекции,
  59. // то выбрасывается ошибка
  60. if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
  61. throw new InvalidArgumentError(
  62. 'Projection schema factory must return an Object, but %v was given.',
  63. schema,
  64. );
  65. }
  66. }
  67. // валидация полученной схемы проекции
  68. // без проверки вложенных схем (shallowMode)
  69. validateProjectionSchema(schema, true);
  70. // если данные не являются объектом (null, undefined, примитив),
  71. // то значение возвращается без изменений
  72. if (data === null || typeof data !== 'object') {
  73. return data;
  74. }
  75. // если данные являются массивом, то проекция
  76. // применяется к каждому элементу
  77. if (Array.isArray(data)) {
  78. return data.map(item => projectData(schema, item, options));
  79. }
  80. // если данные являются объектом,
  81. // то создается проекция согласно схеме
  82. const result = {};
  83. // в обычном режиме итерация выполняется по ключам исходного
  84. // объекта, а в строгом режиме по ключам, описанным в схеме
  85. // (исключая ключи прототипа Object.keys(x))
  86. const keys = Object.keys(strict ? schema : data);
  87. for (const key of keys) {
  88. // если свойство отсутствует в исходных
  89. // данных, то свойство игнорируется
  90. if (!(key in data)) continue;
  91. const propOptionsOrBoolean = schema[key];
  92. // проверка доступности свойства для данной
  93. // области проекции (если определена)
  94. if (_shouldSelect(propOptionsOrBoolean, strict, scope)) {
  95. const value = data[key];
  96. // если определена вложенная схема,
  97. // то проекция применяется рекурсивно
  98. if (
  99. propOptionsOrBoolean &&
  100. typeof propOptionsOrBoolean === 'object' &&
  101. propOptionsOrBoolean.schema
  102. ) {
  103. result[key] = projectData(propOptionsOrBoolean.schema, value, options);
  104. }
  105. // иначе значение присваивается
  106. // свойству без изменений
  107. else {
  108. result[key] = value;
  109. }
  110. }
  111. }
  112. return result;
  113. }
  114. /**
  115. * Should select (internal).
  116. *
  117. * Определяет, следует ли включать свойство в результат.
  118. * Приоритет: правило для области -> общее правило -> по умолчанию true.
  119. *
  120. * @param {object|boolean|undefined} propOptionsOrBoolean
  121. * @param {boolean|undefined} strict
  122. * @param {string|undefined} scope
  123. * @returns {boolean}
  124. */
  125. function _shouldSelect(propOptionsOrBoolean, strict, scope) {
  126. // если настройки свойства являются логическим значением,
  127. // то значение используется как индикатор видимости
  128. if (typeof propOptionsOrBoolean === 'boolean') {
  129. return propOptionsOrBoolean;
  130. }
  131. // если настройки свойства являются объектом,
  132. // то проверяется правило области и общее правило
  133. if (typeof propOptionsOrBoolean === 'object') {
  134. const propOptions = propOptionsOrBoolean;
  135. // если определена область проекции,
  136. // то выполняется проверка правила области
  137. if (
  138. scope &&
  139. propOptions.scopes &&
  140. typeof propOptions.scopes === 'object' &&
  141. propOptions.scopes[scope] != null
  142. ) {
  143. const scopeOptionsOrBoolean = propOptions.scopes[scope];
  144. // если настройки области являются логическим значением,
  145. // то значение используется как индикатор видимости
  146. if (typeof scopeOptionsOrBoolean === 'boolean') {
  147. return scopeOptionsOrBoolean;
  148. }
  149. // если настройки области являются объектом,
  150. // то используется опция select
  151. if (
  152. scopeOptionsOrBoolean &&
  153. typeof scopeOptionsOrBoolean === 'object' &&
  154. typeof scopeOptionsOrBoolean.select === 'boolean'
  155. ) {
  156. return scopeOptionsOrBoolean.select;
  157. }
  158. }
  159. // если область проекции не указана,
  160. // то проверяется общее правило
  161. if (typeof propOptionsOrBoolean.select === 'boolean') {
  162. return propOptionsOrBoolean.select;
  163. }
  164. }
  165. // если для свойства нет правил, то свойство
  166. // по умолчанию доступно (недоступно в режиме strict)
  167. return !strict;
  168. }